From 6067e28bd08645a542bd5547ac3270893cc1023e Mon Sep 17 00:00:00 2001 From: Pierre Lasorak Date: Mon, 17 Apr 2023 18:49:56 +0200 Subject: [PATCH 01/90] missing import os --- scripts/daqconf_multiru_gen | 2 ++ 1 file changed, 2 insertions(+) diff --git a/scripts/daqconf_multiru_gen b/scripts/daqconf_multiru_gen index 6e8b1dec..126d005c 100755 --- a/scripts/daqconf_multiru_gen +++ b/scripts/daqconf_multiru_gen @@ -476,6 +476,8 @@ def cli(config, base_command_port, hardware_map_file, data_rate_slowdown_factor, dir_names = set() + import os + if os.path.commonprefix(['/cvmfs', readout.default_data_file]) != '/cvmfs': dir_names.add(dirname(readout.default_data_file)) From d80d4d8b5d09da320a276df950ffdaf7600d1356 Mon Sep 17 00:00:00 2001 From: Pierre Lasorak Date: Mon, 17 Apr 2023 18:50:26 +0200 Subject: [PATCH 02/90] add env variable for iomanager host resolution --- python/daqconf/core/conf_utils.py | 1 + 1 file changed, 1 insertion(+) diff --git a/python/daqconf/core/conf_utils.py b/python/daqconf/core/conf_utils.py index 58e48e82..f7b87f87 100644 --- a/python/daqconf/core/conf_utils.py +++ b/python/daqconf/core/conf_utils.py @@ -598,6 +598,7 @@ def generate_boot( "CMD_FAC": "rest://localhost:{APP_PORT}", "CONNECTION_SERVER": resolve_localhost(conf.connectivity_service_host), "CONNECTION_PORT": f"{conf.connectivity_service_port}", + "IOMANAGER_RESOLVE_CONNECTIONS": not conf.use_k8s, "INFO_SVC": info_svc_uri, }, "cmd":"daq_application", From d81555f90b8c6c5ca25d8a0ccc762757d5ccc5b1 Mon Sep 17 00:00:00 2001 From: Pierre Lasorak Date: Wed, 19 Apr 2023 18:27:51 +0200 Subject: [PATCH 03/90] turn env var into int --- python/daqconf/core/conf_utils.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/python/daqconf/core/conf_utils.py b/python/daqconf/core/conf_utils.py index f7b87f87..e02f929a 100644 --- a/python/daqconf/core/conf_utils.py +++ b/python/daqconf/core/conf_utils.py @@ -321,8 +321,8 @@ def make_system_connections(the_system, verbose=False, use_k8s=False, use_connec port = the_system.next_unassigned_port() if not use_connectivity_service or use_k8s else '*' address = f'tcp://{{{endpoint["app"]}}}:{port}' if not use_k8s else f'tcp://{endpoint["app"]}:{port}' conn_id =conn.ConnectionId( uid=endpoint['endpoint'].external_name, data_type=endpoint['endpoint'].data_type) - pubsub_connectionids[endpoint['endpoint'].external_name] = conn.Connection(id=conn_id, - connection_type="kPubSub", + pubsub_connectionids[endpoint['endpoint'].external_name] = conn.Connection(id=conn_id, + connection_type="kPubSub", uri=address ) topic_connectionuids += [endpoint['endpoint'].external_name] @@ -409,7 +409,7 @@ def make_app_command_data(system, app, appkey, verbose=False, use_k8s=False, use # Fill in the "standard" command entries in the command_data structure command_data['init'] = appfwk.Init(modules=mod_specs, connections=system.connections[appkey], - queues=system.queues[appkey], + queues=system.queues[appkey], use_connectivity_service=use_connectivity_service, connectivity_service_interval_ms=connectivity_service_interval) @@ -598,7 +598,7 @@ def generate_boot( "CMD_FAC": "rest://localhost:{APP_PORT}", "CONNECTION_SERVER": resolve_localhost(conf.connectivity_service_host), "CONNECTION_PORT": f"{conf.connectivity_service_port}", - "IOMANAGER_RESOLVE_CONNECTIONS": not conf.use_k8s, + "IOMANAGER_RESOLVE_CONNECTIONS": int(not conf.use_k8s), "INFO_SVC": info_svc_uri, }, "cmd":"daq_application", @@ -648,7 +648,7 @@ def generate_boot( case 2: pass - + if not conf.use_k8s: for app in system.apps.values(): From 59b56e88f91b7bb7f7904ff6958b20ed9b772d43 Mon Sep 17 00:00:00 2001 From: Pierre Lasorak Date: Wed, 31 May 2023 11:51:57 +0200 Subject: [PATCH 04/90] crank up the memory --- scripts/daqconf_multiru_gen | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/scripts/daqconf_multiru_gen b/scripts/daqconf_multiru_gen index 126d005c..128e0f26 100755 --- a/scripts/daqconf_multiru_gen +++ b/scripts/daqconf_multiru_gen @@ -33,10 +33,11 @@ CONTEXT_SETTINGS = dict(help_option_names=['-h', '--help']) @click.option('--hardware-map-file', default='', help="File containing detector hardware map for configuration to run") @click.option('-s', '--data-rate-slowdown-factor', default=0, help="Scale factor for readout internal clock to generate less data") @click.option('--enable-dqm', default=False, is_flag=True, help="Enable generation of DQM apps") +@click.option('--enable-k8s', default=None, is_flag=True, help="Enable Kubernetes (default, use the flag in your configuration file, which is off by default)") @click.option('--op-env', default='', help="Operational environment - used for raw data filename prefix and HDF5 Attribute inside the files") @click.option('--debug', default=False, is_flag=True, help="Switch to get a lot of printout and dot files") @click.argument('json_dir', type=click.Path()) -def cli(config, base_command_port, hardware_map_file, data_rate_slowdown_factor, enable_dqm, op_env, debug, json_dir): +def cli(config, base_command_port, hardware_map_file, data_rate_slowdown_factor, enable_dqm, enable_k8s, op_env, debug, json_dir): output_dir = Path(json_dir) if output_dir.exists(): @@ -53,6 +54,7 @@ def cli(config, base_command_port, hardware_map_file, data_rate_slowdown_factor, if debug: console.log(f"Configuration for daqconf: {config_data.pod()}") + # Get our config objects # Loading this one another time... (first time in config_file.generate_cli_from_schema) moo.otypes.load_types('daqconf/confgen.jsonnet') @@ -87,19 +89,31 @@ def cli(config, base_command_port, hardware_map_file, data_rate_slowdown_factor, dpdk_sender = confgen.dpdk_sender(**config_data.dpdk_sender) if debug: console.log(f"dpdk_sender configuration object: {dpdk_sender.pod()}") + + # Update with command-line options + if enable_k8s is not None: + boot.enable_k8s = enable_k8s + if base_command_port != -1: boot.base_command_port = base_command_port + if hardware_map_file != '': readout.hardware_map_file = hardware_map_file + if data_rate_slowdown_factor != 0: readout.data_rate_slowdown_factor = data_rate_slowdown_factor + dqm.enable_dqm |= enable_dqm + if dqm.impl == 'pocket': dqm.kafka_address = boot.pocket_url + ":30092" + if op_env != '': boot.op_env = op_env + + console.log("Loading dataflow config generator") from daqconf.apps.dataflow_gen import get_dataflow_app if dqm.enable_dqm: @@ -471,7 +485,7 @@ def cli(config, base_command_port, hardware_map_file, data_rate_slowdown_factor, c = card_override if card_override != -1 else dro_config.card the_system.apps[ru_name].resources = { f"felix.cern/flx{c}-data": "1", # requesting FLX{c} - "memory": "32Gi" # yes bro + "memory": "70Gi" # yes bro } dir_names = set() From 3b681533749799d50ae40cee122da79c1d89a188 Mon Sep 17 00:00:00 2001 From: Pierre Lasorak Date: Wed, 31 May 2023 12:44:32 +0200 Subject: [PATCH 05/90] revert iomanager changes in the env --- python/daqconf/core/conf_utils.py | 1 - 1 file changed, 1 deletion(-) diff --git a/python/daqconf/core/conf_utils.py b/python/daqconf/core/conf_utils.py index e02f929a..9e2f9071 100644 --- a/python/daqconf/core/conf_utils.py +++ b/python/daqconf/core/conf_utils.py @@ -598,7 +598,6 @@ def generate_boot( "CMD_FAC": "rest://localhost:{APP_PORT}", "CONNECTION_SERVER": resolve_localhost(conf.connectivity_service_host), "CONNECTION_PORT": f"{conf.connectivity_service_port}", - "IOMANAGER_RESOLVE_CONNECTIONS": int(not conf.use_k8s), "INFO_SVC": info_svc_uri, }, "cmd":"daq_application", From 39db05bd8dd3c5b5a5d3f71b89855d82e8663eda Mon Sep 17 00:00:00 2001 From: Pierre Lasorak Date: Wed, 31 May 2023 17:17:04 +0200 Subject: [PATCH 06/90] this flag was confusing --- scripts/daqconf_multiru_gen | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/scripts/daqconf_multiru_gen b/scripts/daqconf_multiru_gen index 128e0f26..a499d1e4 100755 --- a/scripts/daqconf_multiru_gen +++ b/scripts/daqconf_multiru_gen @@ -33,11 +33,11 @@ CONTEXT_SETTINGS = dict(help_option_names=['-h', '--help']) @click.option('--hardware-map-file', default='', help="File containing detector hardware map for configuration to run") @click.option('-s', '--data-rate-slowdown-factor', default=0, help="Scale factor for readout internal clock to generate less data") @click.option('--enable-dqm', default=False, is_flag=True, help="Enable generation of DQM apps") -@click.option('--enable-k8s', default=None, is_flag=True, help="Enable Kubernetes (default, use the flag in your configuration file, which is off by default)") +@click.option('--disable-k8s', default=False, is_flag=True, help="Disable Kubernetes (default, use the flag in your configuration file, which is off by default)") @click.option('--op-env', default='', help="Operational environment - used for raw data filename prefix and HDF5 Attribute inside the files") @click.option('--debug', default=False, is_flag=True, help="Switch to get a lot of printout and dot files") @click.argument('json_dir', type=click.Path()) -def cli(config, base_command_port, hardware_map_file, data_rate_slowdown_factor, enable_dqm, enable_k8s, op_env, debug, json_dir): +def cli(config, base_command_port, hardware_map_file, data_rate_slowdown_factor, enable_dqm, disable_k8s, op_env, debug, json_dir): output_dir = Path(json_dir) if output_dir.exists(): @@ -92,8 +92,8 @@ def cli(config, base_command_port, hardware_map_file, data_rate_slowdown_factor, # Update with command-line options - if enable_k8s is not None: - boot.enable_k8s = enable_k8s + if disable_k8s: + boot.use_k8s = False if base_command_port != -1: boot.base_command_port = base_command_port From ccace46a190ce968aa53f2d9b1a43f01d9d967cb Mon Sep 17 00:00:00 2001 From: Alessandro Thea Date: Wed, 31 May 2023 19:47:15 +0200 Subject: [PATCH 07/90] cpu pinning file treated as relative to config file unless the path is absolute --- scripts/daqconf_multiru_gen | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/scripts/daqconf_multiru_gen b/scripts/daqconf_multiru_gen index 3b382b02..cdc2b779 100755 --- a/scripts/daqconf_multiru_gen +++ b/scripts/daqconf_multiru_gen @@ -57,7 +57,7 @@ def cli(config, base_command_port, detector_readout_map_file, data_rate_slowdown debug_dir.mkdir(parents=True) config_data = config[0] - config_file = config[1] + config_file = Path(config[1]) if debug: console.log(f"Configuration for daqconf: {config_data.pod()}") @@ -757,7 +757,11 @@ def cli(config, base_command_port, detector_readout_map_file, data_rate_slowdown if readout.thread_pinning_file != "": - resolved_thread_pinning_file = Path(os.path.expandvars(readout.thread_pinning_file)).expanduser().absolute() + + resolved_thread_pinning_file = Path(os.path.expandvars(readout.thread_pinning_file)).expanduser() + if not resolved_thread_pinning_file.is_absolute(): + resolved_thread_pinning_file = config_file.parent / resolved_thread_pinning_file + if not resolved_thread_pinning_file.exists(): raise RuntimeError(f'Cannot find the file {readout.thread_pinning_file} ({resolved_thread_pinning_file})') @@ -767,7 +771,7 @@ def cli(config, base_command_port, detector_readout_map_file, data_rate_slowdown "readout-affinity.py --pinfile ${DUNEDAQ_THREAD_PIN_FILE}" ], "env": { - "DUNEDAQ_THREAD_PIN_FILE": wresolved_thread_pinning_file, + "DUNEDAQ_THREAD_PIN_FILE": resolved_thread_pinning_file.as_posix(), "LD_LIBRARY_PATH": "getenv", "PATH": "getenv" } @@ -780,7 +784,7 @@ def cli(config, base_command_port, detector_readout_map_file, data_rate_slowdown console.log(f"MDAapp config generated in {output_dir}") - write_metadata_file(output_dir, "daqconf_multiru_gen", config_file) + write_metadata_file(output_dir, "daqconf_multiru_gen", config_file.as_posix()) write_config_file( output_dir, basename(config_file) if config_file else "default.json", From 04a4a50609df5cf3ba7b88b5268866adfd23d7ac Mon Sep 17 00:00:00 2001 From: Alessandro Thea Date: Wed, 31 May 2023 19:49:30 +0200 Subject: [PATCH 08/90] Adding resolve stage --- scripts/daqconf_multiru_gen | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/daqconf_multiru_gen b/scripts/daqconf_multiru_gen index cdc2b779..dc94b444 100755 --- a/scripts/daqconf_multiru_gen +++ b/scripts/daqconf_multiru_gen @@ -771,7 +771,7 @@ def cli(config, base_command_port, detector_readout_map_file, data_rate_slowdown "readout-affinity.py --pinfile ${DUNEDAQ_THREAD_PIN_FILE}" ], "env": { - "DUNEDAQ_THREAD_PIN_FILE": resolved_thread_pinning_file.as_posix(), + "DUNEDAQ_THREAD_PIN_FILE": resolved_thread_pinning_file.resolve().as_posix(), "LD_LIBRARY_PATH": "getenv", "PATH": "getenv" } From a7c1ce1487ce144649fd826ead7baf58f475dda0 Mon Sep 17 00:00:00 2001 From: Alessandro Thea Date: Wed, 31 May 2023 22:55:14 +0200 Subject: [PATCH 09/90] Fixing commandline --- scripts/daqconf_multiru_gen | 51 ++++++++++++++++++++++++++----------- 1 file changed, 36 insertions(+), 15 deletions(-) diff --git a/scripts/daqconf_multiru_gen b/scripts/daqconf_multiru_gen index 6c67af6c..0c83ff83 100755 --- a/scripts/daqconf_multiru_gen +++ b/scripts/daqconf_multiru_gen @@ -33,25 +33,44 @@ import dunedaq.detchannelmaps.hardwaremapservice as hwms CONTEXT_SETTINGS = dict(help_option_names=['-h', '--help']) @click.command(context_settings=CONTEXT_SETTINGS) @generate_cli_from_schema('daqconf/confgen.jsonnet', 'daqconf_multiru_gen', 'dataflowapp') -@click.option('--disable-k8s', default=False, is_flag=True, help="Disable Kubernetes (default, use the flag in your configuration file, which is off by default)") -@click.option('--base-command-port', type=int, default=-1, help="Base port of application command endpoints") -@click.option('--detector-readout-map-file', default='', help="File containing detector detector-readout map for configuration to run") +@click.option('--force-pm', default=None, type=click.Choice(['ssh', 'k8s']), help="Force process manager") +@click.option('--base-command-port', type=int, default=None, help="Base port of application command endpoints") +@click.option('--detector-readout-map-file', default=None, help="File containing detector detector-readout map for configuration to run") @click.option('-s', '--data-rate-slowdown-factor', default=0, help="Scale factor for readout internal clock to generate less data") +@click.option('--file-label', default=None, help="File - used for raw data filename prefix") @click.option('--enable-dqm', default=False, is_flag=True, help="Enable generation of DQM apps") -@click.option('--op-env', default='', help="Operational environment - used for raw data filename prefix and HDF5 Attribute inside the files") -@click.option('--file-label', default='', help="File - used for raw data filename prefix") -@click.option('-a', '--only-check-args', default=False, is_flag=True, help="Dry run, do not generate output files") +@click.option('-a', '--only-check-args', default=False, is_flag=True, help="Check input arguments and quit") @click.option('-n', '--dry-run', default=False, is_flag=True, help="Dry run, do not generate output files") +@click.option('-f', '--force', default=False, is_flag=True, help="Force configuration generation - delete ") @click.option('--debug', default=False, is_flag=True, help="Switch to get a lot of printout and dot files") @click.argument('json_dir', type=click.Path()) -def cli(config, disable_k8s, base_command_port, detector_readout_map_file, data_rate_slowdown_factor, enable_dqm, file_label, only_check_args, dry_run, debug, json_dir): +def cli( + config, + force_pm, + base_command_port, + detector_readout_map_file, + data_rate_slowdown_factor, + enable_dqm, + file_label, + only_check_args, + dry_run, + force, + debug, + json_dir + ): if only_check_args: return output_dir = Path(json_dir) - if output_dir.exists() and not dry_run: - raise RuntimeError(f"Directory {output_dir} already exists") + if output_dir.exists(): + if dry_run: + pass + elif force: + # Delete output folder if it exists + shutil.rmtree(output_dir) + else: + raise RuntimeError(f"Directory {output_dir} already exists") debug_dir = output_dir / 'debug' @@ -100,15 +119,14 @@ def cli(config, disable_k8s, base_command_port, detector_readout_map_file, data_ if debug: console.log(f"dpdk_sender configuration object: {dpdk_sender.pod()}") - # Update with command-line options - if disable_k8s: - boot.use_k8s = False + if force_pm is not None: + boot.use_k8s = (force_pm == 'k8s') - if base_command_port != -1: + if base_command_port is not None: boot.base_command_port = base_command_port - if detector_readout_map_file != '': + if detector_readout_map_file is not None: readout.detector_readout_map_file = detector_readout_map_file if data_rate_slowdown_factor != 0: readout.data_rate_slowdown_factor = data_rate_slowdown_factor @@ -117,9 +135,12 @@ def cli(config, disable_k8s, base_command_port, detector_readout_map_file, data_ if dqm.impl == 'pocket': dqm.kafka_address = boot.pocket_url + ":30092" + # if op_env != '': # boot.op_env = op_env + file_label = file_label if file_label is not None else boot.op_env + console.log("Loading dataflow config generator") @@ -558,7 +579,7 @@ def cli(config, disable_k8s, base_command_port, detector_readout_map_file, data_ OUTPUT_PATHS = df_config.output_paths, APP_NAME=app_name, OPERATIONAL_ENVIRONMENT = boot.op_env, - FILE_LABEL = file_label if file_label else boot.op_env, + FILE_LABEL = file_label, DATA_STORE_MODE=df_config.data_store_mode, MAX_FILE_SIZE = df_config.max_file_size, MAX_TRIGGER_RECORD_WINDOW = df_config.max_trigger_record_window, From 056d511d63fda5e0bc55eb5a5e545e19f438fe17 Mon Sep 17 00:00:00 2001 From: Alessandro Thea Date: Thu, 1 Jun 2023 07:47:55 +0200 Subject: [PATCH 10/90] small fixes --- python/daqconf/apps/readout_gen.py | 2 - schema/daqconf/confgen.jsonnet | 77 ++++++++++++++++-------------- scripts/daqconf_multiru_gen | 10 +++- 3 files changed, 50 insertions(+), 39 deletions(-) diff --git a/python/daqconf/apps/readout_gen.py b/python/daqconf/apps/readout_gen.py index 5e1b77e0..8b2caa82 100644 --- a/python/daqconf/apps/readout_gen.py +++ b/python/daqconf/apps/readout_gen.py @@ -752,7 +752,6 @@ def create_readout_app( TPG_CHANNEL_MAP= "ProtoDUNESP1ChannelMap", DATA_REQUEST_TIMEOUT=1000, FRAGMENT_SEND_TIMEOUT=10, - READOUT_SENDS_TP_FRAGMENTS=False, EAL_ARGS='-l 0-1 -n 3 -- -m [0:1].0 -j', NUMA_ID=0, LATENCY_BUFFER_SIZE=499968, @@ -858,7 +857,6 @@ def create_readout_app( modules += dlhs_mods # Add the TP datalink handlers - #if TPG_ENABLED and READOUT_SENDS_TP_FRAGMENTS: if TPG_ENABLED: tps = { k:v for k,v in SOURCEID_BROKER.get_all_source_ids("Trigger").items() if isinstance(v, ReadoutUnitDescriptor ) and v==RU_DESCRIPTOR} if len(tps) != 1: diff --git a/schema/daqconf/confgen.jsonnet b/schema/daqconf/confgen.jsonnet index 3f6d89be..eecf2d93 100755 --- a/schema/daqconf/confgen.jsonnet +++ b/schema/daqconf/confgen.jsonnet @@ -19,9 +19,10 @@ local cs = { monitoring_dest: s.enum( "MonitoringDest", ["local", "cern", "pocket"]), path: s.string( "Path", doc="Location on a filesystem"), paths: s.sequence( "Paths", self.path, doc="Multiple paths"), - host: s.string( "Host", moo.re.dnshost, doc="A hostname"), + host: s.string( "Host", moo.re.dnshost, doc="A hostname"), hosts: s.sequence( "Hosts", self.host, "Multiple hosts"), string: s.string( "Str", doc="Generic string"), + strings: s.sequence( "Strings", self.string, doc="List of strings"), tpg_channel_map: s.enum( "TPGChannelMap", ["VDColdboxChannelMap", "ProtoDUNESP1ChannelMap", "PD2HDChannelMap", "HDColdboxChannelMap"]), dqm_channel_map: s.enum( "DQMChannelMap", ['HD', 'VD', 'PD2HD', 'HDCB']), dqm_params: s.sequence( "DQMParams", self.count, doc="Parameters for DQM (fixme)"), @@ -32,42 +33,39 @@ local cs = { readout_time: s.number( "ROTime", "i8", doc="A readout time in ticks"), channel_list: s.sequence( "ChannelList", self.count, doc="List of offline channels to be masked out from the TPHandler"), - numa_exception: s.record( "NUMAException", [ - s.field( "host", self.host, default='localhost', doc="Host of exception"), - s.field( "card", self.count, default=0, doc="Card ID of exception"), - s.field( "numa_id", self.count, default=0, doc="NUMA ID of exception"), - s.field( "felix_card_id", self.count, default=-1, doc="CARD ID override, -1 indicates no override"), - s.field( "latency_buffer_numa_aware", self.flag, default=false, doc="Enable NUMA-aware mode for the Latency Buffer"), - s.field( "latency_buffer_preallocation", self.flag, default=false, doc="Enable Latency Buffer preallocation"), - ], doc="Exception to the default NUMA ID for FELIX cards"), - numa_exceptions: s.sequence( "NUMAExceptions", self.numa_exception, doc="Exceptions to the default NUMA ID"), - numa_config: s.record("numa_config", [ - s.field( "default_id", self.count, default=0, doc="Default NUMA ID for FELIX cards"), - s.field( "default_latency_numa_aware", self.flag, default=false, doc="Default for Latency Buffer NUMA awareness"), - s.field( "default_latency_preallocation", self.flag, default=false, doc="Default for Latency Buffer Preallocation"), - s.field( "exceptions", self.numa_exceptions, default=[], doc="Exceptions to the default NUMA ID"), - ]), + boot: s.record("boot", [ + s.field( "op_env", self.string, default='swtest', doc="Operational environment - used for raw data filename prefix and HDF5 Attribute inside the files"), s.field( "base_command_port", self.port, default=3333, doc="Base port of application command endpoints"), + # Obscure + s.field( "RTE_script_settings", self.three_choice, default=0, doc="0 - Use an RTE script iff not in a dev environment, 1 - Always use RTE, 2 - never use RTE"), + s.field( "capture_env_vars", self.strings, default=['TIMING_SHARE', 'DETCHANNELMAPS_SHARE'], doc="List of variables to capture from the environment"), s.field( "disable_trace", self.flag, false, doc="Do not enable TRACE (default TRACE_FILE is /tmp/trace_buffer_${HOSTNAME}_${USER})"), s.field( "opmon_impl", self.monitoring_dest, default='local', doc="Info collector service implementation to use"), s.field( "ers_impl", self.monitoring_dest, default='local', doc="ERS destination (Kafka used for cern and pocket)"), s.field( "pocket_url", self.host, default='127.0.0.1', doc="URL for connecting to Pocket services"), - s.field( "image", self.string, default="dunedaq/c8-minimal", doc="Which docker image to use"), + + # K8S s.field( "use_k8s", self.flag, default=false, doc="Whether to use k8s"), - s.field( "op_env", self.string, default='swtest', doc="Operational environment - used for raw data filename prefix and HDF5 Attribute inside the files"), - s.field( "data_request_timeout_ms", self.count, default=1000, doc="The baseline data request timeout that will be used by modules in the Readout and Trigger subsystems (i.e. any module that produces data fragments). Downstream timeouts, such as the trigger-record-building timeout, are derived from this."), + s.field( "image", self.string, default="dunedaq/c8-minimal", doc="Which docker image to use"), + + # Connectivity Service s.field( "use_connectivity_service", self.flag, default=true, doc="Whether to use the ConnectivityService to manage connections"), s.field( "start_connectivity_service", self.flag, default=true, doc="Whether to use the ConnectivityService to manage connections"), s.field( "connectivity_service_threads", self.count, default=2, doc="Number of threads for the gunicorn server that serves connection info"), s.field( "connectivity_service_host", self.host, default='localhost', doc="Hostname for the ConnectivityService"), s.field( "connectivity_service_port", self.port, default=15000, doc="Port for the ConnectivityService"), s.field( "connectivity_service_interval", self.count, default=1000, doc="Publish interval for the ConnectivityService"), - s.field( "RTE_script_settings", self.three_choice, default=0, doc="0 - Use an RTE script iff not in a dev environment, 1 - Always use RTE, 2 - never use RTE"), + + # To move away s.field( "use_data_network", self.flag, default = false, doc="Whether to use the data network (Won't work with k8s)"), + s.field( "data_request_timeout_ms", self.count, default=1000, doc="The baseline data request timeout that will be used by modules in the Readout and Trigger subsystems (i.e. any module that produces data fragments). Downstream timeouts, such as the trigger-record-building timeout, are derived from this."), ]), + + + timing: s.record("timing", [ s.field( "timing_session_name", self.string, default="", doc="Name of the global timing session to use, for timing commands"), s.field( "host_tprtc", self.host, default='localhost', doc='Host to run the timing partition controller app on'), @@ -108,20 +106,36 @@ local cs = { # ctb options s.field( "use_ctb_hsi", self.flag, default=false, doc='Flag to control whether CTB HSI config is generated. Default is false'), s.field( "host_ctb_hsi", self.host, default='localhost', doc='Host to run the HSI app on'), - s.field("hlt_triggers", ctbmodule.Hlt_trigger_seq, []), - s.field("beam_llt_triggers", ctbmodule.Llt_mask_trigger_seq, []), - s.field("crt_llt_triggers", ctbmodule.Llt_count_trigger_seq, []), - s.field("pds_llt_triggers", ctbmodule.Llt_count_trigger_seq, []), - s.field("fake_trig_1", ctbmodule.Randomtrigger, ctbmodule.Randomtrigger), - s.field("fake_trig_2", ctbmodule.Randomtrigger, ctbmodule.Randomtrigger) + s.field( "hlt_triggers", ctbmodule.Hlt_trigger_seq, []), + s.field( "beam_llt_triggers", ctbmodule.Llt_mask_trigger_seq, []), + s.field( "crt_llt_triggers", ctbmodule.Llt_count_trigger_seq, []), + s.field( "pds_llt_triggers", ctbmodule.Llt_count_trigger_seq, []), + s.field( "fake_trig_1", ctbmodule.Randomtrigger, ctbmodule.Randomtrigger), + s.field( "fake_trig_2", ctbmodule.Randomtrigger, ctbmodule.Randomtrigger) ]), data_file_entry: s.record("data_file_entry", [ - s.field("data_file", self.path, default='./frames.bin', doc="File containing data frames to be replayed by the fake cards. Former -d. Uses the asset manager, can also be 'asset://checksum/somelonghash', or 'file://somewhere/frames.bin' or 'frames.bin'"), - s.field("detector_id", self.count, default=3, doc="Detector ID that this file applies to"), + s.field( "data_file", self.path, default='./frames.bin', doc="File containing data frames to be replayed by the fake cards. Former -d. Uses the asset manager, can also be 'asset://checksum/somelonghash', or 'file://somewhere/frames.bin' or 'frames.bin'"), + s.field( "detector_id", self.count, default=3, doc="Detector ID that this file applies to"), ]), data_files: s.sequence("data_files", self.data_file_entry), + numa_exception: s.record( "NUMAException", [ + s.field( "host", self.host, default='localhost', doc="Host of exception"), + s.field( "card", self.count, default=0, doc="Card ID of exception"), + s.field( "numa_id", self.count, default=0, doc="NUMA ID of exception"), + s.field( "felix_card_id", self.count, default=-1, doc="CARD ID override, -1 indicates no override"), + s.field( "latency_buffer_numa_aware", self.flag, default=false, doc="Enable NUMA-aware mode for the Latency Buffer"), + s.field( "latency_buffer_preallocation", self.flag, default=false, doc="Enable Latency Buffer preallocation"), + ], doc="Exception to the default NUMA ID for FELIX cards"), + numa_exceptions: s.sequence( "NUMAExceptions", self.numa_exception, doc="Exceptions to the default NUMA ID"), + numa_config: s.record("numa_config", [ + s.field( "default_id", self.count, default=0, doc="Default NUMA ID for FELIX cards"), + s.field( "default_latency_numa_aware", self.flag, default=false, doc="Default for Latency Buffer NUMA awareness"), + s.field( "default_latency_preallocation", self.flag, default=false, doc="Default for Latency Buffer Preallocation"), + s.field( "exceptions", self.numa_exceptions, default=[], doc="Exceptions to the default NUMA ID"), + ]), + readout: s.record("readout", [ s.field( "detector_readout_map_file", self.path, default='./DetectorReadoutMap.json', doc="File containing detector hardware map for configuration to run"), s.field( "emulator_mode", self.flag, default=false, doc="If active, timestamps of data frames are overwritten when processed by the readout. This is necessary if the felix card does not set correct timestamps. Former -e"), @@ -130,8 +144,6 @@ local cs = { s.field( "clock_speed_hz", self.freq, default=62500000), s.field( "default_data_file", self.path, default='asset://?label=ProtoWIB&subsystem=readout', doc="File containing data frames to be replayed by the fake cards. Former -d. Uses the asset manager, can also be 'asset://?checksum=somelonghash', or 'file://somewhere/frames.bin' or 'frames.bin'"), s.field( "data_files", self.data_files, default=[], doc="Files to use by detector type"), - // s.field( "use_felix", self.flag, default=false, doc="Use real felix cards instead of fake ones. Former -f"), - // s.field( "eth_mode", self.flag, default=false, doc="Use ethernet packet format"), s.field( "use_fake_cards", self.flag, default=false, doc="Use fake cards"), s.field( "latency_buffer_size", self.count, default=499968, doc="Size of the latency buffers (in number of elements)"), s.field( "fragment_send_timeout_ms", self.count, default=10, doc="The send timeout that will be used in the readout modules when sending fragments downstream (i.e. to the TRB)."), @@ -139,14 +151,9 @@ local cs = { s.field( "tpg_threshold", self.count, default=120, doc="Select TPG threshold"), s.field( "tpg_algorithm", self.string, default="SWTPG", doc="Select TPG algorithm (SWTPG, AbsRS)"), s.field( "tpg_channel_mask", self.channel_list, default=[], doc="List of offline channels to be masked out from the TPHandler"), - // s.field( "enable_firmware_tpg", self.flag, default=false, doc="Enable firmware TPG"), - // s.field( "dtp_connections_file", self.path, default="${DTPCONTROLS_SHARE}/config/dtp_connections.xml", doc="DTP connections file"), - // s.field( "firmware_hit_threshold", self.count, default=20, doc="firmware hitfinder threshold"), s.field( "enable_raw_recording", self.flag, default=false, doc="Add queues and modules necessary for the record command"), s.field( "raw_recording_output_dir", self.path, default='.', doc="Output directory where recorded data is written to. Data for each link is written to a separate file"), s.field( "use_fake_data_producers", self.flag, default=false, doc="Use fake data producers that respond with empty fragments immediately instead of (fake) cards and DLHs"), - s.field( "readout_sends_tp_fragments",self.flag, default=false, doc="Send TP Fragments from Readout to Dataflow (via enabling TP Fragment links in MLT)"), - // s.field( "enable_dpdk_reader", self.flag, default=false, doc="Enable sending frames using DPDK"), s.field( "host_dpdk_reader", self.hosts, default=['np04-srv-022'], doc="Which host to use to receive frames"), s.field( "eal_args", self.string, default='-l 0-1 -n 3 -- -m [0:1].0 -j', doc='Args passed to the EAL in DPDK'), s.field( "base_source_ip", self.string, default='10.73.139.', doc='First part of the IP of the source'), diff --git a/scripts/daqconf_multiru_gen b/scripts/daqconf_multiru_gen index 0c83ff83..85504434 100755 --- a/scripts/daqconf_multiru_gen +++ b/scripts/daqconf_multiru_gen @@ -67,6 +67,7 @@ def cli( if dry_run: pass elif force: + console.log(f"Removing existing {output_dir}") # Delete output folder if it exists shutil.rmtree(output_dir) else: @@ -122,14 +123,20 @@ def cli( # Update with command-line options if force_pm is not None: boot.use_k8s = (force_pm == 'k8s') + console.log(f"boot.use_k8s set to {boot.use_k8s}") if base_command_port is not None: - boot.base_command_port = base_command_port + boot.base_command_port = base_command_port + console.log(f"boot.base_command_port set to {boot.base_command_port}") + if detector_readout_map_file is not None: readout.detector_readout_map_file = detector_readout_map_file + console.log(f"readout.detector_readout_map_file set to {readout.detector_readout_map_file}") + if data_rate_slowdown_factor != 0: readout.data_rate_slowdown_factor = data_rate_slowdown_factor + console.log(f"readout.data_rate_slowdown_factor set to {readout.data_rate_slowdown_factor}") dqm.enable_dqm |= enable_dqm @@ -489,7 +496,6 @@ def cli( LATENCY_BUFFER_SIZE=readout.latency_buffer_size, DATA_REQUEST_TIMEOUT=readout_data_request_timeout, FRAGMENT_SEND_TIMEOUT=readout.fragment_send_timeout_ms, - READOUT_SENDS_TP_FRAGMENTS = readout.readout_sends_tp_fragments, EAL_ARGS=readout.eal_args, NUMA_ID = numa_id, LATENCY_BUFFER_NUMA_AWARE = latency_numa, From 7e7bb74f108f15426772b1456d976798c382c7ca Mon Sep 17 00:00:00 2001 From: Alessandro Thea Date: Thu, 1 Jun 2023 07:49:37 +0200 Subject: [PATCH 11/90] Adding -m flag to daqconf --- scripts/daqconf_multiru_gen | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/daqconf_multiru_gen b/scripts/daqconf_multiru_gen index 85504434..bd233180 100755 --- a/scripts/daqconf_multiru_gen +++ b/scripts/daqconf_multiru_gen @@ -35,7 +35,7 @@ CONTEXT_SETTINGS = dict(help_option_names=['-h', '--help']) @generate_cli_from_schema('daqconf/confgen.jsonnet', 'daqconf_multiru_gen', 'dataflowapp') @click.option('--force-pm', default=None, type=click.Choice(['ssh', 'k8s']), help="Force process manager") @click.option('--base-command-port', type=int, default=None, help="Base port of application command endpoints") -@click.option('--detector-readout-map-file', default=None, help="File containing detector detector-readout map for configuration to run") +@click.option('-m', '--detector-readout-map-file', default=None, help="File containing detector detector-readout map for configuration to run") @click.option('-s', '--data-rate-slowdown-factor', default=0, help="Scale factor for readout internal clock to generate less data") @click.option('--file-label', default=None, help="File - used for raw data filename prefix") @click.option('--enable-dqm', default=False, is_flag=True, help="Enable generation of DQM apps") From c29ba962d2be1433053b26bd09e0a8dcde2b47c2 Mon Sep 17 00:00:00 2001 From: Alessandro Thea Date: Tue, 6 Jun 2023 15:15:59 +0200 Subject: [PATCH 12/90] working on boot data --- python/daqconf/core/conf_utils.py | 117 +++++++++++++++++------------- schema/daqconf/confgen.jsonnet | 4 +- scripts/daqconf_multiru_gen | 25 +++---- 3 files changed, 81 insertions(+), 65 deletions(-) diff --git a/python/daqconf/core/conf_utils.py b/python/daqconf/core/conf_utils.py index 9e2f9071..c321ae70 100644 --- a/python/daqconf/core/conf_utils.py +++ b/python/daqconf/core/conf_utils.py @@ -504,7 +504,7 @@ def update_with_ssh_boot_data ( -def update_with_k8s_boot_data( +def add_k8s_app_boot_data( boot_data: dict, apps: list, image: str, @@ -547,59 +547,76 @@ def resolve_localhost(host): return host def generate_boot( - conf, + boot_conf, system, verbose=False, control_to_data_network = None) -> dict: """ Generate the dictionary that will become the boot.json file """ + + info_svc_uri_map = { + 'cern': "kafka://monkafka.cern.ch:30092/opmon", + 'pocket': f"kafka://{boot_conf.pocket_url}:30092/opmon", + 'local': "file://info_{APP_NAME}_{APP_PORT}.json" + } + ers_settings=dict() - if conf.ers_impl == 'cern': + if boot_conf.ers_impl == 'cern': use_kafka = True ers_settings["INFO"] = "erstrace,throttle,lstdout,erskafka(monkafka.cern.ch:30092)" ers_settings["WARNING"] = "erstrace,throttle,lstdout,erskafka(monkafka.cern.ch:30092)" ers_settings["ERROR"] = "erstrace,throttle,lstdout,erskafka(monkafka.cern.ch:30092)" ers_settings["FATAL"] = "erstrace,lstdout,erskafka(monkafka.cern.ch:30092)" - elif conf.ers_impl == 'pocket': + elif boot_conf.ers_impl == 'pocket': use_kafka = True - ers_settings["INFO"] = "erstrace,throttle,lstdout,erskafka(" + conf.pocket_url + ":30092)" - ers_settings["WARNING"] = "erstrace,throttle,lstdout,erskafka(" + conf.pocket_url + ":30092)" - ers_settings["ERROR"] = "erstrace,throttle,lstdout,erskafka(" + conf.pocket_url + ":30092)" - ers_settings["FATAL"] = "erstrace,lstdout,erskafka(" + conf.pocket_url + ":30092)" - else: + ers_settings["INFO"] = "erstrace,throttle,lstdout,erskafka(" + boot_conf.pocket_url + ":30092)" + ers_settings["WARNING"] = "erstrace,throttle,lstdout,erskafka(" + boot_conf.pocket_url + ":30092)" + ers_settings["ERROR"] = "erstrace,throttle,lstdout,erskafka(" + boot_conf.pocket_url + ":30092)" + ers_settings["FATAL"] = "erstrace,lstdout,erskafka(" + boot_conf.pocket_url + ":30092)" + elif boot_conf.ers_impl == 'local': use_kafka = False ers_settings["INFO"] = "erstrace,throttle,lstdout" ers_settings["WARNING"] = "erstrace,throttle,lstdout" ers_settings["ERROR"] = "erstrace,throttle,lstdout" ers_settings["FATAL"] = "erstrace,lstdout" - - if conf.opmon_impl == 'cern': - info_svc_uri = "kafka://monkafka.cern.ch:30092/opmon" - elif conf.opmon_impl == 'pocket': - info_svc_uri = "kafka://" + conf.pocket_url + ":30092/opmon" else: - info_svc_uri = "file://info_{APP_NAME}_{APP_PORT}.json" + raise ValueError(f"Unknown boot_conf.ers_impl value {boot_conf.ers_impl}") - daq_app_exec_name = "daq_application_ssh" if not conf.use_k8s else "daq_application_k8s" + info_svc_uri = info_svc_uri_map[boot_conf.opmon_impl] - daq_app_specs = { - daq_app_exec_name : { - "comment": "Application profile using PATH variables (lower start time)", - "env":{ - "CET_PLUGIN_PATH": "getenv", - "DETCHANNELMAPS_SHARE": "getenv", - "DUNEDAQ_SHARE_PATH": "getenv", - "TIMING_SHARE": "getenv", - "LD_LIBRARY_PATH": "getenv", - "PATH": "getenv", + daq_app_exec_name = f"daq_application_{boot_conf.process_manager}" + + capture_paths = [ + 'PATH', + 'LD_LIBRARY_PATH', + 'CET_PLUGIN_PATH', + 'DUNEDAQ_SHARE_PATH' + ] + + app_env = { "TRACE_FILE": "getenv:/tmp/trace_buffer_{APP_HOST}_{DUNEDAQ_PARTITION}", "CMD_FAC": "rest://localhost:{APP_PORT}", - "CONNECTION_SERVER": resolve_localhost(conf.connectivity_service_host), - "CONNECTION_PORT": f"{conf.connectivity_service_port}", + "CONNECTION_SERVER": resolve_localhost(boot_conf.connectivity_service_host), + "CONNECTION_PORT": f"{boot_conf.connectivity_service_port}", "INFO_SVC": info_svc_uri, - }, + } + + app_env.update({ + p:'getenv' for p in capture_paths + }) + + app_env.update({ + v:'getenv' for v in boot_conf.capture_env_vars + }) + + + + daq_app_specs = { + daq_app_exec_name : { + "comment": "Application profile using PATH variables (lower start time)", + "env": app_env, "cmd":"daq_application", "args": [ "--name", @@ -633,10 +650,10 @@ def generate_boot( if use_kafka: boot["env"]["DUNEDAQ_ERS_STREAM_LIBS"] = "erskafka" - if conf.disable_trace: + if boot_conf.disable_trace: del boot["exec"][daq_app_exec_name]["env"]["TRACE_FILE"] - match conf.RTE_script_settings: + match boot_conf.RTE_script_settings: case 0: if (release_or_dev() == 'rel'): boot['rte_script'] = get_rte_script() @@ -649,38 +666,41 @@ def generate_boot( - if not conf.use_k8s: + if boot_conf.process_manager == 'ssh': for app in system.apps.values(): app.host = resolve_localhost(app.host) update_with_ssh_boot_data( boot_data = boot, apps = system.apps, - base_command_port = conf.base_command_port, + base_command_port = boot_conf.base_command_port, verbose = verbose, control_to_data_network = control_to_data_network, ) - else: + elif boot_conf.process_manager == 'k8s': # ARGGGGG (MASSIVE WARNING SIGN HERE) ruapps = [app for app in system.apps.keys() if app[:2] == 'ru'] dfapps = [app for app in system.apps.keys() if app[:2] == 'df'] otherapps = [app for app in system.apps.keys() if not app in ruapps + dfapps] boot_order = ruapps + dfapps + otherapps - update_with_k8s_boot_data( + add_k8s_app_boot_data( boot_data = boot, apps = system.apps, boot_order = boot_order, - image = conf.image, - base_command_port = conf.base_command_port, + image = boot_conf.image, + base_command_port = boot_conf.base_command_port, verbose = verbose, control_to_data_network = control_to_data_network, ) + else: + raise ValueError(f"Unknown boot_conf.process_manager value {boot_conf.process_manager}") + - if conf.start_connectivity_service: - if conf.use_k8s: + if boot_conf.start_connectivity_service: + if boot_conf.process_manager == 'k8s': raise RuntimeError( - 'Starting connectivity service only supported with ssh.\n') + 'Starting connectivity service only supported with ssh') # CONNECTION_PORT will be updatd by nanorc remove this entry daq_app_specs[daq_app_exec_name]["env"].pop("CONNECTION_PORT") @@ -688,7 +708,7 @@ def generate_boot( "connectionservice": { "exec": "consvc_ssh", "host": "connectionservice", - "port": conf.connectivity_service_port, + "port": boot_conf.connectivity_service_port, "update-env": { "CONNECTION_PORT": "{APP_PORT}" } @@ -700,7 +720,7 @@ def generate_boot( "--bind=0.0.0.0:{APP_PORT}", "--workers=1", "--worker-class=gthread", - f"--threads={conf.connectivity_service_threads}", + f"--threads={boot_conf.connectivity_service_threads}", "--timeout=0", "--pid={APP_NAME}_{APP_PORT}.pid", "connection-service.connection-flask:app" @@ -717,9 +737,9 @@ def generate_boot( boot["services"]={} boot["services"].update(consvc) boot["exec"].update(consvc_exec) - conf.connectivity_service_host = resolve_localhost(conf.connectivity_service_host) + boot_conf.connectivity_service_host = resolve_localhost(boot_conf.connectivity_service_host) boot["hosts-ctrl"].update({"connectionservice": - conf.connectivity_service_host}) + boot_conf.connectivity_service_host}) return boot @@ -768,7 +788,7 @@ def make_app_json(app_name, app_command_data, data_dir, verbose=False): with open(data_dir / f'{app_name}_{c}.json', 'w') as f: json.dump(app_command_data[c].pod(), f, indent=4, sort_keys=True) -def make_system_command_datas(daqconf:dict, the_system, forced_deps=[], verbose:bool=False, control_to_data_network:Callable[[str],str]=None) -> dict: +def make_system_command_datas(boot_conf:dict, the_system, forced_deps=[], verbose:bool=False, control_to_data_network:Callable[[str],str]=None) -> dict: """Generate the dictionary of commands and their data for the entire system""" # if the_system.app_start_order is None: @@ -782,11 +802,6 @@ def make_system_command_datas(daqconf:dict, the_system, forced_deps=[], verbose: cfg = { "apps": {app_name: f'data/{app_name}_{c}' for app_name in the_system.apps.keys()} } - # if c == 'start': - # cfg['order'] = the_system.app_start_order - # elif c == 'stop': - # cfg['order'] = the_system.app_start_order[::-1] - system_command_datas[c]=cfg if verbose: @@ -794,7 +809,7 @@ def make_system_command_datas(daqconf:dict, the_system, forced_deps=[], verbose: console.log(f"Generating boot json file") system_command_datas['boot'] = generate_boot( - conf = daqconf, + boot_conf = boot_conf, system = the_system, verbose = verbose, control_to_data_network=control_to_data_network diff --git a/schema/daqconf/confgen.jsonnet b/schema/daqconf/confgen.jsonnet index eecf2d93..154b12d9 100755 --- a/schema/daqconf/confgen.jsonnet +++ b/schema/daqconf/confgen.jsonnet @@ -32,12 +32,14 @@ local cs = { tc_intervals: s.sequence( "TCIntervals", self.tc_interval, doc="List of TC intervals used by CTCM"), readout_time: s.number( "ROTime", "i8", doc="A readout time in ticks"), channel_list: s.sequence( "ChannelList", self.count, doc="List of offline channels to be masked out from the TPHandler"), + pm_kind: s.enum( "PMKind", ["k8s", "ssh"]), boot: s.record("boot", [ s.field( "op_env", self.string, default='swtest', doc="Operational environment - used for raw data filename prefix and HDF5 Attribute inside the files"), s.field( "base_command_port", self.port, default=3333, doc="Base port of application command endpoints"), + # Obscure s.field( "RTE_script_settings", self.three_choice, default=0, doc="0 - Use an RTE script iff not in a dev environment, 1 - Always use RTE, 2 - never use RTE"), s.field( "capture_env_vars", self.strings, default=['TIMING_SHARE', 'DETCHANNELMAPS_SHARE'], doc="List of variables to capture from the environment"), @@ -45,9 +47,9 @@ local cs = { s.field( "opmon_impl", self.monitoring_dest, default='local', doc="Info collector service implementation to use"), s.field( "ers_impl", self.monitoring_dest, default='local', doc="ERS destination (Kafka used for cern and pocket)"), s.field( "pocket_url", self.host, default='127.0.0.1', doc="URL for connecting to Pocket services"), + s.field( "process_manager", self.pm_kind, default="ssh", doc="Choice of process manager"), # K8S - s.field( "use_k8s", self.flag, default=false, doc="Whether to use k8s"), s.field( "image", self.string, default="dunedaq/c8-minimal", doc="Which docker image to use"), # Connectivity Service diff --git a/scripts/daqconf_multiru_gen b/scripts/daqconf_multiru_gen index bd233180..2ee2009b 100755 --- a/scripts/daqconf_multiru_gen +++ b/scripts/daqconf_multiru_gen @@ -122,8 +122,10 @@ def cli( # Update with command-line options if force_pm is not None: - boot.use_k8s = (force_pm == 'k8s') - console.log(f"boot.use_k8s set to {boot.use_k8s}") + boot.process_manager = force_pm + console.log(f"boot.boot.process_manager set to {boot.process_manager}") + + use_k8s = (boot.process_manager == 'k8s') if base_command_port is not None: boot.base_command_port = base_command_port @@ -143,9 +145,6 @@ def cli( if dqm.impl == 'pocket': dqm.kafka_address = boot.pocket_url + ":30092" - # if op_env != '': - # boot.op_env = op_env - file_label = file_label if file_label is not None else boot.op_env @@ -208,7 +207,7 @@ def cli( for entry in readout.data_files: data_file_map[entry["detector_id"]] = resolve_asset_file(entry["data_file"], debug) - if boot.use_k8s: + if use_k8s: console.log(f'Using k8s') trigger.tpset_output_path = abspath(trigger.tpset_output_path) for df_app in appconfig_df.values(): @@ -287,7 +286,7 @@ def cli( if hsi.control_hsi_hw and not hsi.use_timing_hsi: raise Exception("Timing HSI hardware control can only be enabled if timing HSI hardware is used!") - if boot.use_k8s and not boot.image: + if use_k8s and not boot.image: raise Exception("You need to provide an --image if running with k8s") # host_id_dict = {} @@ -504,7 +503,7 @@ def cli( EMULATED_DATA_TIMES_START_WITH_NOW = readout.emulated_data_times_start_with_now, DEBUG=debug) - if boot.use_k8s: + if use_k8s: if ru_desc.kind == 'flx': c = card_override if card_override != -1 else ru_desc.iface the_system.apps[ru_name].resources = { @@ -597,7 +596,7 @@ def cli( SRC_GEO_ID_MAP=dro_map.get_src_geo_map(), DEBUG=debug ) - if boot.use_k8s: + if use_k8s: the_system.apps[app_name].mounted_dirs += [{ 'name': f'raw-data-{i}', 'physical_location': opath, @@ -652,7 +651,7 @@ def cli( SOURCE_IDX=dfidx, HOST=trigger.host_tpw, DEBUG=debug) - if boot.use_k8s: ## TODO schema + if use_k8s: ## TODO schema the_system.apps[tpw_name].mounted_dirs += [{ 'name': 'raw-data', 'physical_location':trigger.tpset_output_path, @@ -732,14 +731,14 @@ def cli( # Arrange per-app command data into the format used by util.write_json_files() app_command_datas = { - name : make_app_command_data(the_system, app,name, verbose=debug, use_k8s=boot.use_k8s, use_connectivity_service=boot.use_connectivity_service, connectivity_service_interval=boot.connectivity_service_interval) + name : make_app_command_data(the_system, app,name, verbose=debug, use_k8s=use_k8s, use_connectivity_service=boot.use_connectivity_service, connectivity_service_interval=boot.connectivity_service_interval) for name,app in the_system.apps.items() } ################################################################################## # Make boot.json config - from daqconf.core.conf_utils import make_system_command_datas,generate_boot, write_json_files + from daqconf.core.conf_utils import make_system_command_datas, write_json_files # HACK: Make sure RUs start after trigger forced_deps = [] @@ -827,7 +826,7 @@ def cli( write_metadata_file(output_dir, "daqconf_multiru_gen", config_file.as_posix()) write_config_file( output_dir, - basename(config_file) if config_file else "default.json", + config_file.name if config_file else "default.json", confgen.daqconf_multiru_gen( # boot = boot, dataflow = dataflow, From 07a79d5b79931d7d630cab670f5375e1dd855c82 Mon Sep 17 00:00:00 2001 From: Alessandro Thea Date: Wed, 7 Jun 2023 17:20:55 +0200 Subject: [PATCH 13/90] Starting another round of refactoring --- python/daqconf/apps/readout_gen.py | 613 ++++++++++++++++++++--------- schema/daqconf/confgen.jsonnet | 2 +- scripts/daqconf_multiru_gen | 140 ++++--- 3 files changed, 493 insertions(+), 262 deletions(-) diff --git a/python/daqconf/apps/readout_gen.py b/python/daqconf/apps/readout_gen.py index f6f55389..96b48592 100644 --- a/python/daqconf/apps/readout_gen.py +++ b/python/daqconf/apps/readout_gen.py @@ -39,6 +39,7 @@ # from appfwk.utils import acmd, mcmd, mrccmd, mspec from os import path +from pathlib import Path from ..core.conf_utils import Direction, Queue from ..core.sourceid import SourceIDBroker @@ -121,52 +122,52 @@ def compute_data_types( ### # Fake Card Reader creator ### -def create_fake_cardreader( - FRONTEND_TYPE: str, - QUEUE_FRAGMENT_TYPE: str, - DATA_RATE_SLOWDOWN_FACTOR: int, - DATA_FILES: dict, - DEFAULT_DATA_FILE: str, - CLOCK_SPEED_HZ: int, - EMULATED_DATA_TIMES_START_WITH_NOW: bool, - RU_DESCRIPTOR # ReadoutUnitDescriptor - -) -> tuple[list, list]: - """ - Create a FAKE Card reader module - """ - - conf = sec.Conf( - link_confs = [ - sec.LinkConfiguration( - source_id=s.src_id, - crate_id = s.geo_id.crate_id, - slot_id = s.geo_id.slot_id, - link_id = s.geo_id.stream_id, - slowdown=DATA_RATE_SLOWDOWN_FACTOR, - queue_name=f"output_{s.src_id}", - data_filename = DATA_FILES[s.geo_id.det_id] if s.geo_id.det_id in DATA_FILES.keys() else DEFAULT_DATA_FILE, - emu_frame_error_rate=0 - ) for s in RU_DESCRIPTOR.streams], - use_now_as_first_data_time=EMULATED_DATA_TIMES_START_WITH_NOW, - clock_speed_hz=CLOCK_SPEED_HZ, - queue_timeout_ms = QUEUE_POP_WAIT_MS - ) - - - modules = [DAQModule(name = "fake_source", - plugin = "FakeCardReader", - conf = conf)] - queues = [ - Queue( - f"fake_source.output_{s.src_id}", - f"datahandler_{s.src_id}.raw_input", - QUEUE_FRAGMENT_TYPE, - f'{FRONTEND_TYPE}_link_{s.src_id}', 100000 - ) for s in RU_DESCRIPTOR.streams - ] +# def create_fake_cardreader( +# FRONTEND_TYPE: str, +# QUEUE_FRAGMENT_TYPE: str, +# DATA_RATE_SLOWDOWN_FACTOR: int, +# DATA_FILES: dict, +# DEFAULT_DATA_FILE: str, +# CLOCK_SPEED_HZ: int, +# EMULATED_DATA_TIMES_START_WITH_NOW: bool, +# RU_DESCRIPTOR # ReadoutUnitDescriptor + +# ) -> tuple[list, list]: +# """ +# Create a FAKE Card reader module +# """ + +# conf = sec.Conf( +# link_confs = [ +# sec.LinkConfiguration( +# source_id=s.src_id, +# crate_id = s.geo_id.crate_id, +# slot_id = s.geo_id.slot_id, +# link_id = s.geo_id.stream_id, +# slowdown=DATA_RATE_SLOWDOWN_FACTOR, +# queue_name=f"output_{s.src_id}", +# data_filename = DATA_FILES[s.geo_id.det_id] if s.geo_id.det_id in DATA_FILES.keys() else DEFAULT_DATA_FILE, +# emu_frame_error_rate=0 +# ) for s in RU_DESCRIPTOR.streams], +# use_now_as_first_data_time=EMULATED_DATA_TIMES_START_WITH_NOW, +# clock_speed_hz=CLOCK_SPEED_HZ, +# queue_timeout_ms = QUEUE_POP_WAIT_MS +# ) + + +# modules = [DAQModule(name = "fake_source", +# plugin = "FakeCardReader", +# conf = conf)] +# queues = [ +# Queue( +# f"fake_source.output_{s.src_id}", +# f"datahandler_{s.src_id}.raw_input", +# QUEUE_FRAGMENT_TYPE, +# f'{FRONTEND_TYPE}_link_{s.src_id}', 100000 +# ) for s in RU_DESCRIPTOR.streams +# ] - return modules, queues +# return modules, queues ### @@ -685,15 +686,6 @@ def add_dro_eps_and_fps( toposort=False ) - # if processing is enabled, add a pubsub endooint for TPSets - #if dlh.conf.rawdataprocessorconf['enable_tpg']: - # mgraph.add_endpoint( - # f"tpsets_ru{RUIDX}_link{dro_sid}", - # f"datahandler_{dro_sid}.tpset_out", - # "TPSet", - # Direction.OUT, - # is_pubsub=True - # ) ### @@ -741,171 +733,400 @@ def add_tpg_eps_and_fps( QUEUE_POP_WAIT_MS = 10 # This affects stop time, as each link will wait this long before stop -### -# Create Readout Application -### -def create_readout_app( - RU_DESCRIPTOR, - SOURCEID_BROKER : SourceIDBroker = None, - EMULATOR_MODE=False, - DATA_RATE_SLOWDOWN_FACTOR=1, - DEFAULT_DATA_FILE="./frames.bin", - DATA_FILES={}, - USE_FAKE_CARDS=True, - CLOCK_SPEED_HZ=62500000, - RAW_RECORDING_ENABLED=False, - RAW_RECORDING_OUTPUT_DIR=".", - CHANNEL_MASK_TPG: list = [], - THRESHOLD_TPG=120, - ALGORITHM_TPG="SWTPG", - TPG_ENABLED=False, - TPG_CHANNEL_MAP= "ProtoDUNESP1ChannelMap", - DATA_REQUEST_TIMEOUT=1000, - FRAGMENT_SEND_TIMEOUT=10, - EAL_ARGS='-l 0-1 -n 3 -- -m [0:1].0 -j', - NUMA_ID=0, - LATENCY_BUFFER_SIZE=499968, - LATENCY_BUFFER_NUMA_AWARE = False, - LATENCY_BUFFER_ALLOCATION_MODE = False, - - CARD_ID_OVERRIDE = -1, - EMULATED_DATA_TIMES_START_WITH_NOW = False, - DEBUG=False -) -> App: - - FRONTEND_TYPE, QUEUE_FRAGMENT_TYPE, _, _, _ = compute_data_types(RU_DESCRIPTOR.det_id, CLOCK_SPEED_HZ, RU_DESCRIPTOR.kind) - - # TPG is automatically disabled for non wib2 frontends - TPG_ENABLED = TPG_ENABLED and (FRONTEND_TYPE=='wib2' or FRONTEND_TYPE=='wibeth') - - modules = [] - queues = [] +class ReadoutAppGenerator: + """Utility class to generate readout applications""" + def __init__(self, readout_cfg): - # Create the card readers - cr_mods = [] - cr_queues = [] + self.config = readout_cfg + excpt = {} + for ex in self.config.numa_config['exceptions']: + excpt[(ex['host'], ex['card'])] = ex - # Create the card readers - if USE_FAKE_CARDS: - fakecr_mods, fakecr_queues = create_fake_cardreader( - FRONTEND_TYPE=FRONTEND_TYPE, - QUEUE_FRAGMENT_TYPE=QUEUE_FRAGMENT_TYPE, - DATA_RATE_SLOWDOWN_FACTOR=DATA_RATE_SLOWDOWN_FACTOR, - DATA_FILES=DATA_FILES, - DEFAULT_DATA_FILE=DEFAULT_DATA_FILE, - CLOCK_SPEED_HZ=CLOCK_SPEED_HZ, - EMULATED_DATA_TIMES_START_WITH_NOW=EMULATED_DATA_TIMES_START_WITH_NOW, - RU_DESCRIPTOR=RU_DESCRIPTOR - ) - cr_mods += fakecr_mods - cr_queues += fakecr_queues - else: - if RU_DESCRIPTOR.kind == 'flx': - flx_mods, flx_queues = create_felix_cardreader( - FRONTEND_TYPE=FRONTEND_TYPE, - QUEUE_FRAGMENT_TYPE=QUEUE_FRAGMENT_TYPE, - CARD_ID_OVERRIDE=CARD_ID_OVERRIDE, - NUMA_ID=NUMA_ID, - RU_DESCRIPTOR=RU_DESCRIPTOR - ) - cr_mods += flx_mods - cr_queues += flx_queues - elif RU_DESCRIPTOR.kind == 'eth' and RU_DESCRIPTOR.streams[0].parameters.protocol == "udp": - dpdk_mods, dpdk_queues = create_dpdk_cardreader( - FRONTEND_TYPE=FRONTEND_TYPE, - QUEUE_FRAGMENT_TYPE=QUEUE_FRAGMENT_TYPE, - EAL_ARGS=EAL_ARGS, - RU_DESCRIPTOR=RU_DESCRIPTOR - ) - cr_mods += dpdk_mods - cr_queues += dpdk_queues + def get_numa_cfg(self, RU_DESCRIPTOR): + + cfg = self.config + try: + ex = self.excpt[(RU_DESCRIPTOR.host_name, RU_DESCRIPTOR.iface)] + numa_id = ex['numa_id'] + latency_numa = ex['latency_buffer_numa_aware'] + latency_preallocate = ex['latency_buffer_preallocation'] + flx_card_override = ex['felix_card_id'] + except KeyError: + numa_id = cfg.numa_config['default_id'] + latency_numa = cfg.numa_config['default_latency_numa_aware'] + latency_preallocate = cfg.numa_config['default_latency_preallocation'] + flx_card_override = -1 + return (numa_id, latency_numa, latency_preallocate, flx_card_override) + + + def generate( + self, + RU_DESCRIPTOR, + SOURCEID_BROKER, + data_file_map, + tpg_channel_map, + data_timeout_requests, + ): + """Generate the readout applicaton + + Args: + RU_DESCRIPTOR (_type_): _description_ + SOURCEID_BROKER (SourceIDBroker): _description_ + data_file_map (_type_): _description_ + tpg_channel_map (_type_): _description_ + data_timeout_requests (_type_): _description_ + + Raises: + RuntimeError: _description_ + + Returns: + _type_: _description_ + """ - elif RU_DESCRIPTOR.kind == 'eth' and RU_DESCRIPTOR.streams[0].parameters.protocol == "zmq": - pac_mods, pac_queues = create_pacman_cardreader( + + numa_id, latency_numa, latency_preallocate, card_override = self.get_numa_cfg(RU_DESCRIPTOR) + cfg = self.config + TPG_ENABLED = cfg.enable_tpg, + DATA_FILES = data_file_map, + # TPG_CHANNEL_MAP = tpg_channel_map, + DATA_REQUEST_TIMEOUT=data_timeout_requests, + + FRONTEND_TYPE, QUEUE_FRAGMENT_TYPE, _, _, _ = compute_data_types(RU_DESCRIPTOR.det_id, cfg.clock_speed_hz, RU_DESCRIPTOR.kind) + + # TPG is automatically disabled for non wib2 frontends + TPG_ENABLED = TPG_ENABLED and (FRONTEND_TYPE=='wib2' or FRONTEND_TYPE=='wibeth') + + modules = [] + queues = [] + + + # Create the card readers + cr_mods = [] + cr_queues = [] + + + # Create the card readers + if cfg.use_fake_cards: + fakecr_mods, fakecr_queues = create_fake_cardreader( FRONTEND_TYPE=FRONTEND_TYPE, QUEUE_FRAGMENT_TYPE=QUEUE_FRAGMENT_TYPE, + DATA_RATE_SLOWDOWN_FACTOR=cfg.data_rate_slowdown_factor, + DATA_FILES=DATA_FILES, + DEFAULT_DATA_FILE=cfg.default_data_file, + CLOCK_SPEED_HZ=cfg.clock_speed_hz, + EMULATED_DATA_TIMES_START_WITH_NOW=cfg.emulated_data_times_start_with_now, RU_DESCRIPTOR=RU_DESCRIPTOR ) - cr_mods += pac_mods - cr_queues += pac_queues - - modules += cr_mods - queues += cr_queues - - # Create the data-link handlers - dlhs_mods, _ = create_det_dhl( - LATENCY_BUFFER_SIZE=LATENCY_BUFFER_SIZE, - LATENCY_BUFFER_NUMA_AWARE=LATENCY_BUFFER_NUMA_AWARE, - LATENCY_BUFFER_ALLOCATION_MODE=LATENCY_BUFFER_ALLOCATION_MODE, - NUMA_ID=NUMA_ID, - SEND_PARTIAL_FRAGMENTS=False, - RAW_RECORDING_OUTPUT_DIR=RAW_RECORDING_OUTPUT_DIR, - DATA_REQUEST_TIMEOUT=DATA_REQUEST_TIMEOUT, - FRAGMENT_SEND_TIMEOUT=FRAGMENT_SEND_TIMEOUT, - RAW_RECORDING_ENABLED=RAW_RECORDING_ENABLED, - RU_DESCRIPTOR=RU_DESCRIPTOR, - EMULATOR_MODE=EMULATOR_MODE - - ) - - # Configure the TP processing if requrested - if TPG_ENABLED: - dlhs_mods = add_tp_processing( - dlh_list=dlhs_mods, - THRESHOLD_TPG=THRESHOLD_TPG, - ALGORITHM_TPG=ALGORITHM_TPG, - CHANNEL_MASK_TPG=CHANNEL_MASK_TPG, - TPG_CHANNEL_MAP=TPG_CHANNEL_MAP, - EMULATOR_MODE=EMULATOR_MODE, - CLOCK_SPEED_HZ=CLOCK_SPEED_HZ, - DATA_RATE_SLOWDOWN_FACTOR=DATA_RATE_SLOWDOWN_FACTOR - ) - - modules += dlhs_mods + cr_mods += fakecr_mods + cr_queues += fakecr_queues + else: + if RU_DESCRIPTOR.kind == 'flx': + flx_mods, flx_queues = create_felix_cardreader( + FRONTEND_TYPE=FRONTEND_TYPE, + QUEUE_FRAGMENT_TYPE=QUEUE_FRAGMENT_TYPE, + CARD_ID_OVERRIDE=card_override, + NUMA_ID=numa_id, + RU_DESCRIPTOR=RU_DESCRIPTOR + ) + cr_mods += flx_mods + cr_queues += flx_queues + + elif RU_DESCRIPTOR.kind == 'eth' and RU_DESCRIPTOR.streams[0].parameters.protocol == "udp": + dpdk_mods, dpdk_queues = create_dpdk_cardreader( + FRONTEND_TYPE=FRONTEND_TYPE, + QUEUE_FRAGMENT_TYPE=QUEUE_FRAGMENT_TYPE, + EAL_ARGS=cfg.eal_args, + RU_DESCRIPTOR=RU_DESCRIPTOR + ) + cr_mods += dpdk_mods + cr_queues += dpdk_queues - # Add the TP datalink handlers - if TPG_ENABLED: - tps = { k:v for k,v in SOURCEID_BROKER.get_all_source_ids("Trigger").items() if isinstance(v, ReadoutUnitDescriptor ) and v==RU_DESCRIPTOR} - if len(tps) != 1: - raise RuntimeError(f"Could not retrieve unique element from source id map {tps}") + elif RU_DESCRIPTOR.kind == 'eth' and RU_DESCRIPTOR.streams[0].parameters.protocol == "zmq": - tpg_mods, tpg_queues = create_tp_dlhs( - dlh_list=dlhs_mods, + pac_mods, pac_queues = create_pacman_cardreader( + FRONTEND_TYPE=FRONTEND_TYPE, + QUEUE_FRAGMENT_TYPE=QUEUE_FRAGMENT_TYPE, + RU_DESCRIPTOR=RU_DESCRIPTOR + ) + cr_mods += pac_mods + cr_queues += pac_queues + + modules += cr_mods + queues += cr_queues + + # Create the data-link handlers + dlhs_mods, _ = create_det_dhl( + LATENCY_BUFFER_SIZE=cfg.latency_buffer_size, + LATENCY_BUFFER_NUMA_AWARE=latency_numa, + LATENCY_BUFFER_ALLOCATION_MODE=latency_preallocate, + NUMA_ID=numa_id, + SEND_PARTIAL_FRAGMENTS=False, + RAW_RECORDING_OUTPUT_DIR=cfg.raw_recording_output_dir, DATA_REQUEST_TIMEOUT=DATA_REQUEST_TIMEOUT, - FRAGMENT_SEND_TIMEOUT=FRAGMENT_SEND_TIMEOUT, - tpset_sid = next(iter(tps)) + FRAGMENT_SEND_TIMEOUT=cfg.fragment_send_timeout_ms, + RAW_RECORDING_ENABLED=cfg.enable_raw_recording, + RU_DESCRIPTOR=RU_DESCRIPTOR, + EMULATOR_MODE=cfg.emulator_mode + ) - modules += tpg_mods - queues += tpg_queues - # Create the Module graphs - mgraph = ModuleGraph(modules, queues=queues) + # Configure the TP processing if requrested + if TPG_ENABLED: + dlhs_mods = add_tp_processing( + dlh_list=dlhs_mods, + THRESHOLD_TPG=cfg.tpg_threshold, + ALGORITHM_TPG=cfg.tpg_algorithm, + CHANNEL_MASK_TPG=cfg.tpg_channel_mask, + TPG_CHANNEL_MAP=tpg_channel_map, + EMULATOR_MODE=cfg.emulator_mode, + CLOCK_SPEED_HZ=cfg.clock_speed_hz, + DATA_RATE_SLOWDOWN_FACTOR=cfg.data_rate_slowdown_factor + ) + + modules += dlhs_mods + + # Add the TP datalink handlers + if TPG_ENABLED: + tps = { k:v for k,v in SOURCEID_BROKER.get_all_source_ids("Trigger").items() if isinstance(v, ReadoutUnitDescriptor ) and v==RU_DESCRIPTOR} + if len(tps) != 1: + raise RuntimeError(f"Could not retrieve unique element from source id map {tps}") - # Add endpoints and frame producers to DRO data handlers - add_dro_eps_and_fps( - mgraph=mgraph, - dlh_list=dlhs_mods, - RUIDX=RU_DESCRIPTOR.label - ) + tpg_mods, tpg_queues = create_tp_dlhs( + dlh_list=dlhs_mods, + DATA_REQUEST_TIMEOUT=DATA_REQUEST_TIMEOUT, + FRAGMENT_SEND_TIMEOUT=cfg.fragment_send_timeout_ms, + tpset_sid = next(iter(tps)) + ) + modules += tpg_mods + queues += tpg_queues + + # Create the Module graphs + mgraph = ModuleGraph(modules, queues=queues) - if TPG_ENABLED: - # Add endpoints and frame producers to TP data handlers - add_tpg_eps_and_fps( + # Add endpoints and frame producers to DRO data handlers + add_dro_eps_and_fps( mgraph=mgraph, - # dlh_list=dlhs_mods, - tpg_dlh_list=tpg_mods, + dlh_list=dlhs_mods, RUIDX=RU_DESCRIPTOR.label ) - # Create the application - readout_app = App(mgraph, host=RU_DESCRIPTOR.host_name) + if TPG_ENABLED: + # Add endpoints and frame producers to TP data handlers + add_tpg_eps_and_fps( + mgraph=mgraph, + # dlh_list=dlhs_mods, + tpg_dlh_list=tpg_mods, + RUIDX=RU_DESCRIPTOR.label + ) - # All done - return readout_app + # Create the application + readout_app = App(mgraph, host=RU_DESCRIPTOR.host_name) + + + + if RU_DESCRIPTOR.kind == 'flx': + c = card_override if card_override != -1 else RU_DESCRIPTOR.iface + readout_app.resources = { + f"felix.cern/flx{c}-data": "1", # requesting FLX{c} + "memory": "64Gi" # yes bro + } + + dir_names = set() + + cvmfs = Path('/cvmfs') + ddf_path = Path(cfg.default_data_file) + if not cvmfs in ddf_path.parents: + dir_name.add(ddf_path.parent) + + for _,file in data_file_map: + f = Path(file) + if not cvmfs in f.parents: + dir_names.add(f.parent) + + for dir_idx, dir_name in enumerate(dir_names): + readout_app.mounted_dirs += [{ + 'name': f'data-file-{dir_idx}', + 'physical_location': dir_name, + 'in_pod_location': dir_name, + 'read_only': True, + }] + + # All done + return readout_app + + + + + +# ### +# # Create Readout Application +# ### +# def create_readout_app( +# RU_DESCRIPTOR, +# SOURCEID_BROKER : SourceIDBroker = None, +# EMULATOR_MODE=False, +# DATA_RATE_SLOWDOWN_FACTOR=1, +# DEFAULT_DATA_FILE="./frames.bin", +# DATA_FILES={}, +# USE_FAKE_CARDS=True, +# CLOCK_SPEED_HZ=62500000, +# RAW_RECORDING_ENABLED=False, +# RAW_RECORDING_OUTPUT_DIR=".", +# CHANNEL_MASK_TPG: list = [], +# THRESHOLD_TPG=120, +# ALGORITHM_TPG="SWTPG", +# TPG_ENABLED=False, +# TPG_CHANNEL_MAP= "ProtoDUNESP1ChannelMap", +# DATA_REQUEST_TIMEOUT=1000, +# FRAGMENT_SEND_TIMEOUT=10, +# EAL_ARGS='-l 0-1 -n 3 -- -m [0:1].0 -j', +# NUMA_ID=0, +# LATENCY_BUFFER_SIZE=499968, +# LATENCY_BUFFER_NUMA_AWARE = False, +# LATENCY_BUFFER_ALLOCATION_MODE = False, + +# CARD_ID_OVERRIDE = -1, +# EMULATED_DATA_TIMES_START_WITH_NOW = False, +# DEBUG=False +# ) -> App: + +# FRONTEND_TYPE, QUEUE_FRAGMENT_TYPE, _, _, _ = compute_data_types(RU_DESCRIPTOR.det_id, CLOCK_SPEED_HZ, RU_DESCRIPTOR.kind) + +# # TPG is automatically disabled for non wib2 frontends +# TPG_ENABLED = TPG_ENABLED and (FRONTEND_TYPE=='wib2' or FRONTEND_TYPE=='wibeth') + +# modules = [] +# queues = [] + + +# # Create the card readers +# cr_mods = [] +# cr_queues = [] + + +# # Create the card readers +# if USE_FAKE_CARDS: +# fakecr_mods, fakecr_queues = create_fake_cardreader( +# FRONTEND_TYPE=FRONTEND_TYPE, +# QUEUE_FRAGMENT_TYPE=QUEUE_FRAGMENT_TYPE, +# DATA_RATE_SLOWDOWN_FACTOR=DATA_RATE_SLOWDOWN_FACTOR, +# DATA_FILES=DATA_FILES, +# DEFAULT_DATA_FILE=DEFAULT_DATA_FILE, +# CLOCK_SPEED_HZ=CLOCK_SPEED_HZ, +# EMULATED_DATA_TIMES_START_WITH_NOW=EMULATED_DATA_TIMES_START_WITH_NOW, +# RU_DESCRIPTOR=RU_DESCRIPTOR +# ) +# cr_mods += fakecr_mods +# cr_queues += fakecr_queues +# else: +# if RU_DESCRIPTOR.kind == 'flx': +# flx_mods, flx_queues = create_felix_cardreader( +# FRONTEND_TYPE=FRONTEND_TYPE, +# QUEUE_FRAGMENT_TYPE=QUEUE_FRAGMENT_TYPE, +# CARD_ID_OVERRIDE=CARD_ID_OVERRIDE, +# NUMA_ID=NUMA_ID, +# RU_DESCRIPTOR=RU_DESCRIPTOR +# ) +# cr_mods += flx_mods +# cr_queues += flx_queues + +# elif RU_DESCRIPTOR.kind == 'eth' and RU_DESCRIPTOR.streams[0].parameters.protocol == "udp": +# dpdk_mods, dpdk_queues = create_dpdk_cardreader( +# FRONTEND_TYPE=FRONTEND_TYPE, +# QUEUE_FRAGMENT_TYPE=QUEUE_FRAGMENT_TYPE, +# EAL_ARGS=EAL_ARGS, +# RU_DESCRIPTOR=RU_DESCRIPTOR +# ) +# cr_mods += dpdk_mods +# cr_queues += dpdk_queues + +# elif RU_DESCRIPTOR.kind == 'eth' and RU_DESCRIPTOR.streams[0].parameters.protocol == "zmq": + +# pac_mods, pac_queues = create_pacman_cardreader( +# FRONTEND_TYPE=FRONTEND_TYPE, +# QUEUE_FRAGMENT_TYPE=QUEUE_FRAGMENT_TYPE, +# RU_DESCRIPTOR=RU_DESCRIPTOR +# ) +# cr_mods += pac_mods +# cr_queues += pac_queues + +# modules += cr_mods +# queues += cr_queues + +# # Create the data-link handlers +# dlhs_mods, _ = create_det_dhl( +# LATENCY_BUFFER_SIZE=LATENCY_BUFFER_SIZE, +# LATENCY_BUFFER_NUMA_AWARE=LATENCY_BUFFER_NUMA_AWARE, +# LATENCY_BUFFER_ALLOCATION_MODE=LATENCY_BUFFER_ALLOCATION_MODE, +# NUMA_ID=NUMA_ID, +# SEND_PARTIAL_FRAGMENTS=False, +# RAW_RECORDING_OUTPUT_DIR=RAW_RECORDING_OUTPUT_DIR, +# DATA_REQUEST_TIMEOUT=DATA_REQUEST_TIMEOUT, +# FRAGMENT_SEND_TIMEOUT=FRAGMENT_SEND_TIMEOUT, +# RAW_RECORDING_ENABLED=RAW_RECORDING_ENABLED, +# RU_DESCRIPTOR=RU_DESCRIPTOR, +# EMULATOR_MODE=EMULATOR_MODE + +# ) + +# # Configure the TP processing if requrested +# if TPG_ENABLED: +# dlhs_mods = add_tp_processing( +# dlh_list=dlhs_mods, +# THRESHOLD_TPG=THRESHOLD_TPG, +# ALGORITHM_TPG=ALGORITHM_TPG, +# CHANNEL_MASK_TPG=CHANNEL_MASK_TPG, +# TPG_CHANNEL_MAP=TPG_CHANNEL_MAP, +# EMULATOR_MODE=EMULATOR_MODE, +# CLOCK_SPEED_HZ=CLOCK_SPEED_HZ, +# DATA_RATE_SLOWDOWN_FACTOR=DATA_RATE_SLOWDOWN_FACTOR +# ) + +# modules += dlhs_mods + +# # Add the TP datalink handlers +# if TPG_ENABLED: +# tps = { k:v for k,v in SOURCEID_BROKER.get_all_source_ids("Trigger").items() if isinstance(v, ReadoutUnitDescriptor ) and v==RU_DESCRIPTOR} +# if len(tps) != 1: +# raise RuntimeError(f"Could not retrieve unique element from source id map {tps}") + +# tpg_mods, tpg_queues = create_tp_dlhs( +# dlh_list=dlhs_mods, +# DATA_REQUEST_TIMEOUT=DATA_REQUEST_TIMEOUT, +# FRAGMENT_SEND_TIMEOUT=FRAGMENT_SEND_TIMEOUT, +# tpset_sid = next(iter(tps)) +# ) +# modules += tpg_mods +# queues += tpg_queues + +# # Create the Module graphs +# mgraph = ModuleGraph(modules, queues=queues) + +# # Add endpoints and frame producers to DRO data handlers +# add_dro_eps_and_fps( +# mgraph=mgraph, +# dlh_list=dlhs_mods, +# RUIDX=RU_DESCRIPTOR.label +# ) + +# if TPG_ENABLED: +# # Add endpoints and frame producers to TP data handlers +# add_tpg_eps_and_fps( +# mgraph=mgraph, +# # dlh_list=dlhs_mods, +# tpg_dlh_list=tpg_mods, +# RUIDX=RU_DESCRIPTOR.label +# ) + +# # Create the application +# readout_app = App(mgraph, host=RU_DESCRIPTOR.host_name) + +# # All done +# return readout_app diff --git a/schema/daqconf/confgen.jsonnet b/schema/daqconf/confgen.jsonnet index 154b12d9..4bfeae53 100755 --- a/schema/daqconf/confgen.jsonnet +++ b/schema/daqconf/confgen.jsonnet @@ -140,6 +140,7 @@ local cs = { readout: s.record("readout", [ s.field( "detector_readout_map_file", self.path, default='./DetectorReadoutMap.json', doc="File containing detector hardware map for configuration to run"), + s.field( "use_fake_data_producers", self.flag, default=false, doc="Use fake data producers that respond with empty fragments immediately instead of (fake) cards and DLHs"), s.field( "emulator_mode", self.flag, default=false, doc="If active, timestamps of data frames are overwritten when processed by the readout. This is necessary if the felix card does not set correct timestamps. Former -e"), s.field( "thread_pinning_file", self.path, default="", doc="A thread pinning configuration file that gets executed after conf."), s.field( "data_rate_slowdown_factor",self.count, default=1, doc="Factor by which to suppress data generation. Former -s"), @@ -155,7 +156,6 @@ local cs = { s.field( "tpg_channel_mask", self.channel_list, default=[], doc="List of offline channels to be masked out from the TPHandler"), s.field( "enable_raw_recording", self.flag, default=false, doc="Add queues and modules necessary for the record command"), s.field( "raw_recording_output_dir", self.path, default='.', doc="Output directory where recorded data is written to. Data for each link is written to a separate file"), - s.field( "use_fake_data_producers", self.flag, default=false, doc="Use fake data producers that respond with empty fragments immediately instead of (fake) cards and DLHs"), s.field( "host_dpdk_reader", self.hosts, default=['np04-srv-022'], doc="Which host to use to receive frames"), s.field( "eal_args", self.string, default='-l 0-1 -n 3 -- -m [0:1].0 -j', doc='Args passed to the EAL in DPDK'), s.field( "base_source_ip", self.string, default='10.73.139.', doc='First part of the IP of the source'), diff --git a/scripts/daqconf_multiru_gen b/scripts/daqconf_multiru_gen index 2ee2009b..b858f246 100755 --- a/scripts/daqconf_multiru_gen +++ b/scripts/daqconf_multiru_gen @@ -155,7 +155,7 @@ def cli( console.log("Loading dqm config generator") from daqconf.apps.dqm_gen import get_dqm_app console.log("Loading readout config generator") - from daqconf.apps.readout_gen import create_readout_app, create_fake_reaout_app + from daqconf.apps.readout_gen import create_readout_app, create_fake_reaout_app, ReadoutAppGenerator console.log("Loading trigger config generator") from daqconf.apps.trigger_gen import get_trigger_app console.log("Loading DFO config generator") @@ -462,73 +462,83 @@ def cli( trb_dqm_sourceid_offset = sourceid_broker.get_next_source_id("TRBuilder") ru_app_names=[] dqm_app_names = [] + + + roapp_gen = ReadoutAppGenerator(readout) for ru_i,(ru_name, ru_desc) in enumerate(ru_descs.items()): if readout.use_fake_data_producers == False: - numa_id = readout.numa_config['default_id'] - latency_numa = readout.numa_config['default_latency_numa_aware'] - latency_preallocate = readout.numa_config['default_latency_preallocation'] - card_override = -1 - for ex in readout.numa_config['exceptions']: - if ex['host'] == ru_desc.host_name and ex['card'] == ru_desc.iface: - numa_id = ex['numa_id'] - latency_numa = ex['latency_buffer_numa_aware'] - latency_preallocate = ex['latency_buffer_preallocation'] - card_override = ex['felix_card_id'] - - the_system.apps[ru_name] = create_readout_app( - RU_DESCRIPTOR = ru_desc, - SOURCEID_BROKER = sourceid_broker, - EMULATOR_MODE = readout.emulator_mode, - DATA_RATE_SLOWDOWN_FACTOR = readout.data_rate_slowdown_factor, - DEFAULT_DATA_FILE = readout.default_data_file, - DATA_FILES = data_file_map, - USE_FAKE_CARDS = readout.use_fake_cards, - CLOCK_SPEED_HZ = readout.clock_speed_hz, - RAW_RECORDING_ENABLED = readout.enable_raw_recording, - RAW_RECORDING_OUTPUT_DIR = readout.raw_recording_output_dir, - TPG_ENABLED = readout.enable_tpg, - THRESHOLD_TPG = readout.tpg_threshold, - ALGORITHM_TPG = readout.tpg_algorithm, - CHANNEL_MASK_TPG = readout.tpg_channel_mask, - TPG_CHANNEL_MAP = trigger.tpg_channel_map, - LATENCY_BUFFER_SIZE=readout.latency_buffer_size, - DATA_REQUEST_TIMEOUT=readout_data_request_timeout, - FRAGMENT_SEND_TIMEOUT=readout.fragment_send_timeout_ms, - EAL_ARGS=readout.eal_args, - NUMA_ID = numa_id, - LATENCY_BUFFER_NUMA_AWARE = latency_numa, - LATENCY_BUFFER_ALLOCATION_MODE = latency_preallocate, - CARD_ID_OVERRIDE = card_override, - EMULATED_DATA_TIMES_START_WITH_NOW = readout.emulated_data_times_start_with_now, - DEBUG=debug) - - if use_k8s: - if ru_desc.kind == 'flx': - c = card_override if card_override != -1 else ru_desc.iface - the_system.apps[ru_name].resources = { - f"felix.cern/flx{c}-data": "1", # requesting FLX{c} - "memory": "32Gi" # yes bro - } - - dir_names = set() - - if os.path.commonprefix(['/cvmfs', readout.default_data_file]) != '/cvmfs': - dir_names.add(dirname(readout.default_data_file)) - - for id,file in data_file_map: - if os.path.commonprefix(['/cvmfs', file]) != '/cvmfs': - dir_names.add(dirname(file)) - - dirindex = 0 - for dir_name in dir_names: - the_system.apps[ru_name].mounted_dirs += [{ - 'name': f'frames-bin-{dirindex}', - 'physical_location': dir_name, - 'in_pod_location': dir_name, - 'read_only': True, - }] - dirindex += 1 + the_system.apps[ru_name] = roapp_gen.generate( + RU_DESCRIPTOR=ru_desc, + SOURCEID_BROKER=sourceid_broker, + data_file_map=data_file_map, + tpg_channel_map=trigger.tpg_channel_map, + data_timeout_requests=readout_data_request_timeout + ) + # numa_id = readout.numa_config['default_id'] + # latency_numa = readout.numa_config['default_latency_numa_aware'] + # latency_preallocate = readout.numa_config['default_latency_preallocation'] + # card_override = -1 + # for ex in readout.numa_config['exceptions']: + # if ex['host'] == ru_desc.host_name and ex['card'] == ru_desc.iface: + # numa_id = ex['numa_id'] + # latency_numa = ex['latency_buffer_numa_aware'] + # latency_preallocate = ex['latency_buffer_preallocation'] + # card_override = ex['felix_card_id'] + + # the_system.apps[ru_name] = create_readout_app( + # RU_DESCRIPTOR = ru_desc, + # SOURCEID_BROKER = sourceid_broker, + # EMULATOR_MODE = readout.emulator_mode, + # DATA_RATE_SLOWDOWN_FACTOR = readout.data_rate_slowdown_factor, + # DEFAULT_DATA_FILE = readout.default_data_file, + # DATA_FILES = data_file_map, + # USE_FAKE_CARDS = readout.use_fake_cards, + # CLOCK_SPEED_HZ = readout.clock_speed_hz, + # RAW_RECORDING_ENABLED = readout.enable_raw_recording, + # RAW_RECORDING_OUTPUT_DIR = readout.raw_recording_output_dir, + # TPG_ENABLED = readout.enable_tpg, + # THRESHOLD_TPG = readout.tpg_threshold, + # ALGORITHM_TPG = readout.tpg_algorithm, + # CHANNEL_MASK_TPG = readout.tpg_channel_mask, + # TPG_CHANNEL_MAP = trigger.tpg_channel_map, + # LATENCY_BUFFER_SIZE=readout.latency_buffer_size, + # DATA_REQUEST_TIMEOUT=readout_data_request_timeout, + # FRAGMENT_SEND_TIMEOUT=readout.fragment_send_timeout_ms, + # EAL_ARGS=readout.eal_args, + # NUMA_ID = numa_id, + # LATENCY_BUFFER_NUMA_AWARE = latency_numa, + # LATENCY_BUFFER_ALLOCATION_MODE = latency_preallocate, + # CARD_ID_OVERRIDE = card_override, + # EMULATED_DATA_TIMES_START_WITH_NOW = readout.emulated_data_times_start_with_now, + # DEBUG=debug) + + # if use_k8s: + # if ru_desc.kind == 'flx': + # c = card_override if card_override != -1 else ru_desc.iface + # the_system.apps[ru_name].resources = { + # f"felix.cern/flx{c}-data": "1", # requesting FLX{c} + # "memory": "64Gi" # yes bro + # } + + # dir_names = set() + + # if os.path.commonprefix(['/cvmfs', readout.default_data_file]) != '/cvmfs': + # dir_names.add(dirname(readout.default_data_file)) + + # for id,file in data_file_map: + # if os.path.commonprefix(['/cvmfs', file]) != '/cvmfs': + # dir_names.add(dirname(file)) + + # dirindex = 0 + # for dir_name in dir_names: + # the_system.apps[ru_name].mounted_dirs += [{ + # 'name': f'frames-bin-{dirindex}', + # 'physical_location': dir_name, + # 'in_pod_location': dir_name, + # 'read_only': True, + # }] + # dirindex += 1 else: the_system.apps[ru_name] = create_fake_reaout_app( RU_DESCRIPTOR = ru_desc, From 00a982cbdfefd1633d8374f38ab62587e4f179cb Mon Sep 17 00:00:00 2001 From: Michal Rigan Date: Thu, 8 Jun 2023 05:37:55 -0500 Subject: [PATCH 14/90] adding bitwords flag, lists to daqcong (MLT) --- python/daqconf/apps/trigger_gen.py | 6 +++++- python/daqconf/core/fragment_producers.py | 10 +++++++--- schema/daqconf/confgen.jsonnet | 5 +++++ scripts/daqconf_multiru_gen | 5 ++++- 4 files changed, 21 insertions(+), 5 deletions(-) diff --git a/python/daqconf/apps/trigger_gen.py b/python/daqconf/apps/trigger_gen.py index 481dffd2..d836c1a6 100644 --- a/python/daqconf/apps/trigger_gen.py +++ b/python/daqconf/apps/trigger_gen.py @@ -102,6 +102,8 @@ def get_trigger_app(CLOCK_SPEED_HZ: int = 62_500_000, MLT_IGNORE_TC: list = [], MLT_USE_READOUT_MAP: bool = False, MLT_READOUT_MAP: dict = {}, + MLT_USE_BITWORDS: bool = False, + MLT_TRIGGER_BITWORDS: dict = {}, USE_CHANNEL_FILTER: bool = True, @@ -306,7 +308,9 @@ def get_trigger_app(CLOCK_SPEED_HZ: int = 62_500_000, ignore_tc=MLT_IGNORE_TC, td_readout_limit=max_td_length_ticks, use_readout_map=MLT_USE_READOUT_MAP, - td_readout_map=MLT_READOUT_MAP))] + td_readout_map=MLT_READOUT_MAP, + use_bitwords=MLT_USE_BITWORDS, + trigger_bitwords=MLT_TRIGGER_BITWORDS))] mgraph = ModuleGraph(modules) diff --git a/python/daqconf/core/fragment_producers.py b/python/daqconf/core/fragment_producers.py index 17b1abf0..a69536c2 100644 --- a/python/daqconf/core/fragment_producers.py +++ b/python/daqconf/core/fragment_producers.py @@ -40,12 +40,14 @@ def set_mlt_links(the_system, mlt_app_name="trigger", verbose=False): mgraph.reset_module_conf("mlt", mlt.ConfParams(links=mlt_links, hsi_trigger_type_passthrough=old_mlt_conf.hsi_trigger_type_passthrough, merge_overlapping_tcs=old_mlt_conf.merge_overlapping_tcs, - buffer_timeout=old_mlt_conf.buffer_timeout, + buffer_timeout=old_mlt_conf.buffer_timeout, td_out_of_timeout=old_mlt_conf.td_out_of_timeout, td_readout_limit=old_mlt_conf.td_readout_limit, ignore_tc=old_mlt_conf.ignore_tc, use_readout_map=old_mlt_conf.use_readout_map, - td_readout_map=old_mlt_conf.td_readout_map)) + td_readout_map=old_mlt_conf.td_readout_map, + use_bitwords=old_mlt_conf.use_bitwords, + trigger_bitwords=old_mlt_conf.trigger_bitwords)) def remove_mlt_link(the_system, source_id, mlt_app_name="trigger"): """ @@ -65,7 +67,9 @@ def remove_mlt_link(the_system, source_id, mlt_app_name="trigger"): td_readout_limit=old_mlt_conf.td_readout_limit, ignore_tc=old_mlt_conf.ignore_tc, use_readout_map=old_mlt_conf.use_readout_map, - td_readout_map=old_mlt_conf.td_readout_map)) + td_readout_map=old_mlt_conf.td_readout_map, + use_bitwords=old_mlt_conf.use_bitwords, + trigger_bitwords=old_mlt_conf.trigger_bitwords)) def connect_fragment_producers(app_name, the_system, verbose=False): """Connect the data request and fragment sending queues from all of diff --git a/schema/daqconf/confgen.jsonnet b/schema/daqconf/confgen.jsonnet index 3f6d89be..b9532d69 100755 --- a/schema/daqconf/confgen.jsonnet +++ b/schema/daqconf/confgen.jsonnet @@ -31,6 +31,9 @@ local cs = { tc_intervals: s.sequence( "TCIntervals", self.tc_interval, doc="List of TC intervals used by CTCM"), readout_time: s.number( "ROTime", "i8", doc="A readout time in ticks"), channel_list: s.sequence( "ChannelList", self.count, doc="List of offline channels to be masked out from the TPHandler"), + bitword: s.string( "Bitword", doc="123"), + bitword_list: s.sequence( "BitwordList", self.bitword, doc="123"), + bitwords: s.sequence( "Bitwords", self.bitword_list, doc="List of bitwords to use when forming trigger decisions in MLT" ), numa_exception: s.record( "NUMAException", [ s.field( "host", self.host, default='localhost', doc="Host of exception"), @@ -259,6 +262,8 @@ local cs = { s.field( "mlt_ignore_tc", self.tc_types, default=[], doc="Optional list of TC types to be ignored in MLT"), s.field( "mlt_use_readout_map", self.flag, default=false, doc="Option to use custom readout map in MLT"), s.field( "mlt_td_readout_map", self.tc_readout_map, default=self.tc_readout_map, doc="The readout windows assigned to TDs in MLT, based on TC type."), + s.field( "mlt_use_bitwords", self.flag, default=false, doc="Option to use bitwords (ie trigger types, coincidences) when forming trigger decisions in MLT" ), + s.field( "mlt_trigger_bitwords", self.bitwords, default=[], doc="Optional dictionary of bitwords to use when forming trigger decisions in MLT" ), s.field( "use_custom_maker", self.flag, default=false, doc="Option to use a Custom Trigger Candidate Maker (plugin)"), s.field( "ctcm_trigger_types", self.tc_types, default=[4], doc="Optional list of TC types to be used by the Custom Trigger Candidate Maker (plugin)"), s.field( "ctcm_trigger_intervals", self.tc_intervals, default=[10000000], doc="Optional list of intervals (clock ticks) for the TC types to be used by the Custom Trigger Candidate Maker (plugin)"), diff --git a/scripts/daqconf_multiru_gen b/scripts/daqconf_multiru_gen index cc041ae1..d66f4a2e 100755 --- a/scripts/daqconf_multiru_gen +++ b/scripts/daqconf_multiru_gen @@ -396,12 +396,15 @@ def cli(config, base_command_port, detector_readout_map_file, data_rate_slowdown TRIGGER_WINDOW_AFTER_TICKS = trigger.trigger_window_after_ticks, HSI_TRIGGER_TYPE_PASSTHROUGH = trigger.hsi_trigger_type_passthrough, MLT_MERGE_OVERLAPPING_TCS = trigger.mlt_merge_overlapping_tcs, - MLT_BUFFER_TIMEOUT = trigger.mlt_buffer_timeout, + MLT_BUFFER_TIMEOUT = trigger.mlt_buffer_timeout, MLT_MAX_TD_LENGTH_MS = trigger.mlt_max_td_length_ms, MLT_SEND_TIMED_OUT_TDS = trigger.mlt_send_timed_out_tds, MLT_IGNORE_TC = trigger.mlt_ignore_tc, MLT_USE_READOUT_MAP = trigger.mlt_use_readout_map, MLT_READOUT_MAP = trigger.mlt_td_readout_map, + MLT_USE_BITWORDS = trigger.mlt_use_bitwords, + MLT_TRIGGER_BITWORDS = trigger.mlt_trigger_bitwords, + USE_CUSTOM_MAKER = trigger.use_custom_maker, CTCM_TYPES = trigger.ctcm_trigger_types, CTCM_INTERVAL = trigger.ctcm_trigger_intervals, From ef433749cde4d18e0d526bf64aa85e082d5c3637 Mon Sep 17 00:00:00 2001 From: Alessandro Thea Date: Thu, 8 Jun 2023 14:19:05 +0200 Subject: [PATCH 15/90] First pass completed --- python/daqconf/apps/readout_gen.py | 1286 +++++++++++++++++++--------- python/daqconf/detreadoutmap.py | 22 +- scripts/daqconf_multiru_gen | 2 +- scripts/dromap_editor | 7 +- 4 files changed, 885 insertions(+), 432 deletions(-) diff --git a/python/daqconf/apps/readout_gen.py b/python/daqconf/apps/readout_gen.py index 96b48592..4087516c 100644 --- a/python/daqconf/apps/readout_gen.py +++ b/python/daqconf/apps/readout_gen.py @@ -119,9 +119,9 @@ def compute_data_types( return fe_type, queue_frag_type, fakedata_frag_type, fakedata_time_tick, fakedata_frame_size -### -# Fake Card Reader creator -### +# ### +# # Fake Card Reader creator +# ### # def create_fake_cardreader( # FRONTEND_TYPE: str, # QUEUE_FRAGMENT_TYPE: str, @@ -170,90 +170,90 @@ def compute_data_types( # return modules, queues -### -# FELIX Card Reader creator -### -def create_felix_cardreader( - FRONTEND_TYPE: str, - QUEUE_FRAGMENT_TYPE: str, - CARD_ID_OVERRIDE: int, - NUMA_ID: int, - RU_DESCRIPTOR # ReadoutUnitDescriptor - ) -> tuple[list, list]: - """ - Create a FELIX Card Reader (and reader->DHL Queues?) - - [CR]->queues - """ - links_slr0 = [] - links_slr1 = [] - sids_slr0 = [] - sids_slr1 = [] - for stream in RU_DESCRIPTOR.streams: - if stream.parameters.slr == 0: - links_slr0.append(stream.parameters.link) - sids_slr0.append(stream.src_id) - if stream.parameters.slr == 1: - links_slr1.append(stream.parameters.link) - sids_slr1.append(stream.src_id) - - links_slr0.sort() - links_slr1.sort() +# ### +# # FELIX Card Reader creator +# ### +# def create_felix_cardreader( +# FRONTEND_TYPE: str, +# QUEUE_FRAGMENT_TYPE: str, +# CARD_ID_OVERRIDE: int, +# NUMA_ID: int, +# RU_DESCRIPTOR # ReadoutUnitDescriptor +# ) -> tuple[list, list]: +# """ +# Create a FELIX Card Reader (and reader->DHL Queues?) - card_id = RU_DESCRIPTOR.iface if CARD_ID_OVERRIDE == -1 else CARD_ID_OVERRIDE +# [CR]->queues +# """ +# links_slr0 = [] +# links_slr1 = [] +# sids_slr0 = [] +# sids_slr1 = [] +# for stream in RU_DESCRIPTOR.streams: +# if stream.parameters.slr == 0: +# links_slr0.append(stream.parameters.link) +# sids_slr0.append(stream.src_id) +# if stream.parameters.slr == 1: +# links_slr1.append(stream.parameters.link) +# sids_slr1.append(stream.src_id) + +# links_slr0.sort() +# links_slr1.sort() + +# card_id = RU_DESCRIPTOR.iface if CARD_ID_OVERRIDE == -1 else CARD_ID_OVERRIDE - modules = [] - queues = [] - if len(links_slr0) > 0: - modules += [DAQModule(name = 'flxcard_0', - plugin = 'FelixCardReader', - conf = flxcr.Conf(card_id = card_id, - logical_unit = 0, - dma_id = 0, - chunk_trailer_size = 32, - dma_block_size_kb = 4, - dma_memory_size_gb = 4, - numa_id = NUMA_ID, - links_enabled = links_slr0 - ) - )] +# modules = [] +# queues = [] +# if len(links_slr0) > 0: +# modules += [DAQModule(name = 'flxcard_0', +# plugin = 'FelixCardReader', +# conf = flxcr.Conf(card_id = card_id, +# logical_unit = 0, +# dma_id = 0, +# chunk_trailer_size = 32, +# dma_block_size_kb = 4, +# dma_memory_size_gb = 4, +# numa_id = NUMA_ID, +# links_enabled = links_slr0 +# ) +# )] - if len(links_slr1) > 0: - modules += [DAQModule(name = "flxcard_1", - plugin = "FelixCardReader", - conf = flxcr.Conf(card_id = card_id, - logical_unit = 1, - dma_id = 0, - chunk_trailer_size = 32, - dma_block_size_kb = 4, - dma_memory_size_gb = 4, - numa_id = NUMA_ID, - links_enabled = links_slr1 - ) - )] +# if len(links_slr1) > 0: +# modules += [DAQModule(name = "flxcard_1", +# plugin = "FelixCardReader", +# conf = flxcr.Conf(card_id = card_id, +# logical_unit = 1, +# dma_id = 0, +# chunk_trailer_size = 32, +# dma_block_size_kb = 4, +# dma_memory_size_gb = 4, +# numa_id = NUMA_ID, +# links_enabled = links_slr1 +# ) +# )] - # Queues for card reader 1 - queues += [ - Queue( - f'flxcard_0.output_{idx}', - f"datahandler_{idx}.raw_input", - QUEUE_FRAGMENT_TYPE, - f'{FRONTEND_TYPE}_link_{idx}', - 100000 - ) for idx in sids_slr0 - ] - # Queues for card reader 2 - queues += [ - Queue( - f'flxcard_1.output_{idx}', - f"datahandler_{idx}.raw_input", - QUEUE_FRAGMENT_TYPE, - f'{FRONTEND_TYPE}_link_{idx}', - 100000 - ) for idx in sids_slr1 - ] +# # Queues for card reader 1 +# queues += [ +# Queue( +# f'flxcard_0.output_{idx}', +# f"datahandler_{idx}.raw_input", +# QUEUE_FRAGMENT_TYPE, +# f'{FRONTEND_TYPE}_link_{idx}', +# 100000 +# ) for idx in sids_slr0 +# ] +# # Queues for card reader 2 +# queues += [ +# Queue( +# f'flxcard_1.output_{idx}', +# f"datahandler_{idx}.raw_input", +# QUEUE_FRAGMENT_TYPE, +# f'{FRONTEND_TYPE}_link_{idx}', +# 100000 +# ) for idx in sids_slr1 +# ] - return modules, queues +# return modules, queues @@ -389,344 +389,344 @@ def build_conf_by_host(self, eal_arg_list): return conf -def create_dpdk_cardreader( - FRONTEND_TYPE: str, - QUEUE_FRAGMENT_TYPE: str, - EAL_ARGS: str, - RU_DESCRIPTOR # ReadoutUnitDescriptor - ) -> tuple[list, list]: - """ - Create a DPDK Card Reader (and reader->DHL Queues?) +# def create_dpdk_cardreader( +# FRONTEND_TYPE: str, +# QUEUE_FRAGMENT_TYPE: str, +# EAL_ARGS: str, +# RU_DESCRIPTOR # ReadoutUnitDescriptor +# ) -> tuple[list, list]: +# """ +# Create a DPDK Card Reader (and reader->DHL Queues?) - [CR]->queues - """ +# [CR]->queues +# """ - eth_ru_bldr = NICReceiverBuilder(RU_DESCRIPTOR) +# eth_ru_bldr = NICReceiverBuilder(RU_DESCRIPTOR) - nic_reader_name = f"nic_reader_{RU_DESCRIPTOR.iface}" +# nic_reader_name = f"nic_reader_{RU_DESCRIPTOR.iface}" - modules = [DAQModule( - name=nic_reader_name, - plugin="NICReceiver", - conf=eth_ru_bldr.build_conf(eal_arg_list=EAL_ARGS), - )] +# modules = [DAQModule( +# name=nic_reader_name, +# plugin="NICReceiver", +# conf=eth_ru_bldr.build_conf(eal_arg_list=EAL_ARGS), +# )] - # Queues - queues = [ - Queue( - f"{nic_reader_name}.output_{stream.src_id}", - f"datahandler_{stream.src_id}.raw_input", QUEUE_FRAGMENT_TYPE, - f'{FRONTEND_TYPE}_stream_{stream.src_id}', 100000 - ) - for stream in RU_DESCRIPTOR.streams - ] +# # Queues +# queues = [ +# Queue( +# f"{nic_reader_name}.output_{stream.src_id}", +# f"datahandler_{stream.src_id}.raw_input", QUEUE_FRAGMENT_TYPE, +# f'{FRONTEND_TYPE}_stream_{stream.src_id}', 100000 +# ) +# for stream in RU_DESCRIPTOR.streams +# ] - return modules, queues +# return modules, queues -def create_pacman_cardreader( - FRONTEND_TYPE: str, - QUEUE_FRAGMENT_TYPE: str, - RU_DESCRIPTOR # ReadoutUnitDescriptor - ) -> tuple[list, list]: - """ - Create a Pacman Cardeader - """ +# def create_pacman_cardreader( +# FRONTEND_TYPE: str, +# QUEUE_FRAGMENT_TYPE: str, +# RU_DESCRIPTOR # ReadoutUnitDescriptor +# ) -> tuple[list, list]: +# """ +# Create a Pacman Cardeader +# """ - reader_name = "nd_reader" - if FRONTEND_TYPE == 'pacman': - reader_name = "pacman_source" +# reader_name = "nd_reader" +# if FRONTEND_TYPE == 'pacman': +# reader_name = "pacman_source" - elif FRONTEND_TYPE == 'mpd': - reader_name = "mpd_source" +# elif FRONTEND_TYPE == 'mpd': +# reader_name = "mpd_source" - else: - raise RuntimeError(f"Pacman Cardreader for {FRONTEND_TYPE} not supported") - - modules = [DAQModule( - name=reader_name, - plugin="PacmanCardReader", - conf=pcr.Conf(link_confs = [pcr.LinkConfiguration(Source_ID=stream.src_id) - for stream in RU_DESCRIPTOR.streams], - zmq_receiver_timeout = 10000) - )] +# else: +# raise RuntimeError(f"Pacman Cardreader for {FRONTEND_TYPE} not supported") + +# modules = [DAQModule( +# name=reader_name, +# plugin="PacmanCardReader", +# conf=pcr.Conf(link_confs = [pcr.LinkConfiguration(Source_ID=stream.src_id) +# for stream in RU_DESCRIPTOR.streams], +# zmq_receiver_timeout = 10000) +# )] - # Queues - queues = [ - Queue( - f"{reader_name}.output_{stream.src_id}", - f"datahandler_{stream.src_id}.raw_input", QUEUE_FRAGMENT_TYPE, - f'{FRONTEND_TYPE}_stream_{stream.src_id}', 100000 - ) - for stream in RU_DESCRIPTOR.streams - ] - - return modules, queues +# # Queues +# queues = [ +# Queue( +# f"{reader_name}.output_{stream.src_id}", +# f"datahandler_{stream.src_id}.raw_input", QUEUE_FRAGMENT_TYPE, +# f'{FRONTEND_TYPE}_stream_{stream.src_id}', 100000 +# ) +# for stream in RU_DESCRIPTOR.streams +# ] + +# return modules, queues -### -# Create detector datalink handlers -### -def create_det_dhl( - LATENCY_BUFFER_SIZE: int, - LATENCY_BUFFER_NUMA_AWARE: int, - LATENCY_BUFFER_ALLOCATION_MODE: int, - NUMA_ID: int, - SEND_PARTIAL_FRAGMENTS: bool, - RAW_RECORDING_OUTPUT_DIR: str, - DATA_REQUEST_TIMEOUT: int, - FRAGMENT_SEND_TIMEOUT: int, - RAW_RECORDING_ENABLED: bool, - RU_DESCRIPTOR, # ReadoutUnitDescriptor - EMULATOR_MODE : bool +# ### +# # Create detector datalink handlers +# ### +# def create_det_dhl( +# LATENCY_BUFFER_SIZE: int, +# LATENCY_BUFFER_NUMA_AWARE: int, +# LATENCY_BUFFER_ALLOCATION_MODE: int, +# NUMA_ID: int, +# SEND_PARTIAL_FRAGMENTS: bool, +# RAW_RECORDING_OUTPUT_DIR: str, +# DATA_REQUEST_TIMEOUT: int, +# FRAGMENT_SEND_TIMEOUT: int, +# RAW_RECORDING_ENABLED: bool, +# RU_DESCRIPTOR, # ReadoutUnitDescriptor +# EMULATOR_MODE : bool - ) -> tuple[list, list]: +# ) -> tuple[list, list]: - # defaults hardcoded values - default_latency_buffer_alignment_size = 4096 - default_pop_limit_pct = 0.8 - default_pop_size_pct = 0.1 - default_stream_buffer_size = 8388608 +# # defaults hardcoded values +# default_latency_buffer_alignment_size = 4096 +# default_pop_limit_pct = 0.8 +# default_pop_size_pct = 0.1 +# default_stream_buffer_size = 8388608 - modules = [] - for stream in RU_DESCRIPTOR.streams: - geo_id = stream.geo_id - modules += [DAQModule( - name = f"datahandler_{stream.src_id}", - plugin = "DataLinkHandler", - conf = rconf.Conf( - readoutmodelconf= rconf.ReadoutModelConf( - source_queue_timeout_ms= QUEUE_POP_WAIT_MS, - # fake_trigger_flag=0, # default - source_id = stream.src_id, - send_partial_fragment_if_available = SEND_PARTIAL_FRAGMENTS - ), - latencybufferconf= rconf.LatencyBufferConf( - latency_buffer_alignment_size = default_latency_buffer_alignment_size, - latency_buffer_size = LATENCY_BUFFER_SIZE, - source_id = stream.src_id, - latency_buffer_numa_aware = LATENCY_BUFFER_NUMA_AWARE, - latency_buffer_numa_node = NUMA_ID, - latency_buffer_preallocation = LATENCY_BUFFER_ALLOCATION_MODE, - latency_buffer_intrinsic_allocator = LATENCY_BUFFER_ALLOCATION_MODE, - ), - rawdataprocessorconf= rconf.RawDataProcessorConf( - emulator_mode = EMULATOR_MODE, - crate_id = geo_id.crate_id, - slot_id = geo_id.slot_id, - link_id = geo_id.stream_id - ), - requesthandlerconf= rconf.RequestHandlerConf( - latency_buffer_size = LATENCY_BUFFER_SIZE, - pop_limit_pct = default_pop_limit_pct, - pop_size_pct = default_pop_size_pct, - source_id = stream.src_id, - det_id = RU_DESCRIPTOR.det_id, - output_file = path.join(RAW_RECORDING_OUTPUT_DIR, f"output_{RU_DESCRIPTOR.label}_{stream.src_id}.out"), - stream_buffer_size = default_stream_buffer_size, - request_timeout_ms = DATA_REQUEST_TIMEOUT, - fragment_send_timeout_ms = FRAGMENT_SEND_TIMEOUT, - enable_raw_recording = RAW_RECORDING_ENABLED, - )) - )] - queues = [] - return modules, queues +# modules = [] +# for stream in RU_DESCRIPTOR.streams: +# geo_id = stream.geo_id +# modules += [DAQModule( +# name = f"datahandler_{stream.src_id}", +# plugin = "DataLinkHandler", +# conf = rconf.Conf( +# readoutmodelconf= rconf.ReadoutModelConf( +# source_queue_timeout_ms= QUEUE_POP_WAIT_MS, +# # fake_trigger_flag=0, # default +# source_id = stream.src_id, +# send_partial_fragment_if_available = SEND_PARTIAL_FRAGMENTS +# ), +# latencybufferconf= rconf.LatencyBufferConf( +# latency_buffer_alignment_size = default_latency_buffer_alignment_size, +# latency_buffer_size = LATENCY_BUFFER_SIZE, +# source_id = stream.src_id, +# latency_buffer_numa_aware = LATENCY_BUFFER_NUMA_AWARE, +# latency_buffer_numa_node = NUMA_ID, +# latency_buffer_preallocation = LATENCY_BUFFER_ALLOCATION_MODE, +# latency_buffer_intrinsic_allocator = LATENCY_BUFFER_ALLOCATION_MODE, +# ), +# rawdataprocessorconf= rconf.RawDataProcessorConf( +# emulator_mode = EMULATOR_MODE, +# crate_id = geo_id.crate_id, +# slot_id = geo_id.slot_id, +# link_id = geo_id.stream_id +# ), +# requesthandlerconf= rconf.RequestHandlerConf( +# latency_buffer_size = LATENCY_BUFFER_SIZE, +# pop_limit_pct = default_pop_limit_pct, +# pop_size_pct = default_pop_size_pct, +# source_id = stream.src_id, +# det_id = RU_DESCRIPTOR.det_id, +# output_file = path.join(RAW_RECORDING_OUTPUT_DIR, f"output_{RU_DESCRIPTOR.label}_{stream.src_id}.out"), +# stream_buffer_size = default_stream_buffer_size, +# request_timeout_ms = DATA_REQUEST_TIMEOUT, +# fragment_send_timeout_ms = FRAGMENT_SEND_TIMEOUT, +# enable_raw_recording = RAW_RECORDING_ENABLED, +# )) +# )] +# queues = [] +# return modules, queues -### -# Enable processing in DHLs -### -def add_tp_processing( - dlh_list: list, - THRESHOLD_TPG: int, - ALGORITHM_TPG: int, - CHANNEL_MASK_TPG: list, - TPG_CHANNEL_MAP: str, - EMULATOR_MODE, - CLOCK_SPEED_HZ: int, - DATA_RATE_SLOWDOWN_FACTOR: int, - ) -> list: +# ### +# # Enable processing in DHLs +# ### +# def add_tp_processing( +# dlh_list: list, +# THRESHOLD_TPG: int, +# ALGORITHM_TPG: int, +# CHANNEL_MASK_TPG: list, +# TPG_CHANNEL_MAP: str, +# EMULATOR_MODE, +# CLOCK_SPEED_HZ: int, +# DATA_RATE_SLOWDOWN_FACTOR: int, +# ) -> list: - modules = [] +# modules = [] - # defaults hardcoded values - default_error_counter_threshold=100 - default_error_reset_freq=10000 - - - # Loop over datalink handlers to re-define the data processor configuration - for dlh in dlh_list: - - # Recover the raw data link source id - # MOOOOOO - dro_sid = dlh.conf.readoutmodelconf["source_id"] - geo_cid = dlh.conf.rawdataprocessorconf["crate_id"] - geo_sid = dlh.conf.rawdataprocessorconf["slot_id"] - geo_lid = dlh.conf.rawdataprocessorconf["link_id"] - # Re-create the module with an extended configuration - modules += [DAQModule( - name = dlh.name, - plugin = dlh.plugin, - conf = rconf.Conf( - readoutmodelconf = dlh.conf.readoutmodelconf, - latencybufferconf = dlh.conf.latencybufferconf, - requesthandlerconf = dlh.conf.requesthandlerconf, - rawdataprocessorconf= rconf.RawDataProcessorConf( - source_id = dro_sid, - crate_id = geo_cid, - slot_id = geo_sid, - link_id = geo_lid, - enable_tpg = True, - tpg_threshold = THRESHOLD_TPG, - tpg_algorithm = ALGORITHM_TPG, - tpg_channel_mask = CHANNEL_MASK_TPG, - channel_map_name = TPG_CHANNEL_MAP, - emulator_mode = EMULATOR_MODE, - clock_speed_hz = (CLOCK_SPEED_HZ / DATA_RATE_SLOWDOWN_FACTOR), - error_counter_threshold=default_error_counter_threshold, - error_reset_freq=default_error_reset_freq - ), - ) - )] +# # defaults hardcoded values +# default_error_counter_threshold=100 +# default_error_reset_freq=10000 + + +# # Loop over datalink handlers to re-define the data processor configuration +# for dlh in dlh_list: + +# # Recover the raw data link source id +# # MOOOOOO +# dro_sid = dlh.conf.readoutmodelconf["source_id"] +# geo_cid = dlh.conf.rawdataprocessorconf["crate_id"] +# geo_sid = dlh.conf.rawdataprocessorconf["slot_id"] +# geo_lid = dlh.conf.rawdataprocessorconf["link_id"] +# # Re-create the module with an extended configuration +# modules += [DAQModule( +# name = dlh.name, +# plugin = dlh.plugin, +# conf = rconf.Conf( +# readoutmodelconf = dlh.conf.readoutmodelconf, +# latencybufferconf = dlh.conf.latencybufferconf, +# requesthandlerconf = dlh.conf.requesthandlerconf, +# rawdataprocessorconf= rconf.RawDataProcessorConf( +# source_id = dro_sid, +# crate_id = geo_cid, +# slot_id = geo_sid, +# link_id = geo_lid, +# enable_tpg = True, +# tpg_threshold = THRESHOLD_TPG, +# tpg_algorithm = ALGORITHM_TPG, +# tpg_channel_mask = CHANNEL_MASK_TPG, +# channel_map_name = TPG_CHANNEL_MAP, +# emulator_mode = EMULATOR_MODE, +# clock_speed_hz = (CLOCK_SPEED_HZ / DATA_RATE_SLOWDOWN_FACTOR), +# error_counter_threshold=default_error_counter_threshold, +# error_reset_freq=default_error_reset_freq +# ), +# ) +# )] - return modules +# return modules -### -# Create TP data link handlers -### -def create_tp_dlhs( - dlh_list: list, - DATA_REQUEST_TIMEOUT: int, # To Check - FRAGMENT_SEND_TIMEOUT: int, # To Check - tpset_sid: int, - )-> tuple[list, list]: +# ### +# # Create TP data link handlers +# ### +# def create_tp_dlhs( +# dlh_list: list, +# DATA_REQUEST_TIMEOUT: int, # To Check +# FRAGMENT_SEND_TIMEOUT: int, # To Check +# tpset_sid: int, +# )-> tuple[list, list]: - default_pop_limit_pct = 0.8 - default_pop_size_pct = 0.1 - default_stream_buffer_size = 8388608 - default_latency_buffer_size = 4000000 - default_detid = 1 +# default_pop_limit_pct = 0.8 +# default_pop_size_pct = 0.1 +# default_stream_buffer_size = 8388608 +# default_latency_buffer_size = 4000000 +# default_detid = 1 - # Create the TP link handler - modules = [ - DAQModule(name = f"tp_datahandler_{tpset_sid}", - plugin = "DataLinkHandler", - conf = rconf.Conf( - readoutmodelconf = rconf.ReadoutModelConf( - source_queue_timeout_ms = QUEUE_POP_WAIT_MS, - source_id = tpset_sid - ), - latencybufferconf = rconf.LatencyBufferConf( - latency_buffer_size = default_latency_buffer_size, - source_id = tpset_sid - ), - rawdataprocessorconf = rconf.RawDataProcessorConf(enable_tpg = False), - requesthandlerconf= rconf.RequestHandlerConf( - latency_buffer_size = default_latency_buffer_size, - pop_limit_pct = default_pop_limit_pct, - pop_size_pct = default_pop_size_pct, - source_id = tpset_sid, - det_id = default_detid, - stream_buffer_size = default_stream_buffer_size, - request_timeout_ms = DATA_REQUEST_TIMEOUT, - fragment_send_timeout_ms = FRAGMENT_SEND_TIMEOUT, - enable_raw_recording = False - ) - ) - ) - ] +# # Create the TP link handler +# modules = [ +# DAQModule(name = f"tp_datahandler_{tpset_sid}", +# plugin = "DataLinkHandler", +# conf = rconf.Conf( +# readoutmodelconf = rconf.ReadoutModelConf( +# source_queue_timeout_ms = QUEUE_POP_WAIT_MS, +# source_id = tpset_sid +# ), +# latencybufferconf = rconf.LatencyBufferConf( +# latency_buffer_size = default_latency_buffer_size, +# source_id = tpset_sid +# ), +# rawdataprocessorconf = rconf.RawDataProcessorConf(enable_tpg = False), +# requesthandlerconf= rconf.RequestHandlerConf( +# latency_buffer_size = default_latency_buffer_size, +# pop_limit_pct = default_pop_limit_pct, +# pop_size_pct = default_pop_size_pct, +# source_id = tpset_sid, +# det_id = default_detid, +# stream_buffer_size = default_stream_buffer_size, +# request_timeout_ms = DATA_REQUEST_TIMEOUT, +# fragment_send_timeout_ms = FRAGMENT_SEND_TIMEOUT, +# enable_raw_recording = False +# ) +# ) +# ) +# ] - queues = [] - for dlh in dlh_list: - # extract source ids - dro_sid = dlh.conf.readoutmodelconf["source_id"] - - # Attach to the detector DLH's tp_out connector - queues += [ - Queue( - f"{dlh.name}.tp_out", - f"tp_datahandler_{tpset_sid}.raw_input", - "TriggerPrimitive", - f"tp_link_{tpset_sid}",1000000 - ) - ] +# queues = [] +# for dlh in dlh_list: +# # extract source ids +# dro_sid = dlh.conf.readoutmodelconf["source_id"] + +# # Attach to the detector DLH's tp_out connector +# queues += [ +# Queue( +# f"{dlh.name}.tp_out", +# f"tp_datahandler_{tpset_sid}.raw_input", +# "TriggerPrimitive", +# f"tp_link_{tpset_sid}",1000000 +# ) +# ] - return modules, queues +# return modules, queues -### -# Add detector endpoints and fragment producers -### -def add_dro_eps_and_fps( - mgraph: ModuleGraph, - dlh_list: list, - RUIDX: str, +# ### +# # Add detector endpoints and fragment producers +# ### +# def add_dro_eps_and_fps( +# mgraph: ModuleGraph, +# dlh_list: list, +# RUIDX: str, -) -> None: - """Adds detector readout endpoints and fragment producers""" - for dlh in dlh_list: - # print(dlh) - - # extract source ids - dro_sid = dlh.conf.readoutmodelconf['source_id'] - # tp_sid = dlh.conf.rawdataprocessorconf.tpset_sourceid - - mgraph.add_fragment_producer( - id = dro_sid, - subsystem = "Detector_Readout", - requests_in = f"datahandler_{dro_sid}.request_input", - fragments_out = f"datahandler_{dro_sid}.fragment_queue" - ) - mgraph.add_endpoint( - f"timesync_ru{RUIDX}_{dro_sid}", - f"datahandler_{dro_sid}.timesync_output", - "TimeSync", Direction.OUT, - is_pubsub=True, - toposort=False - ) +# ) -> None: +# """Adds detector readout endpoints and fragment producers""" +# for dlh in dlh_list: +# # print(dlh) + +# # extract source ids +# dro_sid = dlh.conf.readoutmodelconf['source_id'] +# # tp_sid = dlh.conf.rawdataprocessorconf.tpset_sourceid + +# mgraph.add_fragment_producer( +# id = dro_sid, +# subsystem = "Detector_Readout", +# requests_in = f"datahandler_{dro_sid}.request_input", +# fragments_out = f"datahandler_{dro_sid}.fragment_queue" +# ) +# mgraph.add_endpoint( +# f"timesync_ru_{dro_sid}", +# f"datahandler_{dro_sid}.timesync_output", +# "TimeSync", Direction.OUT, +# is_pubsub=True, +# toposort=False +# ) -### -# Add tpg endpoints and fragment producers -### -def add_tpg_eps_and_fps( - mgraph: ModuleGraph, - tpg_dlh_list: list, - RUIDX: str, +# ### +# # Add tpg endpoints and fragment producers +# ### +# def add_tpg_eps_and_fps( +# mgraph: ModuleGraph, +# tpg_dlh_list: list, +# RUIDX: str, -) -> None: - """Adds detector readout endpoints and fragment producers""" +# ) -> None: +# """Adds detector readout endpoints and fragment producers""" - for dlh in tpg_dlh_list: +# for dlh in tpg_dlh_list: - # extract source ids - tpset_sid = dlh.conf.readoutmodelconf['source_id'] +# # extract source ids +# tpset_sid = dlh.conf.readoutmodelconf['source_id'] - # Add enpointis with this source id for timesync and TPSets - mgraph.add_endpoint( - f"timesync_tp_dlh_ru{RUIDX}_{tpset_sid}", - f"tp_datahandler_{tpset_sid}.timesync_output", - "TimeSync", - Direction.OUT, - is_pubsub=True - ) +# # Add enpointis with this source id for timesync and TPSets +# mgraph.add_endpoint( +# f"timesync_tp_{tpset_sid}", +# f"tp_datahandler_{tpset_sid}.timesync_output", +# "TimeSync", +# Direction.OUT, +# is_pubsub=True +# ) - mgraph.add_endpoint( - f"tpsets_tplink{tpset_sid}", - f"tp_datahandler_{tpset_sid}.tpset_out", - "TPSet", - Direction.OUT, - is_pubsub=True - ) +# mgraph.add_endpoint( +# f"tpsets_tplink{tpset_sid}", +# f"tp_datahandler_{tpset_sid}.tpset_out", +# "TPSet", +# Direction.OUT, +# is_pubsub=True +# ) - # Add Fragment producer with this source id - mgraph.add_fragment_producer( - id = tpset_sid, subsystem = "Trigger", - requests_in = f"tp_datahandler_{tpset_sid}.request_input", - fragments_out = f"tp_datahandler_{tpset_sid}.fragment_queue" - ) +# # Add Fragment producer with this source id +# mgraph.add_fragment_producer( +# id = tpset_sid, subsystem = "Trigger", +# requests_in = f"tp_datahandler_{tpset_sid}.request_input", +# fragments_out = f"tp_datahandler_{tpset_sid}.fragment_queue" +# ) # Time to wait on pop() @@ -743,6 +743,7 @@ def __init__(self, readout_cfg): excpt = {} for ex in self.config.numa_config['exceptions']: excpt[(ex['host'], ex['card'])] = ex + self.excpt = excpt def get_numa_cfg(self, RU_DESCRIPTOR): @@ -762,9 +763,488 @@ def get_numa_cfg(self, RU_DESCRIPTOR): return (numa_id, latency_numa, latency_preallocate, flx_card_override) + ### + # Fake Card Reader creator + ### + def create_fake_cardreader( + self, + FRONTEND_TYPE: str, + QUEUE_FRAGMENT_TYPE: str, + DATA_FILES: dict, + RU_DESCRIPTOR # ReadoutUnitDescriptor + + ) -> tuple[list, list]: + """ + Create a FAKE Card reader module + """ + cfg = self.config + + conf = sec.Conf( + link_confs = [ + sec.LinkConfiguration( + source_id=s.src_id, + crate_id = s.geo_id.crate_id, + slot_id = s.geo_id.slot_id, + link_id = s.geo_id.stream_id, + slowdown=cfg.data_rate_slowdown_factor, + queue_name=f"output_{s.src_id}", + data_filename = DATA_FILES[s.geo_id.det_id] if s.geo_id.det_id in DATA_FILES.keys() else cfg.default_data_file, + emu_frame_error_rate=0 + ) for s in RU_DESCRIPTOR.streams], + use_now_as_first_data_time=cfg.emulated_data_times_start_with_now, + clock_speed_hz=cfg.clock_speed_hz, + queue_timeout_ms = QUEUE_POP_WAIT_MS + ) + + + modules = [DAQModule(name = "fake_source", + plugin = "FakeCardReader", + conf = conf)] + queues = [ + Queue( + f"fake_source.output_{s.src_id}", + f"datahandler_{s.src_id}.raw_input", + QUEUE_FRAGMENT_TYPE, + f'{FRONTEND_TYPE}_link_{s.src_id}', 100000 + ) for s in RU_DESCRIPTOR.streams + ] + + return modules, queues + + + ### + # FELIX Card Reader creator + ### + def create_felix_cardreader( + self, + FRONTEND_TYPE: str, + QUEUE_FRAGMENT_TYPE: str, + CARD_ID_OVERRIDE: int, + NUMA_ID: int, + RU_DESCRIPTOR # ReadoutUnitDescriptor + ) -> tuple[list, list]: + """ + Create a FELIX Card Reader (and reader->DHL Queues?) + + [CR]->queues + """ + links_slr0 = [] + links_slr1 = [] + sids_slr0 = [] + sids_slr1 = [] + for stream in RU_DESCRIPTOR.streams: + if stream.parameters.slr == 0: + links_slr0.append(stream.parameters.link) + sids_slr0.append(stream.src_id) + if stream.parameters.slr == 1: + links_slr1.append(stream.parameters.link) + sids_slr1.append(stream.src_id) + + links_slr0.sort() + links_slr1.sort() + + card_id = RU_DESCRIPTOR.iface if CARD_ID_OVERRIDE == -1 else CARD_ID_OVERRIDE + + modules = [] + queues = [] + if len(links_slr0) > 0: + modules += [DAQModule(name = 'flxcard_0', + plugin = 'FelixCardReader', + conf = flxcr.Conf(card_id = card_id, + logical_unit = 0, + dma_id = 0, + chunk_trailer_size = 32, + dma_block_size_kb = 4, + dma_memory_size_gb = 4, + numa_id = NUMA_ID, + links_enabled = links_slr0 + ) + )] + + if len(links_slr1) > 0: + modules += [DAQModule(name = "flxcard_1", + plugin = "FelixCardReader", + conf = flxcr.Conf(card_id = card_id, + logical_unit = 1, + dma_id = 0, + chunk_trailer_size = 32, + dma_block_size_kb = 4, + dma_memory_size_gb = 4, + numa_id = NUMA_ID, + links_enabled = links_slr1 + ) + )] + + # Queues for card reader 1 + queues += [ + Queue( + f'flxcard_0.output_{idx}', + f"datahandler_{idx}.raw_input", + QUEUE_FRAGMENT_TYPE, + f'{FRONTEND_TYPE}_link_{idx}', + 100000 + ) for idx in sids_slr0 + ] + # Queues for card reader 2 + queues += [ + Queue( + f'flxcard_1.output_{idx}', + f"datahandler_{idx}.raw_input", + QUEUE_FRAGMENT_TYPE, + f'{FRONTEND_TYPE}_link_{idx}', + 100000 + ) for idx in sids_slr1 + ] + + return modules, queues + + + def create_dpdk_cardreader( + self, + FRONTEND_TYPE: str, + QUEUE_FRAGMENT_TYPE: str, + RU_DESCRIPTOR # ReadoutUnitDescriptor + ) -> tuple[list, list]: + """ + Create a DPDK Card Reader (and reader->DHL Queues?) + + [CR]->queues + """ + + cfg = self.config + + eth_ru_bldr = NICReceiverBuilder(RU_DESCRIPTOR) + + nic_reader_name = f"nic_reader_{RU_DESCRIPTOR.iface}" + + modules = [DAQModule( + name=nic_reader_name, + plugin="NICReceiver", + conf=eth_ru_bldr.build_conf(eal_arg_list=cfg.eal_args), + )] + + # Queues + queues = [ + Queue( + f"{nic_reader_name}.output_{stream.src_id}", + f"datahandler_{stream.src_id}.raw_input", QUEUE_FRAGMENT_TYPE, + f'{FRONTEND_TYPE}_stream_{stream.src_id}', 100000 + ) + for stream in RU_DESCRIPTOR.streams + ] + + return modules, queues + + + def create_pacman_cardreader( + self, + FRONTEND_TYPE: str, + QUEUE_FRAGMENT_TYPE: str, + RU_DESCRIPTOR # ReadoutUnitDescriptor + ) -> tuple[list, list]: + """ + Create a Pacman Cardeader + """ + + reader_name = "nd_reader" + if FRONTEND_TYPE == 'pacman': + reader_name = "pacman_source" + + elif FRONTEND_TYPE == 'mpd': + reader_name = "mpd_source" + + else: + raise RuntimeError(f"Pacman Cardreader for {FRONTEND_TYPE} not supported") + + modules = [DAQModule( + name=reader_name, + plugin="PacmanCardReader", + conf=pcr.Conf(link_confs = [pcr.LinkConfiguration(Source_ID=stream.src_id) + for stream in RU_DESCRIPTOR.streams], + zmq_receiver_timeout = 10000) + )] + + # Queues + queues = [ + Queue( + f"{reader_name}.output_{stream.src_id}", + f"datahandler_{stream.src_id}.raw_input", QUEUE_FRAGMENT_TYPE, + f'{FRONTEND_TYPE}_stream_{stream.src_id}', 100000 + ) + for stream in RU_DESCRIPTOR.streams + ] + + return modules, queues + + + + + + ### + # Create detector datalink handlers + ### + def create_det_dhl( + self, + # LATENCY_BUFFER_SIZE: int, + LATENCY_BUFFER_NUMA_AWARE: int, + LATENCY_BUFFER_ALLOCATION_MODE: int, + NUMA_ID: int, + SEND_PARTIAL_FRAGMENTS: bool, + # RAW_RECORDING_OUTPUT_DIR: str, + DATA_REQUEST_TIMEOUT: int, + # FRAGMENT_SEND_TIMEOUT: int, + # RAW_RECORDING_ENABLED: bool, + RU_DESCRIPTOR, # ReadoutUnitDescriptor + # EMULATOR_MODE : bool + + ) -> tuple[list, list]: + + cfg = self.config + + # defaults hardcoded values + default_latency_buffer_alignment_size = 4096 + default_pop_limit_pct = 0.8 + default_pop_size_pct = 0.1 + default_stream_buffer_size = 8388608 + + + modules = [] + for stream in RU_DESCRIPTOR.streams: + geo_id = stream.geo_id + modules += [DAQModule( + name = f"datahandler_{stream.src_id}", + plugin = "DataLinkHandler", + conf = rconf.Conf( + readoutmodelconf= rconf.ReadoutModelConf( + source_queue_timeout_ms= QUEUE_POP_WAIT_MS, + # fake_trigger_flag=0, # default + source_id = stream.src_id, + send_partial_fragment_if_available = SEND_PARTIAL_FRAGMENTS + ), + latencybufferconf= rconf.LatencyBufferConf( + latency_buffer_alignment_size = default_latency_buffer_alignment_size, + latency_buffer_size = cfg.latency_buffer_size, + source_id = stream.src_id, + latency_buffer_numa_aware = LATENCY_BUFFER_NUMA_AWARE, + latency_buffer_numa_node = NUMA_ID, + latency_buffer_preallocation = LATENCY_BUFFER_ALLOCATION_MODE, + latency_buffer_intrinsic_allocator = LATENCY_BUFFER_ALLOCATION_MODE, + ), + rawdataprocessorconf= rconf.RawDataProcessorConf( + emulator_mode = cfg.emulator_mode, + crate_id = geo_id.crate_id, + slot_id = geo_id.slot_id, + link_id = geo_id.stream_id + ), + requesthandlerconf= rconf.RequestHandlerConf( + latency_buffer_size = cfg.latency_buffer_size, + pop_limit_pct = default_pop_limit_pct, + pop_size_pct = default_pop_size_pct, + source_id = stream.src_id, + det_id = RU_DESCRIPTOR.det_id, + output_file = path.join(cfg.raw_recording_output_dir, f"output_{RU_DESCRIPTOR.label}_{stream.src_id}.out"), + stream_buffer_size = default_stream_buffer_size, + request_timeout_ms = DATA_REQUEST_TIMEOUT, + fragment_send_timeout_ms = cfg.fragment_send_timeout_ms, + enable_raw_recording = cfg.enable_raw_recording, + )) + )] + queues = [] + return modules, queues + + + ### + # Enable processing in DHLs + ### + def add_tp_processing( + self, + dlh_list: list, + TPG_CHANNEL_MAP: str, + ) -> list: + + cfg = self.config + + modules = [] + + # defaults hardcoded values + default_error_counter_threshold=100 + default_error_reset_freq=10000 + + + # Loop over datalink handlers to re-define the data processor configuration + for dlh in dlh_list: + + # Recover the raw data link source id + # MOOOOOO + dro_sid = dlh.conf.readoutmodelconf["source_id"] + geo_cid = dlh.conf.rawdataprocessorconf["crate_id"] + geo_sid = dlh.conf.rawdataprocessorconf["slot_id"] + geo_lid = dlh.conf.rawdataprocessorconf["link_id"] + # Re-create the module with an extended configuration + modules += [DAQModule( + name = dlh.name, + plugin = dlh.plugin, + conf = rconf.Conf( + readoutmodelconf = dlh.conf.readoutmodelconf, + latencybufferconf = dlh.conf.latencybufferconf, + requesthandlerconf = dlh.conf.requesthandlerconf, + rawdataprocessorconf= rconf.RawDataProcessorConf( + source_id = dro_sid, + crate_id = geo_cid, + slot_id = geo_sid, + link_id = geo_lid, + enable_tpg = True, + tpg_threshold = cfg.tpg_threshold, + tpg_algorithm = cfg.tpg_algorithm, + tpg_channel_mask = cfg.tpg_channel_mask, + channel_map_name = TPG_CHANNEL_MAP, + emulator_mode = cfg.emulator_mode, + clock_speed_hz = (cfg.clock_speed_hz / cfg.data_rate_slowdown_factor), + error_counter_threshold=default_error_counter_threshold, + error_reset_freq=default_error_reset_freq + ), + ) + )] + + return modules + + ### + # Create TP data link handlers + ### + def create_tp_dlhs( + self, + dlh_list: list, + DATA_REQUEST_TIMEOUT: int, # To Check + FRAGMENT_SEND_TIMEOUT: int, # To Check + tpset_sid: int, + )-> tuple[list, list]: + + default_pop_limit_pct = 0.8 + default_pop_size_pct = 0.1 + default_stream_buffer_size = 8388608 + default_latency_buffer_size = 4000000 + default_detid = 1 + + + # Create the TP link handler + modules = [ + DAQModule(name = f"tp_datahandler_{tpset_sid}", + plugin = "DataLinkHandler", + conf = rconf.Conf( + readoutmodelconf = rconf.ReadoutModelConf( + source_queue_timeout_ms = QUEUE_POP_WAIT_MS, + source_id = tpset_sid + ), + latencybufferconf = rconf.LatencyBufferConf( + latency_buffer_size = default_latency_buffer_size, + source_id = tpset_sid + ), + rawdataprocessorconf = rconf.RawDataProcessorConf(enable_tpg = False), + requesthandlerconf= rconf.RequestHandlerConf( + latency_buffer_size = default_latency_buffer_size, + pop_limit_pct = default_pop_limit_pct, + pop_size_pct = default_pop_size_pct, + source_id = tpset_sid, + det_id = default_detid, + stream_buffer_size = default_stream_buffer_size, + request_timeout_ms = DATA_REQUEST_TIMEOUT, + fragment_send_timeout_ms = FRAGMENT_SEND_TIMEOUT, + enable_raw_recording = False + ) + ) + ) + ] + + queues = [] + for dlh in dlh_list: + # Attach to the detector DLH's tp_out connector + queues += [ + Queue( + f"{dlh.name}.tp_out", + f"tp_datahandler_{tpset_sid}.raw_input", + "TriggerPrimitive", + f"tp_link_{tpset_sid}",1000000 + ) + ] + + return modules, queues + + ### + # Add detector endpoints and fragment producers + ### + def add_dro_eps_and_fps( + self, + mgraph: ModuleGraph, + dlh_list: list, + ) -> None: + """Adds detector readout endpoints and fragment producers""" + for dlh in dlh_list: + # print(dlh) + + # extract source ids + dro_sid = dlh.conf.readoutmodelconf['source_id'] + # tp_sid = dlh.conf.rawdataprocessorconf.tpset_sourceid + + mgraph.add_fragment_producer( + id = dro_sid, + subsystem = "Detector_Readout", + requests_in = f"datahandler_{dro_sid}.request_input", + fragments_out = f"datahandler_{dro_sid}.fragment_queue" + ) + mgraph.add_endpoint( + f"timesync_ru_{dro_sid}", + f"datahandler_{dro_sid}.timesync_output", + "TimeSync", Direction.OUT, + is_pubsub=True, + toposort=False + ) + + + + ### + # Add tpg endpoints and fragment producers + ### + def add_tpg_eps_and_fps( + self, + mgraph: ModuleGraph, + tpg_dlh_list: list, + + ) -> None: + """Adds detector readout endpoints and fragment producers""" + + for dlh in tpg_dlh_list: + + # extract source ids + tpset_sid = dlh.conf.readoutmodelconf['source_id'] + + # Add enpointis with this source id for timesync and TPSets + mgraph.add_endpoint( + f"timesync_tp_{tpset_sid}", + f"tp_datahandler_{tpset_sid}.timesync_output", + "TimeSync", + Direction.OUT, + is_pubsub=True + ) + + mgraph.add_endpoint( + f"tpsets_tplink{tpset_sid}", + f"tp_datahandler_{tpset_sid}.tpset_out", + "TPSet", + Direction.OUT, + is_pubsub=True + ) + + # Add Fragment producer with this source id + mgraph.add_fragment_producer( + id = tpset_sid, subsystem = "Trigger", + requests_in = f"tp_datahandler_{tpset_sid}.request_input", + fragments_out = f"tp_datahandler_{tpset_sid}.fragment_queue" + ) + + def generate( self, - RU_DESCRIPTOR, + RU_DESCRIPTOR, SOURCEID_BROKER, data_file_map, tpg_channel_map, @@ -790,10 +1270,10 @@ def generate( numa_id, latency_numa, latency_preallocate, card_override = self.get_numa_cfg(RU_DESCRIPTOR) cfg = self.config - TPG_ENABLED = cfg.enable_tpg, - DATA_FILES = data_file_map, + TPG_ENABLED = cfg.enable_tpg + DATA_FILES = data_file_map # TPG_CHANNEL_MAP = tpg_channel_map, - DATA_REQUEST_TIMEOUT=data_timeout_requests, + DATA_REQUEST_TIMEOUT=data_timeout_requests FRONTEND_TYPE, QUEUE_FRAGMENT_TYPE, _, _, _ = compute_data_types(RU_DESCRIPTOR.det_id, cfg.clock_speed_hz, RU_DESCRIPTOR.kind) @@ -811,21 +1291,17 @@ def generate( # Create the card readers if cfg.use_fake_cards: - fakecr_mods, fakecr_queues = create_fake_cardreader( + fakecr_mods, fakecr_queues = self.create_fake_cardreader( FRONTEND_TYPE=FRONTEND_TYPE, QUEUE_FRAGMENT_TYPE=QUEUE_FRAGMENT_TYPE, - DATA_RATE_SLOWDOWN_FACTOR=cfg.data_rate_slowdown_factor, DATA_FILES=DATA_FILES, - DEFAULT_DATA_FILE=cfg.default_data_file, - CLOCK_SPEED_HZ=cfg.clock_speed_hz, - EMULATED_DATA_TIMES_START_WITH_NOW=cfg.emulated_data_times_start_with_now, RU_DESCRIPTOR=RU_DESCRIPTOR ) cr_mods += fakecr_mods cr_queues += fakecr_queues else: if RU_DESCRIPTOR.kind == 'flx': - flx_mods, flx_queues = create_felix_cardreader( + flx_mods, flx_queues = self.create_felix_cardreader( FRONTEND_TYPE=FRONTEND_TYPE, QUEUE_FRAGMENT_TYPE=QUEUE_FRAGMENT_TYPE, CARD_ID_OVERRIDE=card_override, @@ -836,10 +1312,9 @@ def generate( cr_queues += flx_queues elif RU_DESCRIPTOR.kind == 'eth' and RU_DESCRIPTOR.streams[0].parameters.protocol == "udp": - dpdk_mods, dpdk_queues = create_dpdk_cardreader( + dpdk_mods, dpdk_queues = self.create_dpdk_cardreader( FRONTEND_TYPE=FRONTEND_TYPE, QUEUE_FRAGMENT_TYPE=QUEUE_FRAGMENT_TYPE, - EAL_ARGS=cfg.eal_args, RU_DESCRIPTOR=RU_DESCRIPTOR ) cr_mods += dpdk_mods @@ -847,7 +1322,7 @@ def generate( elif RU_DESCRIPTOR.kind == 'eth' and RU_DESCRIPTOR.streams[0].parameters.protocol == "zmq": - pac_mods, pac_queues = create_pacman_cardreader( + pac_mods, pac_queues = self.create_pacman_cardreader( FRONTEND_TYPE=FRONTEND_TYPE, QUEUE_FRAGMENT_TYPE=QUEUE_FRAGMENT_TYPE, RU_DESCRIPTOR=RU_DESCRIPTOR @@ -859,32 +1334,22 @@ def generate( queues += cr_queues # Create the data-link handlers - dlhs_mods, _ = create_det_dhl( - LATENCY_BUFFER_SIZE=cfg.latency_buffer_size, + dlhs_mods, _ = self.create_det_dhl( + # LATENCY_BUFFER_SIZE=cfg.latency_buffer_size, LATENCY_BUFFER_NUMA_AWARE=latency_numa, LATENCY_BUFFER_ALLOCATION_MODE=latency_preallocate, NUMA_ID=numa_id, SEND_PARTIAL_FRAGMENTS=False, - RAW_RECORDING_OUTPUT_DIR=cfg.raw_recording_output_dir, DATA_REQUEST_TIMEOUT=DATA_REQUEST_TIMEOUT, - FRAGMENT_SEND_TIMEOUT=cfg.fragment_send_timeout_ms, - RAW_RECORDING_ENABLED=cfg.enable_raw_recording, RU_DESCRIPTOR=RU_DESCRIPTOR, - EMULATOR_MODE=cfg.emulator_mode ) # Configure the TP processing if requrested if TPG_ENABLED: - dlhs_mods = add_tp_processing( + dlhs_mods = self.add_tp_processing( dlh_list=dlhs_mods, - THRESHOLD_TPG=cfg.tpg_threshold, - ALGORITHM_TPG=cfg.tpg_algorithm, - CHANNEL_MASK_TPG=cfg.tpg_channel_mask, TPG_CHANNEL_MAP=tpg_channel_map, - EMULATOR_MODE=cfg.emulator_mode, - CLOCK_SPEED_HZ=cfg.clock_speed_hz, - DATA_RATE_SLOWDOWN_FACTOR=cfg.data_rate_slowdown_factor ) modules += dlhs_mods @@ -895,7 +1360,7 @@ def generate( if len(tps) != 1: raise RuntimeError(f"Could not retrieve unique element from source id map {tps}") - tpg_mods, tpg_queues = create_tp_dlhs( + tpg_mods, tpg_queues = self.create_tp_dlhs( dlh_list=dlhs_mods, DATA_REQUEST_TIMEOUT=DATA_REQUEST_TIMEOUT, FRAGMENT_SEND_TIMEOUT=cfg.fragment_send_timeout_ms, @@ -908,19 +1373,16 @@ def generate( mgraph = ModuleGraph(modules, queues=queues) # Add endpoints and frame producers to DRO data handlers - add_dro_eps_and_fps( + self.add_dro_eps_and_fps( mgraph=mgraph, dlh_list=dlhs_mods, - RUIDX=RU_DESCRIPTOR.label ) if TPG_ENABLED: # Add endpoints and frame producers to TP data handlers - add_tpg_eps_and_fps( + self.add_tpg_eps_and_fps( mgraph=mgraph, - # dlh_list=dlhs_mods, tpg_dlh_list=tpg_mods, - RUIDX=RU_DESCRIPTOR.label ) # Create the application diff --git a/python/daqconf/detreadoutmap.py b/python/daqconf/detreadoutmap.py index 89a8f8f2..2b0f6b6a 100644 --- a/python/daqconf/detreadoutmap.py +++ b/python/daqconf/detreadoutmap.py @@ -92,21 +92,6 @@ def app_name(self): class DetReadoutMapService: """Detector - Readout Link mapping""" - # _tech_map = { - # 'flx': (FelixStreamParameters, dromap.FelixStreamParameters), - # 'eth': (EthStreamParameters, dromap.EthStreamParameters), - # } - - # _host_label_map = { - # 'flx': 'host', - # 'eth': 'rx_host', - # } - - # _iflabel_map = { - # 'flx': 'card', - # 'eth': 'rx_iface', - # } - _traits_map = { 'flx': StreamKindTraits(FelixStreamParameters, dromap.FelixStreamParameters, 'host', 'card'), 'eth': StreamKindTraits(EthStreamParameters, dromap.EthStreamParameters, 'rx_host', 'rx_iface'), @@ -134,7 +119,7 @@ def __init__(self): self._map = {} - def load(self, map_path: str, merge: bool = False) -> None: + def load(self, map_path: str, merge: bool = False, offset: int = 0) -> None: map_fp = pathlib.Path(map_path) @@ -147,8 +132,13 @@ def load(self, map_path: str, merge: bool = False) -> None: self._validate_json(data) + streams = self._build_streams(data) + print(f"Offset = {offset}") + if offset: + streams = [s._replace(src_id = s.src_id + offset) for s in streams] + if merge: src_id_max = max(self.get())+1 if self.get() else 0 new_src_id_min = min([s.src_id for s in streams]) diff --git a/scripts/daqconf_multiru_gen b/scripts/daqconf_multiru_gen index b858f246..c9b408d9 100755 --- a/scripts/daqconf_multiru_gen +++ b/scripts/daqconf_multiru_gen @@ -155,7 +155,7 @@ def cli( console.log("Loading dqm config generator") from daqconf.apps.dqm_gen import get_dqm_app console.log("Loading readout config generator") - from daqconf.apps.readout_gen import create_readout_app, create_fake_reaout_app, ReadoutAppGenerator + from daqconf.apps.readout_gen import create_fake_reaout_app, ReadoutAppGenerator console.log("Loading trigger config generator") from daqconf.apps.trigger_gen import get_trigger_app console.log("Loading DFO config generator") diff --git a/scripts/dromap_editor b/scripts/dromap_editor index 2c2384b0..4ecb81ba 100755 --- a/scripts/dromap_editor +++ b/scripts/dromap_editor @@ -27,11 +27,12 @@ def cli(obj): @cli.command('load', help="Load map from file") @click.argument('path', type=click.Path(exists=True)) -@click.option('--merge', is_flag=True, type=bool, default=False) +@click.option('-m', '--merge', is_flag=True, type=bool, default=False) +@click.option('-o', '--offset', type=int, default=0) @click.pass_obj -def load(obj, path, merge): +def load(obj, path, merge, offset): m = obj - m.load(path, merge) + m.load(path, merge, offset) console.print(m.as_table()) From 98071236a9c350a8fc49b091c8576ddbda1b20f4 Mon Sep 17 00:00:00 2001 From: Alessandro Thea Date: Thu, 8 Jun 2023 23:39:26 +0200 Subject: [PATCH 16/90] Adding support for subfiles --- python/daqconf/apps/readout_gen.py | 484 +---------------------------- python/daqconf/core/config_file.py | 56 +++- schema/daqconf/confgen.jsonnet | 10 +- scripts/daqconf_multiru_gen | 65 +--- test/scripts/check_np04_configs.py | 80 +++++ 5 files changed, 132 insertions(+), 563 deletions(-) create mode 100755 test/scripts/check_np04_configs.py diff --git a/python/daqconf/apps/readout_gen.py b/python/daqconf/apps/readout_gen.py index 4087516c..aa3c3095 100644 --- a/python/daqconf/apps/readout_gen.py +++ b/python/daqconf/apps/readout_gen.py @@ -119,144 +119,6 @@ def compute_data_types( return fe_type, queue_frag_type, fakedata_frag_type, fakedata_time_tick, fakedata_frame_size -# ### -# # Fake Card Reader creator -# ### -# def create_fake_cardreader( -# FRONTEND_TYPE: str, -# QUEUE_FRAGMENT_TYPE: str, -# DATA_RATE_SLOWDOWN_FACTOR: int, -# DATA_FILES: dict, -# DEFAULT_DATA_FILE: str, -# CLOCK_SPEED_HZ: int, -# EMULATED_DATA_TIMES_START_WITH_NOW: bool, -# RU_DESCRIPTOR # ReadoutUnitDescriptor - -# ) -> tuple[list, list]: -# """ -# Create a FAKE Card reader module -# """ - -# conf = sec.Conf( -# link_confs = [ -# sec.LinkConfiguration( -# source_id=s.src_id, -# crate_id = s.geo_id.crate_id, -# slot_id = s.geo_id.slot_id, -# link_id = s.geo_id.stream_id, -# slowdown=DATA_RATE_SLOWDOWN_FACTOR, -# queue_name=f"output_{s.src_id}", -# data_filename = DATA_FILES[s.geo_id.det_id] if s.geo_id.det_id in DATA_FILES.keys() else DEFAULT_DATA_FILE, -# emu_frame_error_rate=0 -# ) for s in RU_DESCRIPTOR.streams], -# use_now_as_first_data_time=EMULATED_DATA_TIMES_START_WITH_NOW, -# clock_speed_hz=CLOCK_SPEED_HZ, -# queue_timeout_ms = QUEUE_POP_WAIT_MS -# ) - - -# modules = [DAQModule(name = "fake_source", -# plugin = "FakeCardReader", -# conf = conf)] -# queues = [ -# Queue( -# f"fake_source.output_{s.src_id}", -# f"datahandler_{s.src_id}.raw_input", -# QUEUE_FRAGMENT_TYPE, -# f'{FRONTEND_TYPE}_link_{s.src_id}', 100000 -# ) for s in RU_DESCRIPTOR.streams -# ] - -# return modules, queues - - -# ### -# # FELIX Card Reader creator -# ### -# def create_felix_cardreader( -# FRONTEND_TYPE: str, -# QUEUE_FRAGMENT_TYPE: str, -# CARD_ID_OVERRIDE: int, -# NUMA_ID: int, -# RU_DESCRIPTOR # ReadoutUnitDescriptor -# ) -> tuple[list, list]: -# """ -# Create a FELIX Card Reader (and reader->DHL Queues?) - -# [CR]->queues -# """ -# links_slr0 = [] -# links_slr1 = [] -# sids_slr0 = [] -# sids_slr1 = [] -# for stream in RU_DESCRIPTOR.streams: -# if stream.parameters.slr == 0: -# links_slr0.append(stream.parameters.link) -# sids_slr0.append(stream.src_id) -# if stream.parameters.slr == 1: -# links_slr1.append(stream.parameters.link) -# sids_slr1.append(stream.src_id) - -# links_slr0.sort() -# links_slr1.sort() - -# card_id = RU_DESCRIPTOR.iface if CARD_ID_OVERRIDE == -1 else CARD_ID_OVERRIDE - -# modules = [] -# queues = [] -# if len(links_slr0) > 0: -# modules += [DAQModule(name = 'flxcard_0', -# plugin = 'FelixCardReader', -# conf = flxcr.Conf(card_id = card_id, -# logical_unit = 0, -# dma_id = 0, -# chunk_trailer_size = 32, -# dma_block_size_kb = 4, -# dma_memory_size_gb = 4, -# numa_id = NUMA_ID, -# links_enabled = links_slr0 -# ) -# )] - -# if len(links_slr1) > 0: -# modules += [DAQModule(name = "flxcard_1", -# plugin = "FelixCardReader", -# conf = flxcr.Conf(card_id = card_id, -# logical_unit = 1, -# dma_id = 0, -# chunk_trailer_size = 32, -# dma_block_size_kb = 4, -# dma_memory_size_gb = 4, -# numa_id = NUMA_ID, -# links_enabled = links_slr1 -# ) -# )] - -# # Queues for card reader 1 -# queues += [ -# Queue( -# f'flxcard_0.output_{idx}', -# f"datahandler_{idx}.raw_input", -# QUEUE_FRAGMENT_TYPE, -# f'{FRONTEND_TYPE}_link_{idx}', -# 100000 -# ) for idx in sids_slr0 -# ] -# # Queues for card reader 2 -# queues += [ -# Queue( -# f'flxcard_1.output_{idx}', -# f"datahandler_{idx}.raw_input", -# QUEUE_FRAGMENT_TYPE, -# f'{FRONTEND_TYPE}_link_{idx}', -# 100000 -# ) for idx in sids_slr1 -# ] - -# return modules, queues - - - ### # DPDK Card Reader creator ### @@ -295,7 +157,7 @@ def streams_by_iface_and_tx_endpoint(self): # m = group_by_key(self.desc.streams, lambda s: (getattr(s.parameters, self.desc._host_label_map[s.kind]), getattr(s.parameters, self.desc._iflabel_map[s.kind]), s.kind, s.geo_id.det_id)) # return m - def build_conf(self, eal_arg_list): + def build_conf(self, eal_arg_list, rxqueues_per_lcore): streams_by_if_and_tx = self.streams_by_iface_and_tx_endpoint() @@ -320,7 +182,7 @@ def build_conf(self, eal_arg_list): nrc.Source( id=sid, # FIXME what is this ID? ip_addr=tx_ip, - lcore=sid+self.lcore_offset, + lcore=(sid//rxqueues_per_lcore)+self.lcore_offset, rx_q=sid, src_info=si, src_streams_mapping=ssm @@ -389,343 +251,6 @@ def build_conf_by_host(self, eal_arg_list): return conf -# def create_dpdk_cardreader( -# FRONTEND_TYPE: str, -# QUEUE_FRAGMENT_TYPE: str, -# EAL_ARGS: str, -# RU_DESCRIPTOR # ReadoutUnitDescriptor -# ) -> tuple[list, list]: -# """ -# Create a DPDK Card Reader (and reader->DHL Queues?) - -# [CR]->queues -# """ - -# eth_ru_bldr = NICReceiverBuilder(RU_DESCRIPTOR) - -# nic_reader_name = f"nic_reader_{RU_DESCRIPTOR.iface}" - -# modules = [DAQModule( -# name=nic_reader_name, -# plugin="NICReceiver", -# conf=eth_ru_bldr.build_conf(eal_arg_list=EAL_ARGS), -# )] - -# # Queues -# queues = [ -# Queue( -# f"{nic_reader_name}.output_{stream.src_id}", -# f"datahandler_{stream.src_id}.raw_input", QUEUE_FRAGMENT_TYPE, -# f'{FRONTEND_TYPE}_stream_{stream.src_id}', 100000 -# ) -# for stream in RU_DESCRIPTOR.streams -# ] - -# return modules, queues - -# def create_pacman_cardreader( -# FRONTEND_TYPE: str, -# QUEUE_FRAGMENT_TYPE: str, -# RU_DESCRIPTOR # ReadoutUnitDescriptor -# ) -> tuple[list, list]: -# """ -# Create a Pacman Cardeader -# """ - -# reader_name = "nd_reader" -# if FRONTEND_TYPE == 'pacman': -# reader_name = "pacman_source" - -# elif FRONTEND_TYPE == 'mpd': -# reader_name = "mpd_source" - -# else: -# raise RuntimeError(f"Pacman Cardreader for {FRONTEND_TYPE} not supported") - -# modules = [DAQModule( -# name=reader_name, -# plugin="PacmanCardReader", -# conf=pcr.Conf(link_confs = [pcr.LinkConfiguration(Source_ID=stream.src_id) -# for stream in RU_DESCRIPTOR.streams], -# zmq_receiver_timeout = 10000) -# )] - -# # Queues -# queues = [ -# Queue( -# f"{reader_name}.output_{stream.src_id}", -# f"datahandler_{stream.src_id}.raw_input", QUEUE_FRAGMENT_TYPE, -# f'{FRONTEND_TYPE}_stream_{stream.src_id}', 100000 -# ) -# for stream in RU_DESCRIPTOR.streams -# ] - -# return modules, queues - - -# ### -# # Create detector datalink handlers -# ### -# def create_det_dhl( -# LATENCY_BUFFER_SIZE: int, -# LATENCY_BUFFER_NUMA_AWARE: int, -# LATENCY_BUFFER_ALLOCATION_MODE: int, -# NUMA_ID: int, -# SEND_PARTIAL_FRAGMENTS: bool, -# RAW_RECORDING_OUTPUT_DIR: str, -# DATA_REQUEST_TIMEOUT: int, -# FRAGMENT_SEND_TIMEOUT: int, -# RAW_RECORDING_ENABLED: bool, -# RU_DESCRIPTOR, # ReadoutUnitDescriptor -# EMULATOR_MODE : bool - -# ) -> tuple[list, list]: - - -# # defaults hardcoded values -# default_latency_buffer_alignment_size = 4096 -# default_pop_limit_pct = 0.8 -# default_pop_size_pct = 0.1 -# default_stream_buffer_size = 8388608 - - -# modules = [] -# for stream in RU_DESCRIPTOR.streams: -# geo_id = stream.geo_id -# modules += [DAQModule( -# name = f"datahandler_{stream.src_id}", -# plugin = "DataLinkHandler", -# conf = rconf.Conf( -# readoutmodelconf= rconf.ReadoutModelConf( -# source_queue_timeout_ms= QUEUE_POP_WAIT_MS, -# # fake_trigger_flag=0, # default -# source_id = stream.src_id, -# send_partial_fragment_if_available = SEND_PARTIAL_FRAGMENTS -# ), -# latencybufferconf= rconf.LatencyBufferConf( -# latency_buffer_alignment_size = default_latency_buffer_alignment_size, -# latency_buffer_size = LATENCY_BUFFER_SIZE, -# source_id = stream.src_id, -# latency_buffer_numa_aware = LATENCY_BUFFER_NUMA_AWARE, -# latency_buffer_numa_node = NUMA_ID, -# latency_buffer_preallocation = LATENCY_BUFFER_ALLOCATION_MODE, -# latency_buffer_intrinsic_allocator = LATENCY_BUFFER_ALLOCATION_MODE, -# ), -# rawdataprocessorconf= rconf.RawDataProcessorConf( -# emulator_mode = EMULATOR_MODE, -# crate_id = geo_id.crate_id, -# slot_id = geo_id.slot_id, -# link_id = geo_id.stream_id -# ), -# requesthandlerconf= rconf.RequestHandlerConf( -# latency_buffer_size = LATENCY_BUFFER_SIZE, -# pop_limit_pct = default_pop_limit_pct, -# pop_size_pct = default_pop_size_pct, -# source_id = stream.src_id, -# det_id = RU_DESCRIPTOR.det_id, -# output_file = path.join(RAW_RECORDING_OUTPUT_DIR, f"output_{RU_DESCRIPTOR.label}_{stream.src_id}.out"), -# stream_buffer_size = default_stream_buffer_size, -# request_timeout_ms = DATA_REQUEST_TIMEOUT, -# fragment_send_timeout_ms = FRAGMENT_SEND_TIMEOUT, -# enable_raw_recording = RAW_RECORDING_ENABLED, -# )) -# )] -# queues = [] -# return modules, queues - - -# ### -# # Enable processing in DHLs -# ### -# def add_tp_processing( -# dlh_list: list, -# THRESHOLD_TPG: int, -# ALGORITHM_TPG: int, -# CHANNEL_MASK_TPG: list, -# TPG_CHANNEL_MAP: str, -# EMULATOR_MODE, -# CLOCK_SPEED_HZ: int, -# DATA_RATE_SLOWDOWN_FACTOR: int, -# ) -> list: - -# modules = [] - -# # defaults hardcoded values -# default_error_counter_threshold=100 -# default_error_reset_freq=10000 - - -# # Loop over datalink handlers to re-define the data processor configuration -# for dlh in dlh_list: - -# # Recover the raw data link source id -# # MOOOOOO -# dro_sid = dlh.conf.readoutmodelconf["source_id"] -# geo_cid = dlh.conf.rawdataprocessorconf["crate_id"] -# geo_sid = dlh.conf.rawdataprocessorconf["slot_id"] -# geo_lid = dlh.conf.rawdataprocessorconf["link_id"] -# # Re-create the module with an extended configuration -# modules += [DAQModule( -# name = dlh.name, -# plugin = dlh.plugin, -# conf = rconf.Conf( -# readoutmodelconf = dlh.conf.readoutmodelconf, -# latencybufferconf = dlh.conf.latencybufferconf, -# requesthandlerconf = dlh.conf.requesthandlerconf, -# rawdataprocessorconf= rconf.RawDataProcessorConf( -# source_id = dro_sid, -# crate_id = geo_cid, -# slot_id = geo_sid, -# link_id = geo_lid, -# enable_tpg = True, -# tpg_threshold = THRESHOLD_TPG, -# tpg_algorithm = ALGORITHM_TPG, -# tpg_channel_mask = CHANNEL_MASK_TPG, -# channel_map_name = TPG_CHANNEL_MAP, -# emulator_mode = EMULATOR_MODE, -# clock_speed_hz = (CLOCK_SPEED_HZ / DATA_RATE_SLOWDOWN_FACTOR), -# error_counter_threshold=default_error_counter_threshold, -# error_reset_freq=default_error_reset_freq -# ), -# ) -# )] - -# return modules - -# ### -# # Create TP data link handlers -# ### -# def create_tp_dlhs( -# dlh_list: list, -# DATA_REQUEST_TIMEOUT: int, # To Check -# FRAGMENT_SEND_TIMEOUT: int, # To Check -# tpset_sid: int, -# )-> tuple[list, list]: - -# default_pop_limit_pct = 0.8 -# default_pop_size_pct = 0.1 -# default_stream_buffer_size = 8388608 -# default_latency_buffer_size = 4000000 -# default_detid = 1 - - -# # Create the TP link handler -# modules = [ -# DAQModule(name = f"tp_datahandler_{tpset_sid}", -# plugin = "DataLinkHandler", -# conf = rconf.Conf( -# readoutmodelconf = rconf.ReadoutModelConf( -# source_queue_timeout_ms = QUEUE_POP_WAIT_MS, -# source_id = tpset_sid -# ), -# latencybufferconf = rconf.LatencyBufferConf( -# latency_buffer_size = default_latency_buffer_size, -# source_id = tpset_sid -# ), -# rawdataprocessorconf = rconf.RawDataProcessorConf(enable_tpg = False), -# requesthandlerconf= rconf.RequestHandlerConf( -# latency_buffer_size = default_latency_buffer_size, -# pop_limit_pct = default_pop_limit_pct, -# pop_size_pct = default_pop_size_pct, -# source_id = tpset_sid, -# det_id = default_detid, -# stream_buffer_size = default_stream_buffer_size, -# request_timeout_ms = DATA_REQUEST_TIMEOUT, -# fragment_send_timeout_ms = FRAGMENT_SEND_TIMEOUT, -# enable_raw_recording = False -# ) -# ) -# ) -# ] - -# queues = [] -# for dlh in dlh_list: -# # extract source ids -# dro_sid = dlh.conf.readoutmodelconf["source_id"] - -# # Attach to the detector DLH's tp_out connector -# queues += [ -# Queue( -# f"{dlh.name}.tp_out", -# f"tp_datahandler_{tpset_sid}.raw_input", -# "TriggerPrimitive", -# f"tp_link_{tpset_sid}",1000000 -# ) -# ] - -# return modules, queues - -# ### -# # Add detector endpoints and fragment producers -# ### -# def add_dro_eps_and_fps( -# mgraph: ModuleGraph, -# dlh_list: list, -# RUIDX: str, - -# ) -> None: -# """Adds detector readout endpoints and fragment producers""" -# for dlh in dlh_list: -# # print(dlh) - -# # extract source ids -# dro_sid = dlh.conf.readoutmodelconf['source_id'] -# # tp_sid = dlh.conf.rawdataprocessorconf.tpset_sourceid - -# mgraph.add_fragment_producer( -# id = dro_sid, -# subsystem = "Detector_Readout", -# requests_in = f"datahandler_{dro_sid}.request_input", -# fragments_out = f"datahandler_{dro_sid}.fragment_queue" -# ) -# mgraph.add_endpoint( -# f"timesync_ru_{dro_sid}", -# f"datahandler_{dro_sid}.timesync_output", -# "TimeSync", Direction.OUT, -# is_pubsub=True, -# toposort=False -# ) - - - -# ### -# # Add tpg endpoints and fragment producers -# ### -# def add_tpg_eps_and_fps( -# mgraph: ModuleGraph, -# tpg_dlh_list: list, -# RUIDX: str, - -# ) -> None: -# """Adds detector readout endpoints and fragment producers""" - -# for dlh in tpg_dlh_list: - -# # extract source ids -# tpset_sid = dlh.conf.readoutmodelconf['source_id'] - -# # Add enpointis with this source id for timesync and TPSets -# mgraph.add_endpoint( -# f"timesync_tp_{tpset_sid}", -# f"tp_datahandler_{tpset_sid}.timesync_output", -# "TimeSync", -# Direction.OUT, -# is_pubsub=True -# ) - -# mgraph.add_endpoint( -# f"tpsets_tplink{tpset_sid}", -# f"tp_datahandler_{tpset_sid}.tpset_out", -# "TPSet", -# Direction.OUT, -# is_pubsub=True -# ) - -# # Add Fragment producer with this source id -# mgraph.add_fragment_producer( -# id = tpset_sid, subsystem = "Trigger", -# requests_in = f"tp_datahandler_{tpset_sid}.request_input", -# fragments_out = f"tp_datahandler_{tpset_sid}.fragment_queue" # ) @@ -920,7 +445,10 @@ def create_dpdk_cardreader( modules = [DAQModule( name=nic_reader_name, plugin="NICReceiver", - conf=eth_ru_bldr.build_conf(eal_arg_list=cfg.eal_args), + conf=eth_ru_bldr.build_conf( + eal_arg_list=cfg.dpdk_eal_args, + rxqueues_per_lcore=cfg.dpdk_rxqueues_per_lcore + ), )] # Queues diff --git a/python/daqconf/core/config_file.py b/python/daqconf/core/config_file.py index 393ea399..5389a107 100755 --- a/python/daqconf/core/config_file.py +++ b/python/daqconf/core/config_file.py @@ -2,12 +2,12 @@ import math import sys import glob -import rich.traceback from rich.console import Console from collections import defaultdict from os.path import exists, join -import random +import json import string +from pathlib import Path console = Console() # Set moo schema search path @@ -40,22 +40,48 @@ def _strict_recursive_update(dico1, dico2): def parse_json(filename, schemed_object): console.log(f"Parsing config json file {filename}") - with open(filename, 'r') as f: + + filepath = Path(filename) + basepath = filepath.parent + + # First pass, load the main json file + with open(filepath, 'r') as f: try: - import json - try: - new_parameters = json.load(f) - # Validate the heck out of this but that doesn't change the object itself (ARG) - _strict_recursive_update(schemed_object.pod(), new_parameters) - # now its validated, update the object with moo - schemed_object.update(new_parameters) - except Exception as e: - raise RuntimeError(f'Couldn\'t update the object {schemed_object} with the file {filename},\nError: {e}') + new_parameters = json.load(f) except Exception as e: - raise RuntimeError(f"Couldn't parse {filename}, error: {str(e)}") - return schemed_object + raise RuntimeError(f"Couldn't parse {filepath}, error: {str(e)}") + + # second pass, look for references + subkeys = [ k for k,v in schemed_object.pod().items() if isinstance(v,dict) ] + for k in new_parameters: + # look for keys that are associated to dicts in the schemed_obj but here are strings + v = new_parameters[k] + if isinstance(v,str) and k in subkeys: + # It's a string! It's a reference! Try loading it + subfile_path = Path(os.path.expandvars(v)).expanduser() + if not subfile_path.is_absolute(): + subfile_path = filepath.parent / subfile_path + if not subfile_path.exists(): + raise RuntimeError(f'Cannot find the file {v} ({subfile_path})') + + console.log(f"Detected subconfiguration for {k} {v} - loading {subfile_path}") + with open(subfile_path, 'r') as f: + try: + new_subpars = json.load(f) + except Exception as e: + raise RuntimeError(f"Couldn't parse {subfile_path}, error: {str(e)}") + new_parameters[k] = new_subpars + + + try: + # Validate the heck out of this but that doesn't change the object itself (ARG) + _strict_recursive_update(schemed_object.pod(), new_parameters) + # now its validated, update the object with moo + schemed_object.update(new_parameters) + except Exception as e: + raise RuntimeError(f'Couldn\'t update the object {schemed_object} with the file {filename},\nError: {e}') - raise RuntimeError(f"Couldn't find file {filename}") + return schemed_object # def _recursive_section(sections, data): diff --git a/schema/daqconf/confgen.jsonnet b/schema/daqconf/confgen.jsonnet index 4bfeae53..77ff7b55 100755 --- a/schema/daqconf/confgen.jsonnet +++ b/schema/daqconf/confgen.jsonnet @@ -140,14 +140,14 @@ local cs = { readout: s.record("readout", [ s.field( "detector_readout_map_file", self.path, default='./DetectorReadoutMap.json', doc="File containing detector hardware map for configuration to run"), + s.field( "clock_speed_hz", self.freq, default=62500000), s.field( "use_fake_data_producers", self.flag, default=false, doc="Use fake data producers that respond with empty fragments immediately instead of (fake) cards and DLHs"), s.field( "emulator_mode", self.flag, default=false, doc="If active, timestamps of data frames are overwritten when processed by the readout. This is necessary if the felix card does not set correct timestamps. Former -e"), s.field( "thread_pinning_file", self.path, default="", doc="A thread pinning configuration file that gets executed after conf."), s.field( "data_rate_slowdown_factor",self.count, default=1, doc="Factor by which to suppress data generation. Former -s"), - s.field( "clock_speed_hz", self.freq, default=62500000), + s.field( "use_fake_cards", self.flag, default=false, doc="Use fake cards"), s.field( "default_data_file", self.path, default='asset://?label=ProtoWIB&subsystem=readout', doc="File containing data frames to be replayed by the fake cards. Former -d. Uses the asset manager, can also be 'asset://?checksum=somelonghash', or 'file://somewhere/frames.bin' or 'frames.bin'"), s.field( "data_files", self.data_files, default=[], doc="Files to use by detector type"), - s.field( "use_fake_cards", self.flag, default=false, doc="Use fake cards"), s.field( "latency_buffer_size", self.count, default=499968, doc="Size of the latency buffers (in number of elements)"), s.field( "fragment_send_timeout_ms", self.count, default=10, doc="The send timeout that will be used in the readout modules when sending fragments downstream (i.e. to the TRB)."), s.field( "enable_tpg", self.flag, default=false, doc="Enable TPG"), @@ -156,10 +156,8 @@ local cs = { s.field( "tpg_channel_mask", self.channel_list, default=[], doc="List of offline channels to be masked out from the TPHandler"), s.field( "enable_raw_recording", self.flag, default=false, doc="Add queues and modules necessary for the record command"), s.field( "raw_recording_output_dir", self.path, default='.', doc="Output directory where recorded data is written to. Data for each link is written to a separate file"), - s.field( "host_dpdk_reader", self.hosts, default=['np04-srv-022'], doc="Which host to use to receive frames"), - s.field( "eal_args", self.string, default='-l 0-1 -n 3 -- -m [0:1].0 -j', doc='Args passed to the EAL in DPDK'), - s.field( "base_source_ip", self.string, default='10.73.139.', doc='First part of the IP of the source'), - s.field( "destination_ip", self.string, default='10.73.139.17', doc='IP of the destination'), + s.field( "dpdk_eal_args", self.string, default='-l 0-1 -n 3 -- -m [0:1].0 -j', doc='Args passed to the EAL in DPDK'), + s.field( "dpdk_rxqueues_per_lcore", self.count, default=1, doc='Number of rx queues per core'), s.field( "numa_config", self.numa_config, default=self.numa_config, doc='Configuration of FELIX NUMA IDs'), s.field( "emulated_data_times_start_with_now", self.flag, default=false, doc="If active, the timestamp of the first emulated data frame is set to the current wallclock time"), ]), diff --git a/scripts/daqconf_multiru_gen b/scripts/daqconf_multiru_gen index c9b408d9..bb815052 100755 --- a/scripts/daqconf_multiru_gen +++ b/scripts/daqconf_multiru_gen @@ -475,70 +475,7 @@ def cli( tpg_channel_map=trigger.tpg_channel_map, data_timeout_requests=readout_data_request_timeout ) - # numa_id = readout.numa_config['default_id'] - # latency_numa = readout.numa_config['default_latency_numa_aware'] - # latency_preallocate = readout.numa_config['default_latency_preallocation'] - # card_override = -1 - # for ex in readout.numa_config['exceptions']: - # if ex['host'] == ru_desc.host_name and ex['card'] == ru_desc.iface: - # numa_id = ex['numa_id'] - # latency_numa = ex['latency_buffer_numa_aware'] - # latency_preallocate = ex['latency_buffer_preallocation'] - # card_override = ex['felix_card_id'] - - # the_system.apps[ru_name] = create_readout_app( - # RU_DESCRIPTOR = ru_desc, - # SOURCEID_BROKER = sourceid_broker, - # EMULATOR_MODE = readout.emulator_mode, - # DATA_RATE_SLOWDOWN_FACTOR = readout.data_rate_slowdown_factor, - # DEFAULT_DATA_FILE = readout.default_data_file, - # DATA_FILES = data_file_map, - # USE_FAKE_CARDS = readout.use_fake_cards, - # CLOCK_SPEED_HZ = readout.clock_speed_hz, - # RAW_RECORDING_ENABLED = readout.enable_raw_recording, - # RAW_RECORDING_OUTPUT_DIR = readout.raw_recording_output_dir, - # TPG_ENABLED = readout.enable_tpg, - # THRESHOLD_TPG = readout.tpg_threshold, - # ALGORITHM_TPG = readout.tpg_algorithm, - # CHANNEL_MASK_TPG = readout.tpg_channel_mask, - # TPG_CHANNEL_MAP = trigger.tpg_channel_map, - # LATENCY_BUFFER_SIZE=readout.latency_buffer_size, - # DATA_REQUEST_TIMEOUT=readout_data_request_timeout, - # FRAGMENT_SEND_TIMEOUT=readout.fragment_send_timeout_ms, - # EAL_ARGS=readout.eal_args, - # NUMA_ID = numa_id, - # LATENCY_BUFFER_NUMA_AWARE = latency_numa, - # LATENCY_BUFFER_ALLOCATION_MODE = latency_preallocate, - # CARD_ID_OVERRIDE = card_override, - # EMULATED_DATA_TIMES_START_WITH_NOW = readout.emulated_data_times_start_with_now, - # DEBUG=debug) - - # if use_k8s: - # if ru_desc.kind == 'flx': - # c = card_override if card_override != -1 else ru_desc.iface - # the_system.apps[ru_name].resources = { - # f"felix.cern/flx{c}-data": "1", # requesting FLX{c} - # "memory": "64Gi" # yes bro - # } - - # dir_names = set() - - # if os.path.commonprefix(['/cvmfs', readout.default_data_file]) != '/cvmfs': - # dir_names.add(dirname(readout.default_data_file)) - - # for id,file in data_file_map: - # if os.path.commonprefix(['/cvmfs', file]) != '/cvmfs': - # dir_names.add(dirname(file)) - - # dirindex = 0 - # for dir_name in dir_names: - # the_system.apps[ru_name].mounted_dirs += [{ - # 'name': f'frames-bin-{dirindex}', - # 'physical_location': dir_name, - # 'in_pod_location': dir_name, - # 'read_only': True, - # }] - # dirindex += 1 + else: the_system.apps[ru_name] = create_fake_reaout_app( RU_DESCRIPTOR = ru_desc, diff --git a/test/scripts/check_np04_configs.py b/test/scripts/check_np04_configs.py new file mode 100755 index 00000000..be735bdb --- /dev/null +++ b/test/scripts/check_np04_configs.py @@ -0,0 +1,80 @@ +#!/usr/bin/env python + + + +import click +import json +import requests +from pathlib import Path +from os import environ as env +from rich import print + +host = 'np04-srv-023' +port = '31011' + +# # http://np04-srv-023:31011/listVersions?name=thea-k8s-test + + +@click.group() +def cli(): + pass + + +@cli.command('list') +def list_configs(): + uri = f'http://{host}:{port}/listConfigs' + print(uri) + r = requests.get(uri) + if (r.status_code != 200): + click.Errors("Failed to read the configurations list from db") + + res = r.json() + for c in sorted(res['configs']): + print(c) + +@cli.command('versions') +@click.argument('config_name') +def config_versions(config_name): + uri = f'http://{host}:{port}/listVersions?name={config_name}' + print(uri) + r = requests.get(uri) + if (r.status_code != 200): + click.Errors("Failed to read the configurations list from db") + + res = r.json() + for v in res['versions']: + print(v) + +@cli.command('dump') +@click.argument('config_name') +@click.option('-v', '--version', type=int, default=None) +@click.option('-w', '--write', is_flag=True, default=False) +@click.option('-o', '--output', default=None) +def config_versions(config_name, version, write, output): + + if not version is None: + uri = f'http://{host}:{port}/retrieveVersion?name={config_name}&version={version}' + else: + uri = f'http://{host}:{port}/retrieveLast?name={config_name}' + + print(uri) + r = requests.get(uri) + if (r.status_code != 200): + click.Errors("Failed to read the configurations list from db") + + res = r.json() + print(res) + + if write: + outname = output if not output is None else f"{config_name}_v{version}.json" + with open(outname, "w") as outfile: + json.dump( + res, + outfile, + sort_keys=True, + indent=4, + ) + +if __name__ == '__main__': + cli() + From 69dd0adb86d731a14c2e6f8e84690fc6cc544993 Mon Sep 17 00:00:00 2001 From: Alessandro Thea Date: Sat, 10 Jun 2023 00:53:08 +0200 Subject: [PATCH 17/90] Improving conf schema --- python/daqconf/core/conf_utils.py | 10 +++++----- schema/daqconf/confgen.jsonnet | 17 +++++++++++++---- scripts/daqconf_multiru_gen | 4 ++-- scripts/dromap_editor | 5 +++++ 4 files changed, 25 insertions(+), 11 deletions(-) diff --git a/python/daqconf/core/conf_utils.py b/python/daqconf/core/conf_utils.py index c321ae70..343b82d7 100644 --- a/python/daqconf/core/conf_utils.py +++ b/python/daqconf/core/conf_utils.py @@ -653,15 +653,15 @@ def generate_boot( if boot_conf.disable_trace: del boot["exec"][daq_app_exec_name]["env"]["TRACE_FILE"] - match boot_conf.RTE_script_settings: - case 0: + match boot_conf.k8s_rte: + case 'auto': if (release_or_dev() == 'rel'): boot['rte_script'] = get_rte_script() - case 1: + case 'release': boot['rte_script'] = get_rte_script() - case 2: + case 'devarea': pass @@ -688,7 +688,7 @@ def generate_boot( boot_data = boot, apps = system.apps, boot_order = boot_order, - image = boot_conf.image, + image = boot_conf.k8s_image, base_command_port = boot_conf.base_command_port, verbose = verbose, control_to_data_network = control_to_data_network, diff --git a/schema/daqconf/confgen.jsonnet b/schema/daqconf/confgen.jsonnet index 77ff7b55..d21119dc 100755 --- a/schema/daqconf/confgen.jsonnet +++ b/schema/daqconf/confgen.jsonnet @@ -32,7 +32,8 @@ local cs = { tc_intervals: s.sequence( "TCIntervals", self.tc_interval, doc="List of TC intervals used by CTCM"), readout_time: s.number( "ROTime", "i8", doc="A readout time in ticks"), channel_list: s.sequence( "ChannelList", self.count, doc="List of offline channels to be masked out from the TPHandler"), - pm_kind: s.enum( "PMKind", ["k8s", "ssh"]), + pm_choice: s.enum( "PMChoice", ["k8s", "ssh"], doc="Process Manager choice: ssh or Kubernetes"), + rte_choice: s.enum( "RTEChoice", ["auto", "release", "devarea"], doc="Kubernetes DAQ application RTC choice"), @@ -41,16 +42,16 @@ local cs = { s.field( "base_command_port", self.port, default=3333, doc="Base port of application command endpoints"), # Obscure - s.field( "RTE_script_settings", self.three_choice, default=0, doc="0 - Use an RTE script iff not in a dev environment, 1 - Always use RTE, 2 - never use RTE"), s.field( "capture_env_vars", self.strings, default=['TIMING_SHARE', 'DETCHANNELMAPS_SHARE'], doc="List of variables to capture from the environment"), s.field( "disable_trace", self.flag, false, doc="Do not enable TRACE (default TRACE_FILE is /tmp/trace_buffer_${HOSTNAME}_${USER})"), s.field( "opmon_impl", self.monitoring_dest, default='local', doc="Info collector service implementation to use"), s.field( "ers_impl", self.monitoring_dest, default='local', doc="ERS destination (Kafka used for cern and pocket)"), s.field( "pocket_url", self.host, default='127.0.0.1', doc="URL for connecting to Pocket services"), - s.field( "process_manager", self.pm_kind, default="ssh", doc="Choice of process manager"), + s.field( "process_manager", self.pm_choice, default="ssh", doc="Choice of process manager"), # K8S - s.field( "image", self.string, default="dunedaq/c8-minimal", doc="Which docker image to use"), + s.field( "k8s_image", self.string, default="dunedaq/c8-minimal", doc="Which docker image to use"), + s.field( "k8s_rte", self.rte_choice, default="auto", doc="0 - Use an RTE script if not in a dev environment, 1 - Always use RTE, 2 - never use RTE"), # Connectivity Service s.field( "use_connectivity_service", self.flag, default=true, doc="Whether to use the ConnectivityService to manage connections"), @@ -66,7 +67,15 @@ local cs = { ]), + daq : s.record("daq", [ + s.field( "data_request_timeout_ms", self.count, default=1000, doc="The baseline data request timeout that will be used by modules in the Readout and Trigger subsystems (i.e. any module that produces data fragments). Downstream timeouts, such as the trigger-record-building timeout, are derived from this."), + s.field( "use_data_network", self.flag, default = false, doc="Whether to use the data network (Won't work with k8s)"), + ], doc="Cmmon daq settings"), + detector : s.record("detector", [ + s.field( "clock_speed_hz", self.freq, default=62500000), + s.field( "tpg_channel_map", self.tpg_channel_map, default="ProtoDUNESP1ChannelMap", doc="Channel map for TPG"), + ], doc="Global common settings"), timing: s.record("timing", [ s.field( "timing_session_name", self.string, default="", doc="Name of the global timing session to use, for timing commands"), diff --git a/scripts/daqconf_multiru_gen b/scripts/daqconf_multiru_gen index bb815052..316d2d80 100755 --- a/scripts/daqconf_multiru_gen +++ b/scripts/daqconf_multiru_gen @@ -286,8 +286,8 @@ def cli( if hsi.control_hsi_hw and not hsi.use_timing_hsi: raise Exception("Timing HSI hardware control can only be enabled if timing HSI hardware is used!") - if use_k8s and not boot.image: - raise Exception("You need to provide an --image if running with k8s") + if use_k8s and not boot.k8s_image: + raise Exception("You need to define k8s_image if running with k8s") # host_id_dict = {} # ru_configs = [] diff --git a/scripts/dromap_editor b/scripts/dromap_editor index 4ecb81ba..544d8fa9 100755 --- a/scripts/dromap_editor +++ b/scripts/dromap_editor @@ -156,6 +156,11 @@ def add_eth(obj, force, src_id, **kwargs): print(m.as_table()) + +@cli.command("add-wib-crate") +def add_wib_crate(): + pass + @cli.command("save", help="Save the map to json file") @click.argument('path', type=click.Path()) @click.pass_obj From a1d8aa3d31f7dd6d72239c462420339e904f30fa Mon Sep 17 00:00:00 2001 From: Jonathan Hancock Date: Tue, 13 Jun 2023 13:38:00 +0200 Subject: [PATCH 18/90] Made basic skeleton of the application, committing for portability. --- scripts/daqconf_viewer | 73 ++++++++++++++++++++++++++++++++++++++ scripts/daqconf_viewer.css | 31 ++++++++++++++++ 2 files changed, 104 insertions(+) create mode 100644 scripts/daqconf_viewer create mode 100644 scripts/daqconf_viewer.css diff --git a/scripts/daqconf_viewer b/scripts/daqconf_viewer new file mode 100644 index 00000000..1877ddd5 --- /dev/null +++ b/scripts/daqconf_viewer @@ -0,0 +1,73 @@ +import asyncio +import httpx + +from textual import log, events +from textual.app import App, ComposeResult +from textual.containers import Horizontal, Content, Container, Vertical +from textual.widget import Widget +from textual.widgets import Button, Header, Footer, Static, Input, Label, ListView, ListItem +from textual.reactive import reactive, Reactive +from textual.message import Message, MessageTarget +from textual.screen import Screen + +class ConfDisplay(Static): pass + +class Configs(Static): + def __init__(self, hostname, **kwargs): + super().__init__(**kwargs) + self.hostname = hostname + self.conf_list = [] + + def on_mount(self) -> None: + self.set_interval(0.1, self.update_configs) + + def compose(self) -> ComposeResult: + yield Vertical ( + ListView( + ListItem(Static("hello")), + ListItem(Static("hello2")), + ListItem(Static("hello3")) + ), + id="verticalconf" + ) + + async def update_configs(self) -> None: + pass + #async with httpx.AsyncClient() as client: + #r = await client.post(f'{self.hostname}/nanorcrest/command', auth=auth, data=payload, timeout=60) + +class Versions(Static): + def __init__(self, hostname, **kwargs): + super().__init__(**kwargs) + self.hostname = hostname + self.version_list = [] + +class Display(Static): + def __init__(self, hostname, **kwargs): + super().__init__(**kwargs) + self.hostname = hostname + + +class ConfViewer(App): + CSS_PATH = "daqconf_viewer.css" + #BINDINGS = [("d", "toggle_dark", "Toggle dark mode")] + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.hostname = "http://np04-srv-023:31011/ListConfigs" + + def compose(self) -> ComposeResult: + """Create child widgets for the app.""" + yield Container( + Configs(hostname=self.hostname, classes='container', id='configs'), + Versions(hostname=self.hostname, classes='container', id='versions'), + Display(hostname=self.hostname, classes='container', id='display'), + id = 'app-grid' + ) + + yield Header(show_clock=True) + yield Footer() + +if __name__ == "__main__": + app = ConfViewer() + app.run() diff --git a/scripts/daqconf_viewer.css b/scripts/daqconf_viewer.css new file mode 100644 index 00000000..8d63d009 --- /dev/null +++ b/scripts/daqconf_viewer.css @@ -0,0 +1,31 @@ +#app-grid { + layout: grid; + grid-size: 4 10; + grid-columns: 1fr; + grid-rows: 1fr; + grid-gutter: 1; +} + +#configs { + column-span: 1; + row-span: 10; +} + +#versions { + column-span: 3; + row-span: 2; +} + +#display { + column-span: 3; + row-span: 8; +} + +#verticalconf { + height: 20; +} + +.container{ + margin: 1; + border: wide red; +} From e589cf430a45cae887107f4840b06d25825a667f Mon Sep 17 00:00:00 2001 From: Michal Rigan Date: Tue, 13 Jun 2023 09:21:36 -0500 Subject: [PATCH 19/90] added a way to parse triger bit word names to flags --- python/daqconf/apps/trigger_gen.py | 29 ++++++++++++++++++++++++++++- schema/daqconf/confgen.jsonnet | 4 ++++ scripts/daqconf_multiru_gen | 7 +++++-- 3 files changed, 37 insertions(+), 3 deletions(-) diff --git a/python/daqconf/apps/trigger_gen.py b/python/daqconf/apps/trigger_gen.py index d836c1a6..4822a110 100644 --- a/python/daqconf/apps/trigger_gen.py +++ b/python/daqconf/apps/trigger_gen.py @@ -16,6 +16,7 @@ moo.otypes.load_types('trigger/txbuffer.jsonnet') moo.otypes.load_types('readoutlibs/readoutconfig.jsonnet') moo.otypes.load_types('trigger/tpchannelfilter.jsonnet') +moo.otypes.load_types('trigger/triggerbitwords.jsonnet') # Import new types import dunedaq.trigger.triggeractivitymaker as tam @@ -28,6 +29,7 @@ import dunedaq.trigger.txbufferconfig as bufferconf import dunedaq.readoutlibs.readoutconfig as readoutconf import dunedaq.trigger.tpchannelfilter as chfilter +import dunedaq.trigger.triggerbitwords as tbw from daqconf.core.app import App, ModuleGraph from daqconf.core.daqmodule import DAQModule @@ -70,6 +72,25 @@ def get_buffer_conf(source_id, data_request_timeout): request_timeout_ms = data_request_timeout, warn_on_timeout = False, enable_raw_recording = False)) + +#=============================================================================== +def get_trigger_bitwords(bitwords, bitwords_map): + # process map + map_bits = [] + for item in bitwords_map.items(): + tmp_pair = (item[1]['tc_type'], item[1]['tc_name']) + map_bits.append(tmp_pair) + # create bitwords flags + final_bit_flags = [] + for bitword in bitwords: + tmp_bit = [] + for bit_name in bitword: + for map_bit in map_bits: + if bit_name == map_bit[1]: + tmp_bit.append(map_bit[0]) + break + final_bit_flags.append(tmp_bit) + return final_bit_flags #=============================================================================== def get_trigger_app(CLOCK_SPEED_HZ: int = 62_500_000, @@ -104,6 +125,7 @@ def get_trigger_app(CLOCK_SPEED_HZ: int = 62_500_000, MLT_READOUT_MAP: dict = {}, MLT_USE_BITWORDS: bool = False, MLT_TRIGGER_BITWORDS: dict = {}, + MLT_TRIGGER_BITWORDS_MAP: dict = {}, USE_CHANNEL_FILTER: bool = True, @@ -111,7 +133,12 @@ def get_trigger_app(CLOCK_SPEED_HZ: int = 62_500_000, DATA_REQUEST_TIMEOUT = 1000, HOST="localhost", DEBUG=False): - + + print("LE HERE") + MLT_TRIGGER_FLAGS = get_trigger_bitwords(MLT_TRIGGER_BITWORDS, MLT_TRIGGER_BITWORDS_MAP) + print(MLT_TRIGGER_BITWORDS) + print(MLT_TRIGGER_FLAGS) + # Generate schema for the maker plugins on the fly in the temptypes module make_moo_record(ACTIVITY_CONFIG , 'ActivityConf' , 'temptypes') make_moo_record(CANDIDATE_CONFIG, 'CandidateConf', 'temptypes') diff --git a/schema/daqconf/confgen.jsonnet b/schema/daqconf/confgen.jsonnet index b9532d69..07884405 100755 --- a/schema/daqconf/confgen.jsonnet +++ b/schema/daqconf/confgen.jsonnet @@ -6,6 +6,9 @@ local moo = import "moo.jsonnet"; local sctb = import "ctbmodules/ctbmodule.jsonnet"; local ctbmodule = moo.oschema.hier(sctb).dunedaq.ctbmodules.ctbmodule; +local tbw = import "trigger/triggerbitwords.jsonnet"; +local tbw_data = moo.oschema.hier(tbw).dunedaq.trigger.triggerbitwords; + local s = moo.oschema.schema("dunedaq.daqconf.confgen"); local nc = moo.oschema.numeric_constraints; // A temporary schema construction context. @@ -264,6 +267,7 @@ local cs = { s.field( "mlt_td_readout_map", self.tc_readout_map, default=self.tc_readout_map, doc="The readout windows assigned to TDs in MLT, based on TC type."), s.field( "mlt_use_bitwords", self.flag, default=false, doc="Option to use bitwords (ie trigger types, coincidences) when forming trigger decisions in MLT" ), s.field( "mlt_trigger_bitwords", self.bitwords, default=[], doc="Optional dictionary of bitwords to use when forming trigger decisions in MLT" ), + s.field( "mlt_trigger_bitwords_map", tbw_data.bitwords_map, [], doc="12345"), s.field( "use_custom_maker", self.flag, default=false, doc="Option to use a Custom Trigger Candidate Maker (plugin)"), s.field( "ctcm_trigger_types", self.tc_types, default=[4], doc="Optional list of TC types to be used by the Custom Trigger Candidate Maker (plugin)"), s.field( "ctcm_trigger_intervals", self.tc_intervals, default=[10000000], doc="Optional list of intervals (clock ticks) for the TC types to be used by the Custom Trigger Candidate Maker (plugin)"), diff --git a/scripts/daqconf_multiru_gen b/scripts/daqconf_multiru_gen index d66f4a2e..22409525 100755 --- a/scripts/daqconf_multiru_gen +++ b/scripts/daqconf_multiru_gen @@ -29,6 +29,9 @@ import moo.otypes moo.otypes.load_types('detchannelmaps/hardwaremapservice.jsonnet') import dunedaq.detchannelmaps.hardwaremapservice as hwms +moo.otypes.load_types('trigger/triggerbitwords.jsonnet') +import dunedaq.trigger.triggerbitwords as tbw + # Add -h as default help option CONTEXT_SETTINGS = dict(help_option_names=['-h', '--help']) @click.command(context_settings=CONTEXT_SETTINGS) @@ -95,7 +98,7 @@ def cli(config, base_command_port, detector_readout_map_file, data_rate_slowdown dpdk_sender = confgen.dpdk_sender(**config_data.dpdk_sender) if debug: console.log(f"dpdk_sender configuration object: {dpdk_sender.pod()}") - + # Update with command-line options if base_command_port != -1: boot.base_command_port = base_command_port @@ -404,7 +407,7 @@ def cli(config, base_command_port, detector_readout_map_file, data_rate_slowdown MLT_READOUT_MAP = trigger.mlt_td_readout_map, MLT_USE_BITWORDS = trigger.mlt_use_bitwords, MLT_TRIGGER_BITWORDS = trigger.mlt_trigger_bitwords, - + MLT_TRIGGER_BITWORDS_MAP = trigger.mlt_trigger_bitwords_map, USE_CUSTOM_MAKER = trigger.use_custom_maker, CTCM_TYPES = trigger.ctcm_trigger_types, CTCM_INTERVAL = trigger.ctcm_trigger_intervals, From 246d6d2a0f6928272dd524e355e111d77b90b21d Mon Sep 17 00:00:00 2001 From: Michal Rigan Date: Tue, 13 Jun 2023 11:55:28 -0500 Subject: [PATCH 20/90] chanding the way bitwords are passed to trigger:mlt, additional check for provided triggerwords --- python/daqconf/apps/trigger_gen.py | 9 ++++++++- schema/daqconf/confgen.jsonnet | 2 +- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/python/daqconf/apps/trigger_gen.py b/python/daqconf/apps/trigger_gen.py index 4822a110..3e74544d 100644 --- a/python/daqconf/apps/trigger_gen.py +++ b/python/daqconf/apps/trigger_gen.py @@ -75,6 +75,8 @@ def get_buffer_conf(source_id, data_request_timeout): #=============================================================================== def get_trigger_bitwords(bitwords, bitwords_map): + count_bitwords = 0 + count_flags = 0 # process map map_bits = [] for item in bitwords_map.items(): @@ -85,11 +87,16 @@ def get_trigger_bitwords(bitwords, bitwords_map): for bitword in bitwords: tmp_bit = [] for bit_name in bitword: + count_bitwords += 1 for map_bit in map_bits: if bit_name == map_bit[1]: tmp_bit.append(map_bit[0]) + count_flags +=1 break final_bit_flags.append(tmp_bit) + if (count_bitwords != count_flags): + raise RuntimeError(f'One or more of provided MLT trigger bitwords is incorrect! Please recheck the names...') + return final_bit_flags #=============================================================================== @@ -337,7 +344,7 @@ def get_trigger_app(CLOCK_SPEED_HZ: int = 62_500_000, use_readout_map=MLT_USE_READOUT_MAP, td_readout_map=MLT_READOUT_MAP, use_bitwords=MLT_USE_BITWORDS, - trigger_bitwords=MLT_TRIGGER_BITWORDS))] + trigger_bitwords=MLT_TRIGGER_FLAGS))] mgraph = ModuleGraph(modules) diff --git a/schema/daqconf/confgen.jsonnet b/schema/daqconf/confgen.jsonnet index 07884405..2e88ab66 100755 --- a/schema/daqconf/confgen.jsonnet +++ b/schema/daqconf/confgen.jsonnet @@ -34,7 +34,7 @@ local cs = { tc_intervals: s.sequence( "TCIntervals", self.tc_interval, doc="List of TC intervals used by CTCM"), readout_time: s.number( "ROTime", "i8", doc="A readout time in ticks"), channel_list: s.sequence( "ChannelList", self.count, doc="List of offline channels to be masked out from the TPHandler"), - bitword: s.string( "Bitword", doc="123"), + bitword: s.string( "Bitword", doc="123"), bitword_list: s.sequence( "BitwordList", self.bitword, doc="123"), bitwords: s.sequence( "Bitwords", self.bitword_list, doc="List of bitwords to use when forming trigger decisions in MLT" ), From adc43284c9f92c1d622e1db67de8563dc775e442 Mon Sep 17 00:00:00 2001 From: Jonathan Hancock Date: Thu, 15 Jun 2023 16:44:52 +0200 Subject: [PATCH 21/90] First working version of the app, more improvements to be made. --- scripts/daqconf_viewer | 117 +++++++++++++++++++++++++++++-------- scripts/daqconf_viewer.css | 26 ++++++--- 2 files changed, 111 insertions(+), 32 deletions(-) diff --git a/scripts/daqconf_viewer b/scripts/daqconf_viewer index 1877ddd5..4ef514b0 100644 --- a/scripts/daqconf_viewer +++ b/scripts/daqconf_viewer @@ -1,5 +1,6 @@ import asyncio import httpx +import json from textual import log, events from textual.app import App, ComposeResult @@ -10,42 +11,111 @@ from textual.reactive import reactive, Reactive from textual.message import Message, MessageTarget from textual.screen import Screen -class ConfDisplay(Static): pass +auth = ("fooUsr", "barPass") + +class LabelItem(ListItem): + def __init__(self, label: str) -> None: + super().__init__() + self.label = label + + def compose( self ) -> ComposeResult: + yield Label(self.label) class Configs(Static): + conflist = reactive([]) + def __init__(self, hostname, **kwargs): super().__init__(**kwargs) self.hostname = hostname - self.conf_list = [] def on_mount(self) -> None: self.set_interval(0.1, self.update_configs) def compose(self) -> ComposeResult: - yield Vertical ( - ListView( - ListItem(Static("hello")), - ListItem(Static("hello2")), - ListItem(Static("hello3")) - ), - id="verticalconf" - ) - + yield ListView(LabelItem("asfas")) + async def update_configs(self) -> None: - pass - #async with httpx.AsyncClient() as client: - #r = await client.post(f'{self.hostname}/nanorcrest/command', auth=auth, data=payload, timeout=60) - -class Versions(Static): + async with httpx.AsyncClient() as client: + r = await client.get(f'{self.hostname}/listConfigs', auth=auth, timeout=60) + self.conflist = r.json()['configs'] + + def watch_conflist(self, conflist:list[str]): + label_list = [LabelItem(c) for c in conflist] + the_list = self.query_one(ListView) + the_list.clear() + for item in label_list: + the_list.append(item) + + def on_list_view_selected(self, event: ListView.Selected): + '''The query gets all children of the app, then we choose Versions.''' + confname = event.item.label + versions = self.app.query_one(Horizontal) + versions.new_conf(confname) + +class Versions(Horizontal): + vlist = reactive([]) + def __init__(self, hostname, **kwargs): super().__init__(**kwargs) self.hostname = hostname - self.version_list = [] + self.current_conf = None + + def on_mount(self) -> None: + self.set_interval(0.1, self.update_versions) + + def new_conf(self, conf) -> None: + self.current_conf = conf + + async def update_versions(self) -> None: + if self.current_conf: + async with httpx.AsyncClient() as client: + payload = {'name': self.current_conf} + r = await client.get(f'{self.hostname}/listVersions', auth=auth, params=payload, timeout=60) + self.vlist = r.json()['versions'] #This is a list of ints + + def watch_vlist(self, vlist:list[int]) -> None: + old_buttons = self.query(Button) + for b in old_buttons: + b.remove() + for v in vlist: + b_id = 'v' + str(v) #An id can't be just a number for some reason + self.mount(Button(str(v), id=b_id, classes='vbuttons', variant='primary')) + + #TODO make buttons smaller, and force them to be in the centre (current position is a coincidence due to the margin) + async def on_button_pressed (self, event: Button.Pressed) -> None: + button_id = event.button.id + version = int(button_id[1:]) + for v in self.app.query(Vertical): + if isinstance(v, Display): + await v.get_json(self.current_conf, version) + break + +class Display(Vertical): + confdata = reactive(None) -class Display(Static): def __init__(self, hostname, **kwargs): super().__init__(**kwargs) self.hostname = hostname + self.confname = None + self.version = None + + def compose(self) -> ComposeResult: + yield Static() + + async def get_json(self, conf, ver) -> None: + self.confname = conf + self.version = ver + if self.confname and self.version: + async with httpx.AsyncClient() as client: + payload = {'name': self.confname, 'version': self.version} + r = await client.get(f'{self.hostname}/retrieveVersion', auth=auth, params=payload, timeout=60) + self.confdata = r.json() + + #TODO the output could possibly be made nice with rich.print_json. It should also be able to collapse sections. + def watch_confdata(self, confdata:dict) -> None: + box = self.query_one(Static) + json_str = json.dumps(confdata, indent=2) + box.update(json_str) class ConfViewer(App): @@ -54,16 +124,13 @@ class ConfViewer(App): def __init__(self, **kwargs): super().__init__(**kwargs) - self.hostname = "http://np04-srv-023:31011/ListConfigs" + self.hostname = "http://np04-srv-023:31011" def compose(self) -> ComposeResult: """Create child widgets for the app.""" - yield Container( - Configs(hostname=self.hostname, classes='container', id='configs'), - Versions(hostname=self.hostname, classes='container', id='versions'), - Display(hostname=self.hostname, classes='container', id='display'), - id = 'app-grid' - ) + yield Configs(hostname=self.hostname, classes='container', id='configs') + yield Versions(hostname=self.hostname, classes='container', id='versions') + yield Display(hostname=self.hostname, classes='container', id='display') yield Header(show_clock=True) yield Footer() diff --git a/scripts/daqconf_viewer.css b/scripts/daqconf_viewer.css index 8d63d009..63642cb4 100644 --- a/scripts/daqconf_viewer.css +++ b/scripts/daqconf_viewer.css @@ -1,24 +1,29 @@ -#app-grid { +/* 4 columns 10 rows */ +Screen { layout: grid; grid-size: 4 10; - grid-columns: 1fr; - grid-rows: 1fr; grid-gutter: 1; } #configs { - column-span: 1; row-span: 10; + column-span: 1; + height: 100%; } #versions { - column-span: 3; row-span: 2; + column-span: 3; + height: 100%; + background: green; } #display { - column-span: 3; row-span: 8; + column-span: 3; + height: 100%; + background: purple; + overflow-y: auto; } #verticalconf { @@ -26,6 +31,13 @@ } .container{ - margin: 1; border: wide red; } + +.vbuttons { + align-vertical: middle; + width: 5%; + margin: 1; +} + + From d7d76e4b7ee7922d1fb96c33bcfcfd7ef6f8e814 Mon Sep 17 00:00:00 2001 From: Michal Rigan Date: Fri, 16 Jun 2023 14:58:56 -0500 Subject: [PATCH 22/90] moving trigger bitwords configuration from schema to pybinds --- python/daqconf/apps/trigger_gen.py | 27 +++++++++++---------------- schema/daqconf/confgen.jsonnet | 4 ---- scripts/daqconf_multiru_gen | 4 ---- 3 files changed, 11 insertions(+), 24 deletions(-) diff --git a/python/daqconf/apps/trigger_gen.py b/python/daqconf/apps/trigger_gen.py index 3e74544d..ceb6d61b 100644 --- a/python/daqconf/apps/trigger_gen.py +++ b/python/daqconf/apps/trigger_gen.py @@ -16,7 +16,6 @@ moo.otypes.load_types('trigger/txbuffer.jsonnet') moo.otypes.load_types('readoutlibs/readoutconfig.jsonnet') moo.otypes.load_types('trigger/tpchannelfilter.jsonnet') -moo.otypes.load_types('trigger/triggerbitwords.jsonnet') # Import new types import dunedaq.trigger.triggeractivitymaker as tam @@ -29,13 +28,14 @@ import dunedaq.trigger.txbufferconfig as bufferconf import dunedaq.readoutlibs.readoutconfig as readoutconf import dunedaq.trigger.tpchannelfilter as chfilter -import dunedaq.trigger.triggerbitwords as tbw from daqconf.core.app import App, ModuleGraph from daqconf.core.daqmodule import DAQModule from daqconf.core.conf_utils import Direction, Queue from daqconf.core.sourceid import TAInfo, TPInfo, TCInfo +from trgdataformats._daq_trgdataformats_py import TriggerBits as trgbs + #FIXME maybe one day, triggeralgs will define schemas... for now allow a dictionary of 4byte int, 4byte floats, and strings moo.otypes.make_type(schema='number', dtype='i4', name='temp_integer', path='temptypes') moo.otypes.make_type(schema='number', dtype='f4', name='temp_float', path='temptypes') @@ -74,14 +74,11 @@ def get_buffer_conf(source_id, data_request_timeout): enable_raw_recording = False)) #=============================================================================== -def get_trigger_bitwords(bitwords, bitwords_map): +def get_trigger_bitwords(bitwords): count_bitwords = 0 count_flags = 0 # process map - map_bits = [] - for item in bitwords_map.items(): - tmp_pair = (item[1]['tc_type'], item[1]['tc_name']) - map_bits.append(tmp_pair) + map_bits = trgbs.get_trigger_candidate_type_names() # create bitwords flags final_bit_flags = [] for bitword in bitwords: @@ -89,8 +86,8 @@ def get_trigger_bitwords(bitwords, bitwords_map): for bit_name in bitword: count_bitwords += 1 for map_bit in map_bits: - if bit_name == map_bit[1]: - tmp_bit.append(map_bit[0]) + if bit_name == map_bit.name: + tmp_bit.append(map_bit.value) count_flags +=1 break final_bit_flags.append(tmp_bit) @@ -132,7 +129,6 @@ def get_trigger_app(CLOCK_SPEED_HZ: int = 62_500_000, MLT_READOUT_MAP: dict = {}, MLT_USE_BITWORDS: bool = False, MLT_TRIGGER_BITWORDS: dict = {}, - MLT_TRIGGER_BITWORDS_MAP: dict = {}, USE_CHANNEL_FILTER: bool = True, @@ -140,12 +136,7 @@ def get_trigger_app(CLOCK_SPEED_HZ: int = 62_500_000, DATA_REQUEST_TIMEOUT = 1000, HOST="localhost", DEBUG=False): - - print("LE HERE") - MLT_TRIGGER_FLAGS = get_trigger_bitwords(MLT_TRIGGER_BITWORDS, MLT_TRIGGER_BITWORDS_MAP) - print(MLT_TRIGGER_BITWORDS) - print(MLT_TRIGGER_FLAGS) - + # Generate schema for the maker plugins on the fly in the temptypes module make_moo_record(ACTIVITY_CONFIG , 'ActivityConf' , 'temptypes') make_moo_record(CANDIDATE_CONFIG, 'CandidateConf', 'temptypes') @@ -324,6 +315,10 @@ def get_trigger_app(CLOCK_SPEED_HZ: int = 62_500_000, trigger_intervals=CTCM_INTERVAL, clock_frequency_hz=CLOCK_SPEED_HZ, timestamp_method="kSystemClock"))] + + ### get trigger bitwords for mlt + MLT_TRIGGER_FLAGS = get_trigger_bitwords(MLT_TRIGGER_BITWORDS) + print(MLT_TRIGGER_FLAGS) # We need to populate the list of links based on the fragment # producers available in the system. This is a bit of a diff --git a/schema/daqconf/confgen.jsonnet b/schema/daqconf/confgen.jsonnet index 2e88ab66..6915ca87 100755 --- a/schema/daqconf/confgen.jsonnet +++ b/schema/daqconf/confgen.jsonnet @@ -6,9 +6,6 @@ local moo = import "moo.jsonnet"; local sctb = import "ctbmodules/ctbmodule.jsonnet"; local ctbmodule = moo.oschema.hier(sctb).dunedaq.ctbmodules.ctbmodule; -local tbw = import "trigger/triggerbitwords.jsonnet"; -local tbw_data = moo.oschema.hier(tbw).dunedaq.trigger.triggerbitwords; - local s = moo.oschema.schema("dunedaq.daqconf.confgen"); local nc = moo.oschema.numeric_constraints; // A temporary schema construction context. @@ -267,7 +264,6 @@ local cs = { s.field( "mlt_td_readout_map", self.tc_readout_map, default=self.tc_readout_map, doc="The readout windows assigned to TDs in MLT, based on TC type."), s.field( "mlt_use_bitwords", self.flag, default=false, doc="Option to use bitwords (ie trigger types, coincidences) when forming trigger decisions in MLT" ), s.field( "mlt_trigger_bitwords", self.bitwords, default=[], doc="Optional dictionary of bitwords to use when forming trigger decisions in MLT" ), - s.field( "mlt_trigger_bitwords_map", tbw_data.bitwords_map, [], doc="12345"), s.field( "use_custom_maker", self.flag, default=false, doc="Option to use a Custom Trigger Candidate Maker (plugin)"), s.field( "ctcm_trigger_types", self.tc_types, default=[4], doc="Optional list of TC types to be used by the Custom Trigger Candidate Maker (plugin)"), s.field( "ctcm_trigger_intervals", self.tc_intervals, default=[10000000], doc="Optional list of intervals (clock ticks) for the TC types to be used by the Custom Trigger Candidate Maker (plugin)"), diff --git a/scripts/daqconf_multiru_gen b/scripts/daqconf_multiru_gen index 22409525..09989162 100755 --- a/scripts/daqconf_multiru_gen +++ b/scripts/daqconf_multiru_gen @@ -29,9 +29,6 @@ import moo.otypes moo.otypes.load_types('detchannelmaps/hardwaremapservice.jsonnet') import dunedaq.detchannelmaps.hardwaremapservice as hwms -moo.otypes.load_types('trigger/triggerbitwords.jsonnet') -import dunedaq.trigger.triggerbitwords as tbw - # Add -h as default help option CONTEXT_SETTINGS = dict(help_option_names=['-h', '--help']) @click.command(context_settings=CONTEXT_SETTINGS) @@ -407,7 +404,6 @@ def cli(config, base_command_port, detector_readout_map_file, data_rate_slowdown MLT_READOUT_MAP = trigger.mlt_td_readout_map, MLT_USE_BITWORDS = trigger.mlt_use_bitwords, MLT_TRIGGER_BITWORDS = trigger.mlt_trigger_bitwords, - MLT_TRIGGER_BITWORDS_MAP = trigger.mlt_trigger_bitwords_map, USE_CUSTOM_MAKER = trigger.use_custom_maker, CTCM_TYPES = trigger.ctcm_trigger_types, CTCM_INTERVAL = trigger.ctcm_trigger_intervals, From 3e8cbc8a95c7f636f3001a6a5cee36a3a8d50250 Mon Sep 17 00:00:00 2001 From: Alessandro Thea Date: Fri, 16 Jun 2023 22:38:07 +0200 Subject: [PATCH 23/90] Reorganising generator parameters --- python/daqconf/apps/fake_hsi_gen.py | 51 ++++-- python/daqconf/apps/hsi_gen.py | 85 +++++++--- python/daqconf/apps/readout_gen.py | 31 ++-- python/daqconf/apps/tprtc_gen.py | 29 +++- python/daqconf/apps/tpwriter_gen.py | 1 + python/daqconf/apps/trigger_gen.py | 113 ++++++++----- python/daqconf/core/conf_utils.py | 4 +- schema/daqconf/confgen.jsonnet | 82 +++++---- scripts/daqconf_multiru_gen | 247 +++++++++++++++------------- scripts/dromap_editor | 93 ++++++++++- 10 files changed, 479 insertions(+), 257 deletions(-) diff --git a/python/daqconf/apps/fake_hsi_gen.py b/python/daqconf/apps/fake_hsi_gen.py index 93f889c0..c01b67e3 100644 --- a/python/daqconf/apps/fake_hsi_gen.py +++ b/python/daqconf/apps/fake_hsi_gen.py @@ -42,28 +42,46 @@ import math #=============================================================================== -def get_fake_hsi_app(RUN_NUMBER=333, - CLOCK_SPEED_HZ: int=62500000, - DATA_RATE_SLOWDOWN_FACTOR: int=1, - TRIGGER_RATE_HZ: int=1, - HSI_SOURCE_ID: int=0, - MEAN_SIGNAL_MULTIPLICITY: int=0, - SIGNAL_EMULATION_MODE: int=0, - ENABLED_SIGNALS: int=0b00000001, +def get_fake_hsi_app( + detector, + hsi, + daq_common, + source_id, + # TRIGGER_RATE_HZ: int=1, + + # CLOCK_SPEED_HZ: int=62500000, + # DATA_RATE_SLOWDOWN_FACTOR: int=1, + # TRIGGER_RATE_HZ: int=1, + # HSI_SOURCE_ID: int=0, + # MEAN_SIGNAL_MULTIPLICITY: int=0, + # SIGNAL_EMULATION_MODE: int=0, + # ENABLED_SIGNALS: int=0b00000001, QUEUE_POP_WAIT_MS=10, LATENCY_BUFFER_SIZE=100000, DATA_REQUEST_TIMEOUT=1000, - HOST="localhost", + # HOST="localhost", DEBUG=False): - region_id=0 - element_id=0 + + CLOCK_SPEED_HZ = detector.clock_speed_hz + DATA_RATE_SLOWDOWN_FACTOR = daq_common.data_rate_slowdown_factor + # TRIGGER_RATE_HZ = trigger.trigger_rate_hz + HSI_SOURCE_ID=source_id + MEAN_SIGNAL_MULTIPLICITY = hsi.mean_hsi_signal_multiplicity + SIGNAL_EMULATION_MODE = hsi.hsi_signal_emulation_mode + ENABLED_SIGNALS = hsi.enabled_hsi_signals + HOST=hsi.host_fake_hsi + + TRIGGER_RATE_HZ: int=1 + + # region_id=0 + # element_id=0 - trigger_interval_ticks = 0 - if TRIGGER_RATE_HZ > 0: - trigger_interval_ticks = math.floor((1 / TRIGGER_RATE_HZ) * CLOCK_SPEED_HZ / DATA_RATE_SLOWDOWN_FACTOR) + # trigger_interval_ticks = 0 + # if TRIGGER_RATE_HZ > 0: + # trigger_interval_ticks = math.floor((1 / TRIGGER_RATE_HZ) * CLOCK_SPEED_HZ / DATA_RATE_SLOWDOWN_FACTOR) - startpars = rccmd.StartParams(run=RUN_NUMBER, trigger_rate = TRIGGER_RATE_HZ) + # startpars = rccmd.StartParams(run=RUN_NUMBER, trigger_rate = TRIGGER_RATE_HZ) modules = [DAQModule(name = 'fhsig', plugin = "FakeHSIEventGenerator", @@ -72,7 +90,8 @@ def get_fake_hsi_app(RUN_NUMBER=333, mean_signal_multiplicity=MEAN_SIGNAL_MULTIPLICITY, signal_emulation_mode=SIGNAL_EMULATION_MODE, enabled_signals=ENABLED_SIGNALS), - extra_commands = {"start": startpars})] + # extra_commands = {"start": startpars} + )] modules += [DAQModule(name = f"hsi_datahandler", diff --git a/python/daqconf/apps/hsi_gen.py b/python/daqconf/apps/hsi_gen.py index 8f59abef..0b9e9fe1 100644 --- a/python/daqconf/apps/hsi_gen.py +++ b/python/daqconf/apps/hsi_gen.py @@ -36,29 +36,63 @@ from ..core.conf_utils import Direction, Queue #=============================================================================== -def get_timing_hsi_app(RUN_NUMBER = 333, - CLOCK_SPEED_HZ: int = 62500000, - TRIGGER_RATE_HZ: int = 1, - DATA_RATE_SLOWDOWN_FACTOR: int=1, - CONTROL_HSI_HARDWARE = False, - READOUT_PERIOD_US: int = 1e3, - HSI_ENDPOINT_ADDRESS = 1, - HSI_ENDPOINT_PARTITION = 0, - HSI_RE_MASK = 0x20000, - HSI_FE_MASK = 0, - HSI_INV_MASK = 0, - HSI_SOURCE = 1, - HSI_SOURCE_ID = 0, - CONNECTIONS_FILE="${TIMING_SHARE}/config/etc/connections.xml", - HSI_DEVICE_NAME="BOREAS_TLU", +def get_timing_hsi_app( + detector, + hsi, + daq_common, + source_id, + timing_session_name, + # TRIGGER_RATE_HZ: int = 1, + # CLOCK_SPEED_HZ: int = 62500000, + # TRIGGER_RATE_HZ: int = 1, + # DATA_RATE_SLOWDOWN_FACTOR: int=1, + # CONTROL_HSI_HARDWARE = False, + # READOUT_PERIOD_US: int = 1e3, + # HSI_ENDPOINT_ADDRESS = 1, + # HSI_ENDPOINT_PARTITION = 0, + # HSI_RE_MASK = 0x20000, + # HSI_FE_MASK = 0, + # HSI_INV_MASK = 0, + # HSI_SOURCE = 1, + # HSI_SOURCE_ID = 0, + # CONNECTIONS_FILE="${TIMING_SHARE}/config/etc/connections.xml", + # HSI_DEVICE_NAME="BOREAS_TLU", UHAL_LOG_LEVEL="notice", QUEUE_POP_WAIT_MS=10, LATENCY_BUFFER_SIZE=100000, DATA_REQUEST_TIMEOUT=1000, - TIMING_SESSION="", - HARDWARE_STATE_RECOVERY_ENABLED=True, - HOST="localhost", + # TIMING_SESSION="", + # HARDWARE_STATE_RECOVERY_ENABLED=True, + # HOST="localhost", DEBUG=False): + + + + + + CLOCK_SPEED_HZ = detector.clock_speed_hz + # TRIGGER_RATE_HZ = trigger.trigger_rate_hz + DATA_RATE_SLOWDOWN_FACTOR = daq_common.data_rate_slowdown_factor + CONTROL_HSI_HARDWARE=hsi.control_hsi_hw + CONNECTIONS_FILE=hsi.hsi_hw_connections_file + READOUT_PERIOD_US = hsi.hsi_readout_period + HSI_DEVICE_NAME = hsi.hsi_device_name + HARDWARE_STATE_RECOVERY_ENABLED = hsi.enable_hardware_state_recovery + HSI_ENDPOINT_ADDRESS = hsi.hsi_endpoint_address + HSI_ENDPOINT_PARTITION = hsi.hsi_endpoint_partition + HSI_RE_MASK=hsi.hsi_re_mask + HSI_FE_MASK=hsi.hsi_fe_mask + HSI_INV_MASK=hsi.hsi_inv_mask + HSI_SOURCE=hsi.hsi_source + HSI_SOURCE_ID=source_id + TIMING_SESSION=timing_session_name + HOST=hsi.host_timing_hsi + + + # (Useless) constant + TRIGGER_RATE_HZ: int = 1, + + modules = {} ## TODO all the connections... @@ -91,13 +125,13 @@ def get_timing_hsi_app(RUN_NUMBER = 333, enable_raw_recording = False) ))] - trigger_interval_ticks=0 - if TRIGGER_RATE_HZ > 0: - trigger_interval_ticks=math.floor((1/TRIGGER_RATE_HZ) * CLOCK_SPEED_HZ) - elif CONTROL_HSI_HARDWARE: - console.log('WARNING! Emulated trigger rate of 0 will not disable signal emulation in real HSI hardware! To disable emulated HSI triggers, use option: "--hsi-source 0" or mask all signal bits', style="bold red") + # trigger_interval_ticks=0 + # if TRIGGER_RATE_HZ > 0: + # trigger_interval_ticks=math.floor((1/TRIGGER_RATE_HZ) * CLOCK_SPEED_HZ) + # elif CONTROL_HSI_HARDWARE: + # console.log('WARNING! Emulated trigger rate of 0 will not disable signal emulation in real HSI hardware! To disable emulated HSI triggers, use option: "--hsi-source 0" or mask all signal bits', style="bold red") - startpars = rccmd.StartParams(run=RUN_NUMBER, trigger_rate = TRIGGER_RATE_HZ) + # startpars = rccmd.StartParams(run=RUN_NUMBER, trigger_rate = TRIGGER_RATE_HZ) # resumepars = rccmd.ResumeParams(trigger_interval_ticks = trigger_interval_ticks) if CONTROL_HSI_HARDWARE: @@ -115,7 +149,8 @@ def get_timing_hsi_app(RUN_NUMBER = 333, falling_edge_mask=HSI_FE_MASK, invert_edge_mask=HSI_INV_MASK, data_source=HSI_SOURCE), - extra_commands = {"start": startpars}), + # extra_commands = {"start": startpars} + ), ] ) queues = [Queue(f"hsir.output",f"hsi_datahandler.raw_input", "HSIFrame", f'hsi_link_0', 100000)] diff --git a/python/daqconf/apps/readout_gen.py b/python/daqconf/apps/readout_gen.py index aa3c3095..b35b4607 100644 --- a/python/daqconf/apps/readout_gen.py +++ b/python/daqconf/apps/readout_gen.py @@ -261,19 +261,21 @@ def build_conf_by_host(self, eal_arg_list): class ReadoutAppGenerator: """Utility class to generate readout applications""" - def __init__(self, readout_cfg): + def __init__(self, readout_cfg, det_cfg, daq_cfg): - self.config = readout_cfg + self.ro_cfg = readout_cfg + self.det_cfg = det_cfg + self.daq_cfg = daq_cfg excpt = {} - for ex in self.config.numa_config['exceptions']: + for ex in self.ro_cfg.numa_config['exceptions']: excpt[(ex['host'], ex['card'])] = ex self.excpt = excpt def get_numa_cfg(self, RU_DESCRIPTOR): - cfg = self.config + cfg = self.ro_cfg try: ex = self.excpt[(RU_DESCRIPTOR.host_name, RU_DESCRIPTOR.iface)] numa_id = ex['numa_id'] @@ -302,7 +304,7 @@ def create_fake_cardreader( """ Create a FAKE Card reader module """ - cfg = self.config + cfg = self.ro_cfg conf = sec.Conf( link_confs = [ @@ -311,13 +313,13 @@ def create_fake_cardreader( crate_id = s.geo_id.crate_id, slot_id = s.geo_id.slot_id, link_id = s.geo_id.stream_id, - slowdown=cfg.data_rate_slowdown_factor, + slowdown=self.daq_cfg.data_rate_slowdown_factor, queue_name=f"output_{s.src_id}", data_filename = DATA_FILES[s.geo_id.det_id] if s.geo_id.det_id in DATA_FILES.keys() else cfg.default_data_file, emu_frame_error_rate=0 ) for s in RU_DESCRIPTOR.streams], use_now_as_first_data_time=cfg.emulated_data_times_start_with_now, - clock_speed_hz=cfg.clock_speed_hz, + clock_speed_hz=self.det_cfg.clock_speed_hz, queue_timeout_ms = QUEUE_POP_WAIT_MS ) @@ -436,7 +438,7 @@ def create_dpdk_cardreader( [CR]->queues """ - cfg = self.config + cfg = self.ro_cfg eth_ru_bldr = NICReceiverBuilder(RU_DESCRIPTOR) @@ -527,7 +529,7 @@ def create_det_dhl( ) -> tuple[list, list]: - cfg = self.config + cfg = self.ro_cfg # defaults hardcoded values default_latency_buffer_alignment_size = 4096 @@ -590,7 +592,7 @@ def add_tp_processing( TPG_CHANNEL_MAP: str, ) -> list: - cfg = self.config + cfg = self.ro_cfg modules = [] @@ -627,7 +629,7 @@ def add_tp_processing( tpg_channel_mask = cfg.tpg_channel_mask, channel_map_name = TPG_CHANNEL_MAP, emulator_mode = cfg.emulator_mode, - clock_speed_hz = (cfg.clock_speed_hz / cfg.data_rate_slowdown_factor), + clock_speed_hz = (self.det_cfg.clock_speed_hz / self.daq_cfg.data_rate_slowdown_factor), error_counter_threshold=default_error_counter_threshold, error_reset_freq=default_error_reset_freq ), @@ -797,13 +799,12 @@ def generate( numa_id, latency_numa, latency_preallocate, card_override = self.get_numa_cfg(RU_DESCRIPTOR) - cfg = self.config + cfg = self.ro_cfg TPG_ENABLED = cfg.enable_tpg DATA_FILES = data_file_map - # TPG_CHANNEL_MAP = tpg_channel_map, DATA_REQUEST_TIMEOUT=data_timeout_requests - FRONTEND_TYPE, QUEUE_FRAGMENT_TYPE, _, _, _ = compute_data_types(RU_DESCRIPTOR.det_id, cfg.clock_speed_hz, RU_DESCRIPTOR.kind) + FRONTEND_TYPE, QUEUE_FRAGMENT_TYPE, _, _, _ = compute_data_types(RU_DESCRIPTOR.det_id, self.det_cfg.clock_speed_hz, RU_DESCRIPTOR.kind) # TPG is automatically disabled for non wib2 frontends TPG_ENABLED = TPG_ENABLED and (FRONTEND_TYPE=='wib2' or FRONTEND_TYPE=='wibeth') @@ -922,7 +923,7 @@ def generate( c = card_override if card_override != -1 else RU_DESCRIPTOR.iface readout_app.resources = { f"felix.cern/flx{c}-data": "1", # requesting FLX{c} - "memory": "64Gi" # yes bro + # "memory": f"{}Gi" # yes bro } dir_names = set() diff --git a/python/daqconf/apps/tprtc_gen.py b/python/daqconf/apps/tprtc_gen.py index 5e7d06b0..939bc249 100644 --- a/python/daqconf/apps/tprtc_gen.py +++ b/python/daqconf/apps/tprtc_gen.py @@ -30,14 +30,27 @@ from daqconf.core.conf_utils import Direction #=============================================================================== -def get_tprtc_app(MASTER_DEVICE_NAME="", - TIMING_PARTITION_ID=0, - TRIGGER_MASK=0xff, - RATE_CONTROL_ENABLED=True, - SPILL_GATE_ENABLED=False, - TIMING_SESSION="", - HOST="localhost", - DEBUG=False): +def get_tprtc_app( + timing, + # MASTER_DEVICE_NAME="", + # TIMING_PARTITION_ID=0, + # TRIGGER_MASK=0xff, + # RATE_CONTROL_ENABLED=True, + # SPILL_GATE_ENABLED=False, + # TIMING_SESSION="", + # HOST="localhost", + DEBUG=False + ): + + + MASTER_DEVICE_NAME=timing.timing_partition_master_device_name + TIMING_PARTITION_ID=timing.timing_partition_id + TRIGGER_MASK=timing.timing_partition_trigger_mask + RATE_CONTROL_ENABLED=timing.timing_partition_rate_control_enabled + SPILL_GATE_ENABLED=timing.timing_partition_spill_gate_enabled + TIMING_SESSION=timing.timing_session_name + HOST=timing.host_tprtc + modules = {} diff --git a/python/daqconf/apps/tpwriter_gen.py b/python/daqconf/apps/tpwriter_gen.py index 6543269c..0a17509d 100644 --- a/python/daqconf/apps/tpwriter_gen.py +++ b/python/daqconf/apps/tpwriter_gen.py @@ -29,6 +29,7 @@ def get_tpwriter_app( OUTPUT_PATH=".", APP_NAME="tpwriter", OPERATIONAL_ENVIRONMENT="swtest", + FILE_LABEL = "swtest", MAX_FILE_SIZE=4*1024*1024*1024, DATA_RATE_SLOWDOWN_FACTOR=1, CLOCK_SPEED_HZ=62500000, diff --git a/python/daqconf/apps/trigger_gen.py b/python/daqconf/apps/trigger_gen.py index 481dffd2..61b66241 100644 --- a/python/daqconf/apps/trigger_gen.py +++ b/python/daqconf/apps/trigger_gen.py @@ -72,43 +72,82 @@ def get_buffer_conf(source_id, data_request_timeout): enable_raw_recording = False)) #=============================================================================== -def get_trigger_app(CLOCK_SPEED_HZ: int = 62_500_000, - DATA_RATE_SLOWDOWN_FACTOR: float = 1, - TP_CONFIG: dict = {}, - TOLERATE_INCOMPLETENESS=False, - COMPLETENESS_TOLERANCE=1, - - ACTIVITY_PLUGIN: str = 'TriggerActivityMakerPrescalePlugin', - ACTIVITY_CONFIG: dict = dict(prescale=10000), - - CANDIDATE_PLUGIN: str = 'TriggerCandidateMakerPrescalePlugin', - CANDIDATE_CONFIG: int = dict(prescale=10), - - USE_HSI_INPUT = True, - TTCM_S1: int = 1, - TTCM_S2: int = 2, - TRIGGER_WINDOW_BEFORE_TICKS: int = 1000, - TRIGGER_WINDOW_AFTER_TICKS: int = 1000, - HSI_TRIGGER_TYPE_PASSTHROUGH: bool = False, - - USE_CUSTOM_MAKER: bool = False, - CTCM_TYPES: list = [4], - CTCM_INTERVAL: list = [62500000], - - MLT_MERGE_OVERLAPPING_TCS: bool = False, - MLT_BUFFER_TIMEOUT: int = 100, - MLT_SEND_TIMED_OUT_TDS: bool = False, - MLT_MAX_TD_LENGTH_MS: int = 1000, - MLT_IGNORE_TC: list = [], - MLT_USE_READOUT_MAP: bool = False, - MLT_READOUT_MAP: dict = {}, - - USE_CHANNEL_FILTER: bool = True, - - CHANNEL_MAP_NAME = "ProtoDUNESP1ChannelMap", - DATA_REQUEST_TIMEOUT = 1000, - HOST="localhost", - DEBUG=False): +def get_trigger_app( + trigger, + detector, + daq_common, + tp_infos, + trigger_data_request_timeout, + # CLOCK_SPEED_HZ: int = 62_500_000, + # DATA_RATE_SLOWDOWN_FACTOR: float = 1, + # TP_CONFIG: dict = {}, + # TOLERATE_INCOMPLETENESS=False, + # COMPLETENESS_TOLERANCE=1, + + # ACTIVITY_PLUGIN: str = 'TriggerActivityMakerPrescalePlugin', + # ACTIVITY_CONFIG: dict = dict(prescale=10000), + + # CANDIDATE_PLUGIN: str = 'TriggerCandidateMakerPrescalePlugin', + # CANDIDATE_CONFIG: int = dict(prescale=10), + + USE_HSI_INPUT = True, + # TTCM_S1: int = 1, + # TTCM_S2: int = 2, + # TRIGGER_WINDOW_BEFORE_TICKS: int = 1000, + # TRIGGER_WINDOW_AFTER_TICKS: int = 1000, + # HSI_TRIGGER_TYPE_PASSTHROUGH: bool = False, + + # USE_CUSTOM_MAKER: bool = False, + # CTCM_TYPES: list = [4], + # CTCM_INTERVAL: list = [62500000], + + # MLT_MERGE_OVERLAPPING_TCS: bool = False, + # MLT_BUFFER_TIMEOUT: int = 100, + # MLT_SEND_TIMED_OUT_TDS: bool = False, + # MLT_MAX_TD_LENGTH_MS: int = 1000, + # MLT_IGNORE_TC: list = [], + # MLT_USE_READOUT_MAP: bool = False, + # MLT_READOUT_MAP: dict = {}, + + USE_CHANNEL_FILTER: bool = True, + + # CHANNEL_MAP_NAME = "ProtoDUNESP1ChannelMap", + # DATA_REQUEST_TIMEOUT = 1000, + # HOST="localhost", + DEBUG=False + ): + + # To cleanup + DATA_RATE_SLOWDOWN_FACTOR = daq_common.data_rate_slowdown_factor + CLOCK_SPEED_HZ = detector.clock_speed_hz + TP_CONFIG = tp_infos + TOLERATE_INCOMPLETENESS=trigger.tolerate_incompleteness + COMPLETENESS_TOLERANCE=trigger.completeness_tolerance + ACTIVITY_PLUGIN = trigger.trigger_activity_plugin + ACTIVITY_CONFIG = trigger.trigger_activity_config + CANDIDATE_PLUGIN = trigger.trigger_candidate_plugin + CANDIDATE_CONFIG = trigger.trigger_candidate_config + TTCM_S1=trigger.ttcm_s1 + TTCM_S2=trigger.ttcm_s2 + TRIGGER_WINDOW_BEFORE_TICKS = trigger.trigger_window_before_ticks + TRIGGER_WINDOW_AFTER_TICKS = trigger.trigger_window_after_ticks + HSI_TRIGGER_TYPE_PASSTHROUGH = trigger.hsi_trigger_type_passthrough + MLT_MERGE_OVERLAPPING_TCS = trigger.mlt_merge_overlapping_tcs + MLT_BUFFER_TIMEOUT = trigger.mlt_buffer_timeout + MLT_MAX_TD_LENGTH_MS = trigger.mlt_max_td_length_ms + MLT_SEND_TIMED_OUT_TDS = trigger.mlt_send_timed_out_tds + MLT_IGNORE_TC = trigger.mlt_ignore_tc + MLT_USE_READOUT_MAP = trigger.mlt_use_readout_map + MLT_READOUT_MAP = trigger.mlt_td_readout_map + USE_CUSTOM_MAKER = trigger.use_custom_maker + CTCM_TYPES = trigger.ctcm_trigger_types + CTCM_INTERVAL = trigger.ctcm_trigger_intervals + CHANNEL_MAP_NAME = detector.tpg_channel_map + DATA_REQUEST_TIMEOUT=trigger_data_request_timeout + HOST=trigger.host_trigger + + + # Generate schema for the maker plugins on the fly in the temptypes module make_moo_record(ACTIVITY_CONFIG , 'ActivityConf' , 'temptypes') diff --git a/python/daqconf/core/conf_utils.py b/python/daqconf/core/conf_utils.py index 343b82d7..68d449a9 100644 --- a/python/daqconf/core/conf_utils.py +++ b/python/daqconf/core/conf_utils.py @@ -836,8 +836,10 @@ def write_json_files(app_command_datas, system_command_datas, json_dir, verbose= # System commands for cmd, cfg in system_command_datas.items(): - with open(json_dir / f'{cmd}.json', 'w') as f: + data_file = json_dir / f'{cmd}.json' + with open(data_file, 'w') as f: json.dump(cfg, f, indent=4, sort_keys=True) + console.log(f"- {data_file} generated") console.log(f"System configuration generated in directory '{json_dir}'") diff --git a/schema/daqconf/confgen.jsonnet b/schema/daqconf/confgen.jsonnet index d21119dc..205dd8b3 100755 --- a/schema/daqconf/confgen.jsonnet +++ b/schema/daqconf/confgen.jsonnet @@ -32,13 +32,14 @@ local cs = { tc_intervals: s.sequence( "TCIntervals", self.tc_interval, doc="List of TC intervals used by CTCM"), readout_time: s.number( "ROTime", "i8", doc="A readout time in ticks"), channel_list: s.sequence( "ChannelList", self.count, doc="List of offline channels to be masked out from the TPHandler"), + tpg_algo_choice: s.enum( "TPGAlgoChoice", ["SimpleThreshold", "AbsRS"], doc="Trigger algorithm choice"), pm_choice: s.enum( "PMChoice", ["k8s", "ssh"], doc="Process Manager choice: ssh or Kubernetes"), - rte_choice: s.enum( "RTEChoice", ["auto", "release", "devarea"], doc="Kubernetes DAQ application RTC choice"), + rte_choice: s.enum( "RTEChoice", ["auto", "release", "devarea"], doc="Kubernetes DAQ application RTE choice"), boot: s.record("boot", [ - s.field( "op_env", self.string, default='swtest', doc="Operational environment - used for raw data filename prefix and HDF5 Attribute inside the files"), + // s.field( "op_env", self.string, default='swtest', doc="Operational environment - used for raw data filename prefix and HDF5 Attribute inside the files"), s.field( "base_command_port", self.port, default=3333, doc="Base port of application command endpoints"), # Obscure @@ -59,22 +60,20 @@ local cs = { s.field( "connectivity_service_threads", self.count, default=2, doc="Number of threads for the gunicorn server that serves connection info"), s.field( "connectivity_service_host", self.host, default='localhost', doc="Hostname for the ConnectivityService"), s.field( "connectivity_service_port", self.port, default=15000, doc="Port for the ConnectivityService"), - s.field( "connectivity_service_interval", self.count, default=1000, doc="Publish interval for the ConnectivityService"), - - # To move away - s.field( "use_data_network", self.flag, default = false, doc="Whether to use the data network (Won't work with k8s)"), - s.field( "data_request_timeout_ms", self.count, default=1000, doc="The baseline data request timeout that will be used by modules in the Readout and Trigger subsystems (i.e. any module that produces data fragments). Downstream timeouts, such as the trigger-record-building timeout, are derived from this."), - ]), + s.field( "connectivity_service_interval", self.count, default=1000, doc="Publish interval for the ConnectivityService") + ]), - daq : s.record("daq", [ + daq_common : s.record("daq_common", [ s.field( "data_request_timeout_ms", self.count, default=1000, doc="The baseline data request timeout that will be used by modules in the Readout and Trigger subsystems (i.e. any module that produces data fragments). Downstream timeouts, such as the trigger-record-building timeout, are derived from this."), s.field( "use_data_network", self.flag, default = false, doc="Whether to use the data network (Won't work with k8s)"), - ], doc="Cmmon daq settings"), + s.field( "data_rate_slowdown_factor",self.count, default=1, doc="Factor by which to suppress data generation. Former -s"), + ], doc="Cmmon daq_common settings"), detector : s.record("detector", [ + s.field( "op_env", self.string, default='swtest', doc="Operational environment - used for HDF5 Attribute inside the files"), s.field( "clock_speed_hz", self.freq, default=62500000), - s.field( "tpg_channel_map", self.tpg_channel_map, default="ProtoDUNESP1ChannelMap", doc="Channel map for TPG"), + s.field( "tpg_channel_map", self.tpg_channel_map, default="PD2HDChannelMap", doc="Channel map for TPG"), ], doc="Global common settings"), timing: s.record("timing", [ @@ -139,7 +138,9 @@ local cs = { s.field( "latency_buffer_numa_aware", self.flag, default=false, doc="Enable NUMA-aware mode for the Latency Buffer"), s.field( "latency_buffer_preallocation", self.flag, default=false, doc="Enable Latency Buffer preallocation"), ], doc="Exception to the default NUMA ID for FELIX cards"), + numa_exceptions: s.sequence( "NUMAExceptions", self.numa_exception, doc="Exceptions to the default NUMA ID"), + numa_config: s.record("numa_config", [ s.field( "default_id", self.count, default=0, doc="Default NUMA ID for FELIX cards"), s.field( "default_latency_numa_aware", self.flag, default=false, doc="Default for Latency Buffer NUMA awareness"), @@ -149,26 +150,30 @@ local cs = { readout: s.record("readout", [ s.field( "detector_readout_map_file", self.path, default='./DetectorReadoutMap.json', doc="File containing detector hardware map for configuration to run"), - s.field( "clock_speed_hz", self.freq, default=62500000), s.field( "use_fake_data_producers", self.flag, default=false, doc="Use fake data producers that respond with empty fragments immediately instead of (fake) cards and DLHs"), - s.field( "emulator_mode", self.flag, default=false, doc="If active, timestamps of data frames are overwritten when processed by the readout. This is necessary if the felix card does not set correct timestamps. Former -e"), - s.field( "thread_pinning_file", self.path, default="", doc="A thread pinning configuration file that gets executed after conf."), - s.field( "data_rate_slowdown_factor",self.count, default=1, doc="Factor by which to suppress data generation. Former -s"), + // s.field( "memory_limit_gb", self.count, default=64, doc="Application memory limit in GB") + // Fake cards s.field( "use_fake_cards", self.flag, default=false, doc="Use fake cards"), + s.field( "emulated_data_times_start_with_now", self.flag, default=false, doc="If active, the timestamp of the first emulated data frame is set to the current wallclock time"), s.field( "default_data_file", self.path, default='asset://?label=ProtoWIB&subsystem=readout', doc="File containing data frames to be replayed by the fake cards. Former -d. Uses the asset manager, can also be 'asset://?checksum=somelonghash', or 'file://somewhere/frames.bin' or 'frames.bin'"), s.field( "data_files", self.data_files, default=[], doc="Files to use by detector type"), + // DPDK + s.field( "dpdk_eal_args", self.string, default='-l 0-1 -n 3 -- -m [0:1].0 -j', doc='Args passed to the EAL in DPDK'), + s.field( "dpdk_rxqueues_per_lcore", self.count, default=1, doc='Number of rx queues per core'), + // FLX + s.field( "numa_config", self.numa_config, default=self.numa_config, doc='Configuration of FELIX NUMA IDs'), + // DLH + s.field( "emulator_mode", self.flag, default=false, doc="If active, timestamps of data frames are overwritten when processed by the readout. This is necessary if the felix card does not set correct timestamps. Former -e"), + s.field( "thread_pinning_file", self.path, default="", doc="A thread pinning configuration file that gets executed after conf."), + // s.field( "data_rate_slowdown_factor",self.count, default=1, doc="Factor by which to suppress data generation. Former -s"), s.field( "latency_buffer_size", self.count, default=499968, doc="Size of the latency buffers (in number of elements)"), s.field( "fragment_send_timeout_ms", self.count, default=10, doc="The send timeout that will be used in the readout modules when sending fragments downstream (i.e. to the TRB)."), s.field( "enable_tpg", self.flag, default=false, doc="Enable TPG"), s.field( "tpg_threshold", self.count, default=120, doc="Select TPG threshold"), - s.field( "tpg_algorithm", self.string, default="SWTPG", doc="Select TPG algorithm (SWTPG, AbsRS)"), + s.field( "tpg_algorithm", self.string, default="SimpleThreshold", doc="Select TPG algorithm (SimpleThreshold, AbsRS)"), s.field( "tpg_channel_mask", self.channel_list, default=[], doc="List of offline channels to be masked out from the TPHandler"), s.field( "enable_raw_recording", self.flag, default=false, doc="Add queues and modules necessary for the record command"), - s.field( "raw_recording_output_dir", self.path, default='.', doc="Output directory where recorded data is written to. Data for each link is written to a separate file"), - s.field( "dpdk_eal_args", self.string, default='-l 0-1 -n 3 -- -m [0:1].0 -j', doc='Args passed to the EAL in DPDK'), - s.field( "dpdk_rxqueues_per_lcore", self.count, default=1, doc='Number of rx queues per core'), - s.field( "numa_config", self.numa_config, default=self.numa_config, doc='Configuration of FELIX NUMA IDs'), - s.field( "emulated_data_times_start_with_now", self.flag, default=false, doc="If active, the timestamp of the first emulated data frame is set to the current wallclock time"), + s.field( "raw_recording_output_dir", self.path, default='.', doc="Output directory where recorded data is written to. Data for each link is written to a separate file") ]), trigger_algo_config: s.record("trigger_algo_config", [ @@ -264,10 +269,10 @@ local cs = { s.field( "trigger_candidate_plugin", self.string, default='TriggerCandidateMakerPrescalePlugin', doc="Trigger candidate algorithm plugin"), s.field( "trigger_candidate_config", self.trigger_algo_config, default=self.trigger_algo_config, doc="Trigger candidate algorithm config (string containing python dictionary)"), s.field( "hsi_trigger_type_passthrough", self.flag, default=false, doc="Option to override trigger type in the MLT"), - s.field( "enable_tpset_writing", self.flag, default=false, doc="Enable the writing of TPs to disk (only works with enable_tpg or enable_firmware_tpg)"), - s.field( "tpset_output_path", self.path,default='.', doc="Output directory for TPSet stream files"), - s.field( "tpset_output_file_size",self.count, default=4*1024*1024*1024, doc="The size threshold when TPSet stream files are closed (in bytes)"), - s.field( "tpg_channel_map", self.tpg_channel_map, default="ProtoDUNESP1ChannelMap", doc="Channel map for TPG"), + // s.field( "enable_tpset_writing", self.flag, default=false, doc="Enable the writing of TPs to disk (only works with enable_tpg or enable_firmware_tpg)"), + // s.field( "tpset_output_path", self.path,default='.', doc="Output directory for TPSet stream files"), + // s.field( "tpset_output_file_size",self.count, default=4*1024*1024*1024, doc="The size threshold when TPSet stream files are closed (in bytes)"), + // s.field( "tpg_channel_map", self.tpg_channel_map, default="ProtoDUNESP1ChannelMap", doc="Channel map for TPG"), s.field( "mlt_merge_overlapping_tcs", self.flag, default=true, doc="Option to turn off merging of overlapping TCs when forming TDs in MLT"), s.field( "mlt_buffer_timeout", self.count, default=100, doc="Timeout (buffer) to wait for new overlapping TCs before sending TD"), s.field( "mlt_send_timed_out_tds", self.flag, default=true, doc="Option to drop TD if TC comes out of timeout window"), @@ -281,7 +286,7 @@ local cs = { ]), dataflowapp: s.record("dataflowapp",[ - s.field("app_name", self.string, default="dataflow0"), + s.field( "app_name", self.string, default="dataflow0"), s.field( "output_paths",self.paths, default=['.'], doc="Location(s) for the dataflow app to write data. Former -o"), s.field( "host_df", self.host, default='localhost'), s.field( "max_file_size",self.count, default=4*1024*1024*1024, doc="The size threshold when raw data files are closed (in bytes)"), @@ -289,12 +294,17 @@ local cs = { s.field( "max_trigger_record_window",self.count, default=0, doc="The maximum size for the window of data that will included in a single TriggerRecord (in ticks). Readout windows that are longer than this size will result in TriggerRecords being split into a sequence of TRs. A zero value for this parameter means no splitting."), ], doc="Element of the dataflow.apps array"), + dataflowapps: s.sequence("dataflowapps", self.dataflowapp, doc="List of dataflowapp instances"), dataflow: s.record("dataflow", [ s.field( "host_dfo", self.host, default='localhost', doc="Sets the host for the DFO app"), - s.field("apps", self.dataflowapps, default=[], doc="Configuration for the dataflow apps (see dataflowapp for options)"), + s.field( "apps", self.dataflowapps, default=[], doc="Configuration for the dataflow apps (see dataflowapp for options)"), s.field( "token_count",self.count, default=10, doc="Number of tokens the dataflow apps give to the DFO. Former -c"), + // Trigger + s.field( "enable_tpset_writing", self.flag, default=false, doc="Enable the writing of TPs to disk (only works with enable_tpg or enable_firmware_tpg)"), + s.field( "tpset_output_path", self.path,default='.', doc="Output directory for TPSet stream files"), + s.field( "tpset_output_file_size",self.count, default=4*1024*1024*1024, doc="The size threshold when TPSet stream files are closed (in bytes)"), ]), dqm: s.record("dqm", [ @@ -321,14 +331,16 @@ local cs = { ]), daqconf_multiru_gen: s.record('daqconf_multiru_gen', [ - s.field('boot', self.boot, default=self.boot, doc='Boot parameters'), - s.field('dataflow', self.dataflow, default=self.dataflow, doc='Dataflow paramaters'), - s.field('dqm', self.dqm, default=self.dqm, doc='DQM parameters'), - s.field('hsi', self.hsi, default=self.hsi, doc='HSI parameters'), - s.field('ctb_hsi', self.ctb_hsi, default=self.ctb_hsi, doc='CTB parameters'), - s.field('readout', self.readout, default=self.readout, doc='Readout parameters'), - s.field('timing', self.timing, default=self.timing, doc='Timing parameters'), - s.field('trigger', self.trigger, default=self.trigger, doc='Trigger parameters'), + s.field('detector', self.detector, default=self.detector, doc='Boot parameters'), + s.field('daq_common', self.daq_common, default=self.daq_common, doc='DAQ common parameters'), + s.field('boot', self.boot, default=self.boot, doc='Boot parameters'), + s.field('dataflow', self.dataflow, default=self.dataflow, doc='Dataflow paramaters'), + s.field('dqm', self.dqm, default=self.dqm, doc='DQM parameters'), + s.field('hsi', self.hsi, default=self.hsi, doc='HSI parameters'), + s.field('ctb_hsi', self.ctb_hsi, default=self.ctb_hsi, doc='CTB parameters'), + s.field('readout', self.readout, default=self.readout, doc='Readout parameters'), + s.field('timing', self.timing, default=self.timing, doc='Timing parameters'), + s.field('trigger', self.trigger, default=self.trigger, doc='Trigger parameters'), s.field('dpdk_sender', self.dpdk_sender, default=self.dpdk_sender, doc='DPDK sender parameters'), ]), diff --git a/scripts/daqconf_multiru_gen b/scripts/daqconf_multiru_gen index 316d2d80..65463611 100755 --- a/scripts/daqconf_multiru_gen +++ b/scripts/daqconf_multiru_gen @@ -94,7 +94,12 @@ def cli( boot = confgen.boot(**config_data.boot) if debug: console.log(f"boot configuration object: {boot.pod()}") - ## etc... + detector = confgen.detector(**config_data.detector) + if debug: console.log(f"detector configuration object: {detector.pod()}") + + daq_common = confgen.daq_common(**config_data.daq_common) + if debug: console.log(f"daq_common configuration object: {daq_common.pod()}") + timing = confgen.timing(**config_data.timing) if debug: console.log(f"timing configuration object: {timing.pod()}") @@ -137,15 +142,15 @@ def cli( console.log(f"readout.detector_readout_map_file set to {readout.detector_readout_map_file}") if data_rate_slowdown_factor != 0: - readout.data_rate_slowdown_factor = data_rate_slowdown_factor - console.log(f"readout.data_rate_slowdown_factor set to {readout.data_rate_slowdown_factor}") + daq_common.data_rate_slowdown_factor = data_rate_slowdown_factor + console.log(f"daq_common.data_rate_slowdown_factor set to {daq_common.data_rate_slowdown_factor}") dqm.enable_dqm |= enable_dqm if dqm.impl == 'pocket': dqm.kafka_address = boot.pocket_url + ":30092" - file_label = file_label if file_label is not None else boot.op_env + file_label = file_label if file_label is not None else detector.op_env @@ -170,7 +175,7 @@ def cli( from daqconf.apps.tprtc_gen import get_tprtc_app console.log("Loading DPDK sender config generator") from daqconf.apps.dpdk_sender_gen import get_dpdk_sender_app - if trigger.enable_tpset_writing: + if dataflow.enable_tpset_writing: console.log("Loading TPWriter config generator") from daqconf.apps.tpwriter_gen import get_tpwriter_app @@ -209,7 +214,7 @@ def cli( if use_k8s: console.log(f'Using k8s') - trigger.tpset_output_path = abspath(trigger.tpset_output_path) + dataflow.tpset_output_path = abspath(dataflow.tpset_output_path) for df_app in appconfig_df.values(): new_output_path = [] for op in df_app.output_paths: @@ -264,7 +269,7 @@ def cli( if readout.use_fake_data_producers and dqm.enable_dqm: raise Exception("DQM can't be used with fake data producers") - if trigger.enable_tpset_writing and not readout.enable_tpg: + if dataflow.enable_tpset_writing and not readout.enable_tpg: raise Exception("TP writing can only be used when either software or firmware TPG is enabled") # if (len(region_id) != len(host_ru)) and (len(region_id) != 0): @@ -289,32 +294,6 @@ def cli( if use_k8s and not boot.k8s_image: raise Exception("You need to define k8s_image if running with k8s") -# host_id_dict = {} -# ru_configs = [] -# ru_channel_counts = {} -# for region in region_id: ru_channel_counts[region] = 0 -# -# ru_app_names=[f"ruflx{idx}" if readout.use_felix else f"ruemu{idx}" for idx in range(len(host_ru))] -# dqm_app_names = [f"dqm{idx}_ru" for idx in range(len(host_ru))] -# -# for hostidx,ru_host in enumerate(ru_app_names): -# cardid = 0 -# if host_ru[hostidx] in host_id_dict: -# host_id_dict[host_ru[hostidx]] = host_id_dict[host_ru[hostidx]] + 1 -# cardid = host_id_dict[host_ru[hostidx]] -# else: -# host_id_dict[host_ru[hostidx]] = 0 -# ru_configs.append( {"host": host_ru[hostidx], -# "card_id": cardid, -# "region_id": region_id[hostidx], -# "start_channel": ru_channel_counts[region_id[hostidx]], -# "channel_count": number_of_data_producers }) -# ru_channel_counts[region_id[hostidx]] += number_of_data_producers - -# if debug: -# console.log(f"Output data written to \"{output_path}\"") - - max_expected_tr_sequences = 1 for df_config in appconfig_df.values(): if df_config.max_trigger_record_window >= 1: @@ -334,8 +313,8 @@ def cli( TRB_TIMEOUT_SAFETY_FACTOR = 2 DFO_TIMEOUT_SAFETY_FACTOR = 3 MINIMUM_DFO_TIMEOUT = 10000 - readout_data_request_timeout = boot.data_request_timeout_ms # can that be put somewhere else? in dataflow? - trigger_data_request_timeout = boot.data_request_timeout_ms + readout_data_request_timeout = daq_common.data_request_timeout_ms # can that be put somewhere else? in dataflow? + trigger_data_request_timeout = daq_common.data_request_timeout_ms trigger_record_building_timeout = max(MINIMUM_BASIC_TRB_TIMEOUT, TRB_TIMEOUT_SAFETY_FACTOR * max(readout_data_request_timeout, trigger_data_request_timeout)) if len(ru_descs) >= 1: effective_number_of_data_producers = len(ru_descs) # number of DataLinkHandlers @@ -346,6 +325,10 @@ def cli( trigger_record_building_timeout += 15 * TRB_TIMEOUT_SAFETY_FACTOR * max_expected_tr_sequences dfo_stop_timeout = max(DFO_TIMEOUT_SAFETY_FACTOR * trigger_record_building_timeout, MINIMUM_DFO_TIMEOUT) + + # + # CTB + # if ctb_hsi.use_ctb_hsi: ctb_llt_source_id = sourceid_broker.get_next_source_id("HW_Signals_Interface") sourceid_broker.register_source_id("HW_Signals_Interface", ctb_llt_source_id, None) @@ -367,43 +350,59 @@ def cli( ) if debug: console.log("ctb hsi cmd data:", the_system.apps["ctbhsi"]) + # + # Real HSI + # if hsi.use_timing_hsi: timing_hsi_source_id = sourceid_broker.get_next_source_id("HW_Signals_Interface") sourceid_broker.register_source_id("HW_Signals_Interface", timing_hsi_source_id, None) the_system.apps["timinghsi"] = get_timing_hsi_app( - CLOCK_SPEED_HZ = readout.clock_speed_hz, - TRIGGER_RATE_HZ = trigger.trigger_rate_hz, - CONTROL_HSI_HARDWARE=hsi.control_hsi_hw, - CONNECTIONS_FILE=hsi.hsi_hw_connections_file, - READOUT_PERIOD_US = hsi.hsi_readout_period, - HSI_DEVICE_NAME = hsi.hsi_device_name, - HARDWARE_STATE_RECOVERY_ENABLED = hsi.enable_hardware_state_recovery, - HSI_ENDPOINT_ADDRESS = hsi.hsi_endpoint_address, - HSI_ENDPOINT_PARTITION = hsi.hsi_endpoint_partition, - HSI_RE_MASK=hsi.hsi_re_mask, - HSI_FE_MASK=hsi.hsi_fe_mask, - HSI_INV_MASK=hsi.hsi_inv_mask, - HSI_SOURCE=hsi.hsi_source, - HSI_SOURCE_ID=timing_hsi_source_id, - TIMING_SESSION=timing.timing_session_name, - HOST=hsi.host_timing_hsi, - DEBUG=debug) + detector = detector, + hsi = hsi, + source_id = timing_hsi_source_id, + + # CLOCK_SPEED_HZ = detector.clock_speed_hz, + # TRIGGER_RATE_HZ = trigger.trigger_rate_hz, + # CONTROL_HSI_HARDWARE=hsi.control_hsi_hw, + # CONNECTIONS_FILE=hsi.hsi_hw_connections_file, + # READOUT_PERIOD_US = hsi.hsi_readout_period, + # HSI_DEVICE_NAME = hsi.hsi_device_name, + # HARDWARE_STATE_RECOVERY_ENABLED = hsi.enable_hardware_state_recovery, + # HSI_ENDPOINT_ADDRESS = hsi.hsi_endpoint_address, + # HSI_ENDPOINT_PARTITION = hsi.hsi_endpoint_partition, + # HSI_RE_MASK=hsi.hsi_re_mask, + # HSI_FE_MASK=hsi.hsi_fe_mask, + # HSI_INV_MASK=hsi.hsi_inv_mask, + # HSI_SOURCE=hsi.hsi_source, + # HSI_SOURCE_ID=timing_hsi_source_id, + # TIMING_SESSION=timing.timing_session_name, + # HOST=hsi.host_timing_hsi, + DEBUG=debug + ) if debug: console.log("timing hsi cmd data:", the_system.apps["timinghsi"]) + # + # Fake HSI + # if hsi.use_fake_hsi: fake_hsi_source_id = sourceid_broker.get_next_source_id("HW_Signals_Interface") sourceid_broker.register_source_id("HW_Signals_Interface", fake_hsi_source_id, None) the_system.apps["fakehsi"] = get_fake_hsi_app( - CLOCK_SPEED_HZ = readout.clock_speed_hz, - DATA_RATE_SLOWDOWN_FACTOR = readout.data_rate_slowdown_factor, - TRIGGER_RATE_HZ = trigger.trigger_rate_hz, - HSI_SOURCE_ID=fake_hsi_source_id, - MEAN_SIGNAL_MULTIPLICITY = hsi.mean_hsi_signal_multiplicity, - SIGNAL_EMULATION_MODE = hsi.hsi_signal_emulation_mode, - ENABLED_SIGNALS = hsi.enabled_hsi_signals, - HOST=hsi.host_fake_hsi, + detector = detector, + hsi = hsi, + daq_common = daq_common, + source_id = fake_hsi_source_id, + + # CLOCK_SPEED_HZ = detector.clock_speed_hz, + # DATA_RATE_SLOWDOWN_FACTOR = daq_common.data_rate_slowdown_factor, + # TRIGGER_RATE_HZ = trigger.trigger_rate_hz, + # HSI_SOURCE_ID=fake_hsi_source_id, + # MEAN_SIGNAL_MULTIPLICITY = hsi.mean_hsi_signal_multiplicity, + # SIGNAL_EMULATION_MODE = hsi.hsi_signal_emulation_mode, + # ENABLED_SIGNALS = hsi.enabled_hsi_signals, + # HOST=hsi.host_fake_hsi, DEBUG=debug) if debug: console.log("fake hsi cmd data:", the_system.apps["fakehsi"]) @@ -411,43 +410,50 @@ def cli( if timing.control_timing_partition: the_system.apps["tprtc"] = get_tprtc_app( - MASTER_DEVICE_NAME=timing.timing_partition_master_device_name, - TIMING_PARTITION_ID=timing.timing_partition_id, - TRIGGER_MASK=timing.timing_partition_trigger_mask, - RATE_CONTROL_ENABLED=timing.timing_partition_rate_control_enabled, - SPILL_GATE_ENABLED=timing.timing_partition_spill_gate_enabled, - TIMING_SESSION=timing.timing_session_name, - HOST=timing.host_tprtc, - DEBUG=debug) + timing, + # MASTER_DEVICE_NAME=timing.timing_partition_master_device_name, + # TIMING_PARTITION_ID=timing.timing_partition_id, + # TRIGGER_MASK=timing.timing_partition_trigger_mask, + # RATE_CONTROL_ENABLED=timing.timing_partition_rate_control_enabled, + # SPILL_GATE_ENABLED=timing.timing_partition_spill_gate_enabled, + # TIMING_SESSION=timing.timing_session_name, + # HOST=timing.host_tprtc, + DEBUG=debug + ) the_system.apps['trigger'] = get_trigger_app( - DATA_RATE_SLOWDOWN_FACTOR = readout.data_rate_slowdown_factor, - CLOCK_SPEED_HZ = readout.clock_speed_hz, - TP_CONFIG = tp_infos, - TOLERATE_INCOMPLETENESS=trigger.tolerate_incompleteness, - COMPLETENESS_TOLERANCE=trigger.completeness_tolerance, - ACTIVITY_PLUGIN = trigger.trigger_activity_plugin, - ACTIVITY_CONFIG = trigger.trigger_activity_config, - CANDIDATE_PLUGIN = trigger.trigger_candidate_plugin, - CANDIDATE_CONFIG = trigger.trigger_candidate_config, - TTCM_S1=trigger.ttcm_s1, - TTCM_S2=trigger.ttcm_s2, - TRIGGER_WINDOW_BEFORE_TICKS = trigger.trigger_window_before_ticks, - TRIGGER_WINDOW_AFTER_TICKS = trigger.trigger_window_after_ticks, - HSI_TRIGGER_TYPE_PASSTHROUGH = trigger.hsi_trigger_type_passthrough, - MLT_MERGE_OVERLAPPING_TCS = trigger.mlt_merge_overlapping_tcs, - MLT_BUFFER_TIMEOUT = trigger.mlt_buffer_timeout, - MLT_MAX_TD_LENGTH_MS = trigger.mlt_max_td_length_ms, - MLT_SEND_TIMED_OUT_TDS = trigger.mlt_send_timed_out_tds, - MLT_IGNORE_TC = trigger.mlt_ignore_tc, - MLT_USE_READOUT_MAP = trigger.mlt_use_readout_map, - MLT_READOUT_MAP = trigger.mlt_td_readout_map, - USE_CUSTOM_MAKER = trigger.use_custom_maker, - CTCM_TYPES = trigger.ctcm_trigger_types, - CTCM_INTERVAL = trigger.ctcm_trigger_intervals, - CHANNEL_MAP_NAME = trigger.tpg_channel_map, - DATA_REQUEST_TIMEOUT=trigger_data_request_timeout, - HOST=trigger.host_trigger, + trigger, + detector, + daq_common, + tp_infos, + trigger_data_request_timeout, + # DATA_RATE_SLOWDOWN_FACTOR = daq_common.data_rate_slowdown_factor, + # CLOCK_SPEED_HZ = detector.clock_speed_hz, + # TP_CONFIG = tp_infos, + # TOLERATE_INCOMPLETENESS=trigger.tolerate_incompleteness, + # COMPLETENESS_TOLERANCE=trigger.completeness_tolerance, + # ACTIVITY_PLUGIN = trigger.trigger_activity_plugin, + # ACTIVITY_CONFIG = trigger.trigger_activity_config, + # CANDIDATE_PLUGIN = trigger.trigger_candidate_plugin, + # CANDIDATE_CONFIG = trigger.trigger_candidate_config, + # TTCM_S1=trigger.ttcm_s1, + # TTCM_S2=trigger.ttcm_s2, + # TRIGGER_WINDOW_BEFORE_TICKS = trigger.trigger_window_before_ticks, + # TRIGGER_WINDOW_AFTER_TICKS = trigger.trigger_window_after_ticks, + # HSI_TRIGGER_TYPE_PASSTHROUGH = trigger.hsi_trigger_type_passthrough, + # MLT_MERGE_OVERLAPPING_TCS = trigger.mlt_merge_overlapping_tcs, + # MLT_BUFFER_TIMEOUT = trigger.mlt_buffer_timeout, + # MLT_MAX_TD_LENGTH_MS = trigger.mlt_max_td_length_ms, + # MLT_SEND_TIMED_OUT_TDS = trigger.mlt_send_timed_out_tds, + # MLT_IGNORE_TC = trigger.mlt_ignore_tc, + # MLT_USE_READOUT_MAP = trigger.mlt_use_readout_map, + # MLT_READOUT_MAP = trigger.mlt_td_readout_map, + # USE_CUSTOM_MAKER = trigger.use_custom_maker, + # CTCM_TYPES = trigger.ctcm_trigger_types, + # CTCM_INTERVAL = trigger.ctcm_trigger_intervals, + # CHANNEL_MAP_NAME = detector.tpg_channel_map, + # DATA_REQUEST_TIMEOUT=trigger_data_request_timeout, + # HOST=trigger.host_trigger, DEBUG=debug) the_system.apps['dfo'] = get_dfo_app( @@ -463,8 +469,10 @@ def cli( ru_app_names=[] dqm_app_names = [] - - roapp_gen = ReadoutAppGenerator(readout) + # + # Readout applications generatioo + # + roapp_gen = ReadoutAppGenerator(readout, detector, daq_common) for ru_i,(ru_name, ru_desc) in enumerate(ru_descs.items()): if readout.use_fake_data_producers == False: @@ -472,14 +480,14 @@ def cli( RU_DESCRIPTOR=ru_desc, SOURCEID_BROKER=sourceid_broker, data_file_map=data_file_map, - tpg_channel_map=trigger.tpg_channel_map, + tpg_channel_map=detector.tpg_channel_map, data_timeout_requests=readout_data_request_timeout ) else: the_system.apps[ru_name] = create_fake_reaout_app( RU_DESCRIPTOR = ru_desc, - CLOCK_SPEED_HZ = readout.clock_speed_hz, + CLOCK_SPEED_HZ = detector.clock_speed_hz, ) @@ -500,8 +508,8 @@ def cli( the_system.apps[dqm_name] = get_dqm_app( DQM_IMPL=dqm.impl, - DATA_RATE_SLOWDOWN_FACTOR=readout.data_rate_slowdown_factor, - CLOCK_SPEED_HZ=readout.clock_speed_hz, + DATA_RATE_SLOWDOWN_FACTOR=daq_common.data_rate_slowdown_factor, + CLOCK_SPEED_HZ=detector.clock_speed_hz, MAX_NUM_FRAMES=dqm.max_num_frames, DQMIDX = ru_i, KAFKA_ADDRESS=dqm.kafka_address, @@ -524,13 +532,17 @@ def cli( dqm_df_app_names = [] idx = 0 + + # + # Dataflow applications generatioo + # for app_name,df_config in appconfig_df.items(): dfidx = df_config.source_id the_system.apps[app_name] = get_dataflow_app( HOSTIDX=dfidx, OUTPUT_PATHS = df_config.output_paths, APP_NAME=app_name, - OPERATIONAL_ENVIRONMENT = boot.op_env, + OPERATIONAL_ENVIRONMENT = detector.op_env, FILE_LABEL = file_label, DATA_STORE_MODE=df_config.data_store_mode, MAX_FILE_SIZE = df_config.max_file_size, @@ -558,8 +570,8 @@ def cli( dqm_links = [ s.src_id for s in ru_desc.streams ] the_system.apps[dqm_name] = get_dqm_app( DQM_IMPL=dqm.impl, - DATA_RATE_SLOWDOWN_FACTOR = readout.data_rate_slowdown_factor, - CLOCK_SPEED_HZ = readout.clock_speed_hz, + DATA_RATE_SLOWDOWN_FACTOR = daq_common.data_rate_slowdown_factor, + CLOCK_SPEED_HZ = detector.clock_speed_hz, MAX_NUM_FRAMES=dqm.max_num_frames, DQMIDX = dfidx, KAFKA_ADDRESS=dqm.kafka_address, @@ -583,17 +595,21 @@ def cli( if debug: console.log(f"{dqm_name} app: {the_system.apps[dqm_name]}") idx += 1 - if trigger.enable_tpset_writing: + # + # TPSet Writer applications generatioo + # + if dataflow.enable_tpset_writing: tpw_name=f'tpwriter' dfidx = sourceid_broker.get_next_source_id("TRBuilder") sourceid_broker.register_source_id("TRBuilder", dfidx, None) the_system.apps[tpw_name] = get_tpwriter_app( - OUTPUT_PATH = trigger.tpset_output_path, + OUTPUT_PATH = dataflow.tpset_output_path, APP_NAME = tpw_name, - OPERATIONAL_ENVIRONMENT = boot.op_env, - MAX_FILE_SIZE = trigger.tpset_output_file_size, - DATA_RATE_SLOWDOWN_FACTOR = readout.data_rate_slowdown_factor, - CLOCK_SPEED_HZ = readout.clock_speed_hz, + OPERATIONAL_ENVIRONMENT = detector.op_env, + FILE_LABEL = file_label, + MAX_FILE_SIZE = dataflow.tpset_output_file_size, + DATA_RATE_SLOWDOWN_FACTOR = daq_common.data_rate_slowdown_factor, + CLOCK_SPEED_HZ = detector.clock_speed_hz, SRC_GEO_ID_MAP=dro_map.get_src_geo_map(), SOURCE_IDX=dfidx, HOST=trigger.host_tpw, @@ -601,8 +617,8 @@ def cli( if use_k8s: ## TODO schema the_system.apps[tpw_name].mounted_dirs += [{ 'name': 'raw-data', - 'physical_location':trigger.tpset_output_path, - 'in_pod_location':trigger.tpset_output_path, + 'physical_location':dataflow.tpset_output_path, + 'in_pod_location':dataflow.tpset_output_path, 'read_only': False }] @@ -660,9 +676,6 @@ def cli( # # This code should be removed after 2.10, when we will have # decided how to handle raw TP data as fragments -# for link in mlt_links: -# if link["subsystem"] == system_type and link["element"] >= 1000: -# remove_mlt_link(the_system, link) mlt_links=the_system.apps["trigger"].modulegraph.get_module("mlt").conf.links if debug: @@ -694,7 +707,7 @@ def cli( # ru_name = ru_app_names[i] for name in ru_app_names: forced_deps.append(['hsi', ru_name]) - if trigger.enable_tpset_writing: + if dataflow.enable_tpset_writing: forced_deps.append(['tpwriter', ru_name]) if dqm.enable_dqm: @@ -728,7 +741,7 @@ def cli( else: return control_hostname - if boot.use_data_network: + if daq_common.use_data_network: CDN = control_to_data_network else: CDN = None diff --git a/scripts/dromap_editor b/scripts/dromap_editor index 544d8fa9..73ff5397 100755 --- a/scripts/dromap_editor +++ b/scripts/dromap_editor @@ -1,6 +1,7 @@ #!/usr/bin/env python import daqconf.detreadoutmap as dromap +import detdataformats import click @@ -157,9 +158,95 @@ def add_eth(obj, force, src_id, **kwargs): -@cli.command("add-wib-crate") -def add_wib_crate(): - pass +@cli.command("add-np-wib-crate") +@click.argument('addrbook_path', type=click.Path(exists=True)) +@click.argument('wib_filter', type=str) +@click.argument('ru_interface', type=str) +@click.option('--rx-iface', type=int, default=0, help="Interface id on the receiver host") +@click.pass_obj +def add_np_wib_crate(obj, addrbook_path, wib_filter, ru_interface, rx_iface): + """Adds collections of wibs to the readout map and routes them to a destination""" + m = obj + + with (open(addrbook_path, 'r')) as f: + addrbook = json.load(f) + + import re + + wib_re = re.compile(wib_filter) + wib_sources = { k:v.copy() for k,v in addrbook.items() if wib_re.match(k)} + if not wib_sources: + raise RuntimeError(f'No sources selected by {wib_filter}') + + for host in wib_sources: + del wib_sources[host][host] + + + + ru_hosts = [k for k,v in addrbook.items() if ru_interface in v] + if not ru_hosts: + raise RuntimeError(f"Readout unit interface '{ru_interface}' not found") + elif len(ru_hosts) > 1: + raise RuntimeError(f"Readout unit interface '{ru_interface}' found on multiple hosts {ru_hosts}") + + ru_host = ru_hosts[0] + ru_rx = addrbook[ru_host][ru_interface] + + # Start from the next available src id + src_id = max(m.get())+1 if m.get() else 0 + + # Constant + link_stream_offset = 0x40 + + for name, ifaces in wib_sources.items(): + + # Recover detector, crate, slot from NP wib name + name_tokens = name.split('-') + print(name_tokens) + + match name_tokens[0]: + case 'np04': + det_id = detdataformats.DetID.kHD_TPC + case 'np02': + det_id = detdataformats.DetID.kVD_BottomTPC + case other: + raise ValueError(f'Detector {name_tokens[0]} Unknown') + + wib_id = int(name_tokens[-1]) + + crate_id = (wib_id % 1000)//100 + slot_id = (wib_id % 100)-1 + + for ifname, ifdata in ifaces.items(): + + link = int(ifname.removeprefix(name+'-d')) + if link not in (0,1): + raise ValueError(f"Recovered link id {link} from {ifname} is not 0 or 1 as expected") + + for s in range(4): + m.add_srcid( + src_id, + dromap.GeoID( + det_id=det_id.value, + crate_id=crate_id, + slot_id=slot_id, + stream_id=link_stream_offset*link+s + ), + 'eth', + protocol='udp', + mode='fix_rate', + tx_host=name, + tx_mac=ifdata['mac'], + tx_ip=ifdata['ip'], + rx_host=ru_host, + rx_mac=ru_rx['mac'], + rx_ip=ru_rx['ip'], + rx_iface=rx_iface, + ) + src_id += 1 + + print(m.as_table()) + @cli.command("save", help="Save the map to json file") @click.argument('path', type=click.Path()) From c9ecb6261a5537b87dde6e923e8516d3d817eecb Mon Sep 17 00:00:00 2001 From: Alessandro Thea Date: Sun, 18 Jun 2023 16:02:43 +0200 Subject: [PATCH 24/90] Refactoring in progress --- python/daqconf/apps/dataflow_gen.py | 45 ++- python/daqconf/apps/dqm_gen.py | 77 ++-- python/daqconf/apps/fake_hsi_gen.py | 20 +- python/daqconf/apps/hsi_gen.py | 20 +- python/daqconf/apps/readout_gen.py | 187 +-------- python/daqconf/apps/tprtc_gen.py | 9 +- python/daqconf/apps/tpwriter_gen.py | 38 +- python/daqconf/apps/trigger_gen.py | 36 +- python/daqconf/core/conf_utils.py | 19 +- python/daqconf/core/config_file.py | 2 +- schema/daqconf/confgen.jsonnet | 5 +- scripts/daqconf_multiru_gen | 566 ++++++++++++++-------------- 12 files changed, 429 insertions(+), 595 deletions(-) diff --git a/python/daqconf/apps/dataflow_gen.py b/python/daqconf/apps/dataflow_gen.py index cd1ca44e..ac14fef1 100644 --- a/python/daqconf/apps/dataflow_gen.py +++ b/python/daqconf/apps/dataflow_gen.py @@ -29,24 +29,31 @@ # Time to wait on pop() QUEUE_POP_WAIT_MS = 100 -def get_dataflow_app(HOSTIDX=0, - OUTPUT_PATHS=["."], - APP_NAME="dataflow0", - OPERATIONAL_ENVIRONMENT="swtest", - FILE_LABEL="swtest", - DATA_STORE_MODE='all-per-file', - MAX_FILE_SIZE=4*1024*1024*1024, - MAX_TRIGGER_RECORD_WINDOW=0, - MAX_EXPECTED_TR_SEQUENCES=1, - TOKEN_COUNT=10, - TRB_TIMEOUT=200, - HOST="localhost", - HAS_DQM=False, - SRC_GEO_ID_MAP='', - DEBUG=False): +def get_dataflow_app( + df_config, + dataflow, + detector, + HOSTIDX=0, + APP_NAME="dataflow0", + FILE_LABEL="swtest", + MAX_EXPECTED_TR_SEQUENCES=1, + TRB_TIMEOUT=200, + HAS_DQM=False, + SRC_GEO_ID_MAP='', + DEBUG=False + ): """Generate the json configuration for the readout and DF process""" + OUTPUT_PATHS = df_config.output_paths + OPERATIONAL_ENVIRONMENT = detector.op_env + DATA_STORE_MODE=df_config.data_store_mode + MAX_FILE_SIZE = df_config.max_file_size + MAX_TRIGGER_RECORD_WINDOW = df_config.max_trigger_record_window + TOKEN_COUNT = dataflow.token_count + HOST=df_config.host_df + + modules = [] queues = [] @@ -118,4 +125,12 @@ def get_dataflow_app(HOSTIDX=0, df_app = App(modulegraph=mgraph, host=HOST) + + df_app.mounted_dirs += [{ + 'name': f'raw-data-{i}', + 'physical_location': opath, + 'in_pod_location': opath, + 'read_only': False, + } for i,opath in enumerate(set(OUTPUT_PATHS))] + return df_app diff --git a/python/daqconf/apps/dqm_gen.py b/python/daqconf/apps/dqm_gen.py index b358ecaf..10bbaaa9 100644 --- a/python/daqconf/apps/dqm_gen.py +++ b/python/daqconf/apps/dqm_gen.py @@ -30,31 +30,56 @@ # Time to wait on pop() QUEUE_POP_WAIT_MS = 100 -def get_dqm_app(DQM_IMPL='', - DATA_RATE_SLOWDOWN_FACTOR=1, - CLOCK_SPEED_HZ=62500000, - DQMIDX=0, - MAX_NUM_FRAMES=32768, - KAFKA_ADDRESS='', - KAFKA_TOPIC='', - CMAP='HD', - RAW_PARAMS=[60, 50], - RMS_PARAMS=[10, 1000], - STD_PARAMS=[10, 1000], - FOURIER_CHANNEL_PARAMS=[600, 100], - FOURIER_PLANE_PARAMS=[60, 1000], - LINKS=[], - HOST="localhost", - MODE="readout", - DF_RATE=10, - DF_ALGS='raw std fourier_plane', - DF_TIME_WINDOW=0, - # DRO_CONFIG=None, - RU_STREAMS=None, - RU_APPNAME="ru_0", - TRB_DQM_SOURCEID_OFFSET=0, - DEBUG=False, - ): +def get_dqm_app( + dqm, + detector, + daq_common, + + # DQM_IMPL='', + # DATA_RATE_SLOWDOWN_FACTOR=1, + # CLOCK_SPEED_HZ=62500000, + DQMIDX=0, + # MAX_NUM_FRAMES=32768, + # KAFKA_ADDRESS='', + # KAFKA_TOPIC='', + # CMAP='HD', + # RAW_PARAMS=[60, 50], + # RMS_PARAMS=[10, 1000], + # STD_PARAMS=[10, 1000], + # FOURIER_CHANNEL_PARAMS=[600, 100], + # FOURIER_PLANE_PARAMS=[60, 1000], + LINKS=[], + # HOST="localhost", + MODE="readout", + DF_RATE=10, + DF_ALGS='raw std fourier_plane', + DF_TIME_WINDOW=0, + # DRO_CONFIG=None, + RU_STREAMS=None, + RU_APPNAME="ru_0", + TRB_DQM_SOURCEID_OFFSET=0, + DEBUG=False, + ): + + + DQM_IMPL=dqm.impl + DATA_RATE_SLOWDOWN_FACTOR=daq_common.data_rate_slowdown_factor + CLOCK_SPEED_HZ=detector.clock_speed_hz + MAX_NUM_FRAMES=dqm.max_num_frames + # DQMIDX = ru_i + KAFKA_ADDRESS=dqm.kafka_address + KAFKA_TOPIC=dqm.kafka_topic + CMAP=dqm.cmap + RAW_PARAMS=dqm.raw_params + RMS_PARAMS=dqm.rms_params + STD_PARAMS=dqm.std_params + FOURIER_CHANNEL_PARAMS=dqm.fourier_channel_params + FOURIER_PLANE_PARAMS=dqm.fourier_plane_params + # LINKS=dqm_links + # RU_APPNAME=ru_name + # TRB_DQM_SOURCEID_OFFSET=trb_dqm_sourceid_offset + HOST=dqm.host_dqm[DQMIDX % len(dqm.host_dqm)] + # RU_STREAMS=ru_desc.streams FRONTEND_TYPE = DetID.subdetector_to_string(DetID.Subdetector(RU_STREAMS[0].geo_id.det_id)) if ((FRONTEND_TYPE== "HD_TPC" or FRONTEND_TYPE== "VD_Bottom_TPC") and CLOCK_SPEED_HZ== 50000000): @@ -115,7 +140,7 @@ def get_dqm_app(DQM_IMPL='', mgraph = ModuleGraph(modules) if MODE == 'readout': - mgraph.add_endpoint(f"timesync_{RU_APPNAME}_.*", "dqmprocessor.timesync_input", "TimeSync", Direction.IN, is_pubsub=True) + mgraph.add_endpoint(f"timesync_.*", "dqmprocessor.timesync_input", "TimeSync", Direction.IN, is_pubsub=True) mgraph.connect_modules("dqmprocessor.trigger_decision_output", "trb_dqm.trigger_decision_input", "TriggerDecision", 'trigger_decision_q_dqm') mgraph.connect_modules('trb_dqm.trigger_record_output', 'dqmprocessor.trigger_record_input', "TriggerRecord", 'trigger_record_q_dqm', toposort=False) else: diff --git a/python/daqconf/apps/fake_hsi_gen.py b/python/daqconf/apps/fake_hsi_gen.py index c01b67e3..d105da1f 100644 --- a/python/daqconf/apps/fake_hsi_gen.py +++ b/python/daqconf/apps/fake_hsi_gen.py @@ -47,20 +47,12 @@ def get_fake_hsi_app( hsi, daq_common, source_id, - # TRIGGER_RATE_HZ: int=1, - - # CLOCK_SPEED_HZ: int=62500000, - # DATA_RATE_SLOWDOWN_FACTOR: int=1, - # TRIGGER_RATE_HZ: int=1, - # HSI_SOURCE_ID: int=0, - # MEAN_SIGNAL_MULTIPLICITY: int=0, - # SIGNAL_EMULATION_MODE: int=0, - # ENABLED_SIGNALS: int=0b00000001, - QUEUE_POP_WAIT_MS=10, - LATENCY_BUFFER_SIZE=100000, - DATA_REQUEST_TIMEOUT=1000, - # HOST="localhost", - DEBUG=False): + QUEUE_POP_WAIT_MS=10, + LATENCY_BUFFER_SIZE=100000, + DATA_REQUEST_TIMEOUT=1000, + # HOST="localhost", + DEBUG=False + ): CLOCK_SPEED_HZ = detector.clock_speed_hz diff --git a/python/daqconf/apps/hsi_gen.py b/python/daqconf/apps/hsi_gen.py index 0b9e9fe1..8a6deb1b 100644 --- a/python/daqconf/apps/hsi_gen.py +++ b/python/daqconf/apps/hsi_gen.py @@ -42,34 +42,16 @@ def get_timing_hsi_app( daq_common, source_id, timing_session_name, - # TRIGGER_RATE_HZ: int = 1, - # CLOCK_SPEED_HZ: int = 62500000, - # TRIGGER_RATE_HZ: int = 1, - # DATA_RATE_SLOWDOWN_FACTOR: int=1, - # CONTROL_HSI_HARDWARE = False, - # READOUT_PERIOD_US: int = 1e3, - # HSI_ENDPOINT_ADDRESS = 1, - # HSI_ENDPOINT_PARTITION = 0, - # HSI_RE_MASK = 0x20000, - # HSI_FE_MASK = 0, - # HSI_INV_MASK = 0, - # HSI_SOURCE = 1, - # HSI_SOURCE_ID = 0, - # CONNECTIONS_FILE="${TIMING_SHARE}/config/etc/connections.xml", - # HSI_DEVICE_NAME="BOREAS_TLU", UHAL_LOG_LEVEL="notice", QUEUE_POP_WAIT_MS=10, LATENCY_BUFFER_SIZE=100000, DATA_REQUEST_TIMEOUT=1000, - # TIMING_SESSION="", - # HARDWARE_STATE_RECOVERY_ENABLED=True, - # HOST="localhost", DEBUG=False): - + # Temp vars CLOCK_SPEED_HZ = detector.clock_speed_hz # TRIGGER_RATE_HZ = trigger.trigger_rate_hz DATA_RATE_SLOWDOWN_FACTOR = daq_common.data_rate_slowdown_factor diff --git a/python/daqconf/apps/readout_gen.py b/python/daqconf/apps/readout_gen.py index b35b4607..a6b43875 100644 --- a/python/daqconf/apps/readout_gen.py +++ b/python/daqconf/apps/readout_gen.py @@ -777,17 +777,15 @@ def generate( RU_DESCRIPTOR, SOURCEID_BROKER, data_file_map, - tpg_channel_map, data_timeout_requests, ): """Generate the readout applicaton Args: - RU_DESCRIPTOR (_type_): _description_ - SOURCEID_BROKER (SourceIDBroker): _description_ - data_file_map (_type_): _description_ - tpg_channel_map (_type_): _description_ - data_timeout_requests (_type_): _description_ + RU_DESCRIPTOR (ReadoutUnitDescriptor): A readout unit descriptor object + SOURCEID_BROKER (SourceIDBroker): The source ID brocker + data_file_map (dict): Map of pattern files to application + data_timeout_requests (_type_): Data timeout request Raises: RuntimeError: _description_ @@ -796,8 +794,6 @@ def generate( _type_: _description_ """ - - numa_id, latency_numa, latency_preallocate, card_override = self.get_numa_cfg(RU_DESCRIPTOR) cfg = self.ro_cfg TPG_ENABLED = cfg.enable_tpg @@ -878,7 +874,7 @@ def generate( if TPG_ENABLED: dlhs_mods = self.add_tp_processing( dlh_list=dlhs_mods, - TPG_CHANNEL_MAP=tpg_channel_map, + TPG_CHANNEL_MAP=self.det_cfg.tpg_channel_map, ) modules += dlhs_mods @@ -918,7 +914,7 @@ def generate( readout_app = App(mgraph, host=RU_DESCRIPTOR.host_name) - + # Kubernetes-specific extensions if RU_DESCRIPTOR.kind == 'flx': c = card_override if card_override != -1 else RU_DESCRIPTOR.iface readout_app.resources = { @@ -952,179 +948,10 @@ def generate( - -# ### -# # Create Readout Application -# ### -# def create_readout_app( -# RU_DESCRIPTOR, -# SOURCEID_BROKER : SourceIDBroker = None, -# EMULATOR_MODE=False, -# DATA_RATE_SLOWDOWN_FACTOR=1, -# DEFAULT_DATA_FILE="./frames.bin", -# DATA_FILES={}, -# USE_FAKE_CARDS=True, -# CLOCK_SPEED_HZ=62500000, -# RAW_RECORDING_ENABLED=False, -# RAW_RECORDING_OUTPUT_DIR=".", -# CHANNEL_MASK_TPG: list = [], -# THRESHOLD_TPG=120, -# ALGORITHM_TPG="SWTPG", -# TPG_ENABLED=False, -# TPG_CHANNEL_MAP= "ProtoDUNESP1ChannelMap", -# DATA_REQUEST_TIMEOUT=1000, -# FRAGMENT_SEND_TIMEOUT=10, -# EAL_ARGS='-l 0-1 -n 3 -- -m [0:1].0 -j', -# NUMA_ID=0, -# LATENCY_BUFFER_SIZE=499968, -# LATENCY_BUFFER_NUMA_AWARE = False, -# LATENCY_BUFFER_ALLOCATION_MODE = False, - -# CARD_ID_OVERRIDE = -1, -# EMULATED_DATA_TIMES_START_WITH_NOW = False, -# DEBUG=False -# ) -> App: - -# FRONTEND_TYPE, QUEUE_FRAGMENT_TYPE, _, _, _ = compute_data_types(RU_DESCRIPTOR.det_id, CLOCK_SPEED_HZ, RU_DESCRIPTOR.kind) - -# # TPG is automatically disabled for non wib2 frontends -# TPG_ENABLED = TPG_ENABLED and (FRONTEND_TYPE=='wib2' or FRONTEND_TYPE=='wibeth') - -# modules = [] -# queues = [] - - -# # Create the card readers -# cr_mods = [] -# cr_queues = [] - - -# # Create the card readers -# if USE_FAKE_CARDS: -# fakecr_mods, fakecr_queues = create_fake_cardreader( -# FRONTEND_TYPE=FRONTEND_TYPE, -# QUEUE_FRAGMENT_TYPE=QUEUE_FRAGMENT_TYPE, -# DATA_RATE_SLOWDOWN_FACTOR=DATA_RATE_SLOWDOWN_FACTOR, -# DATA_FILES=DATA_FILES, -# DEFAULT_DATA_FILE=DEFAULT_DATA_FILE, -# CLOCK_SPEED_HZ=CLOCK_SPEED_HZ, -# EMULATED_DATA_TIMES_START_WITH_NOW=EMULATED_DATA_TIMES_START_WITH_NOW, -# RU_DESCRIPTOR=RU_DESCRIPTOR -# ) -# cr_mods += fakecr_mods -# cr_queues += fakecr_queues -# else: -# if RU_DESCRIPTOR.kind == 'flx': -# flx_mods, flx_queues = create_felix_cardreader( -# FRONTEND_TYPE=FRONTEND_TYPE, -# QUEUE_FRAGMENT_TYPE=QUEUE_FRAGMENT_TYPE, -# CARD_ID_OVERRIDE=CARD_ID_OVERRIDE, -# NUMA_ID=NUMA_ID, -# RU_DESCRIPTOR=RU_DESCRIPTOR -# ) -# cr_mods += flx_mods -# cr_queues += flx_queues - -# elif RU_DESCRIPTOR.kind == 'eth' and RU_DESCRIPTOR.streams[0].parameters.protocol == "udp": -# dpdk_mods, dpdk_queues = create_dpdk_cardreader( -# FRONTEND_TYPE=FRONTEND_TYPE, -# QUEUE_FRAGMENT_TYPE=QUEUE_FRAGMENT_TYPE, -# EAL_ARGS=EAL_ARGS, -# RU_DESCRIPTOR=RU_DESCRIPTOR -# ) -# cr_mods += dpdk_mods -# cr_queues += dpdk_queues - -# elif RU_DESCRIPTOR.kind == 'eth' and RU_DESCRIPTOR.streams[0].parameters.protocol == "zmq": - -# pac_mods, pac_queues = create_pacman_cardreader( -# FRONTEND_TYPE=FRONTEND_TYPE, -# QUEUE_FRAGMENT_TYPE=QUEUE_FRAGMENT_TYPE, -# RU_DESCRIPTOR=RU_DESCRIPTOR -# ) -# cr_mods += pac_mods -# cr_queues += pac_queues - -# modules += cr_mods -# queues += cr_queues - -# # Create the data-link handlers -# dlhs_mods, _ = create_det_dhl( -# LATENCY_BUFFER_SIZE=LATENCY_BUFFER_SIZE, -# LATENCY_BUFFER_NUMA_AWARE=LATENCY_BUFFER_NUMA_AWARE, -# LATENCY_BUFFER_ALLOCATION_MODE=LATENCY_BUFFER_ALLOCATION_MODE, -# NUMA_ID=NUMA_ID, -# SEND_PARTIAL_FRAGMENTS=False, -# RAW_RECORDING_OUTPUT_DIR=RAW_RECORDING_OUTPUT_DIR, -# DATA_REQUEST_TIMEOUT=DATA_REQUEST_TIMEOUT, -# FRAGMENT_SEND_TIMEOUT=FRAGMENT_SEND_TIMEOUT, -# RAW_RECORDING_ENABLED=RAW_RECORDING_ENABLED, -# RU_DESCRIPTOR=RU_DESCRIPTOR, -# EMULATOR_MODE=EMULATOR_MODE - -# ) - -# # Configure the TP processing if requrested -# if TPG_ENABLED: -# dlhs_mods = add_tp_processing( -# dlh_list=dlhs_mods, -# THRESHOLD_TPG=THRESHOLD_TPG, -# ALGORITHM_TPG=ALGORITHM_TPG, -# CHANNEL_MASK_TPG=CHANNEL_MASK_TPG, -# TPG_CHANNEL_MAP=TPG_CHANNEL_MAP, -# EMULATOR_MODE=EMULATOR_MODE, -# CLOCK_SPEED_HZ=CLOCK_SPEED_HZ, -# DATA_RATE_SLOWDOWN_FACTOR=DATA_RATE_SLOWDOWN_FACTOR -# ) - -# modules += dlhs_mods - -# # Add the TP datalink handlers -# if TPG_ENABLED: -# tps = { k:v for k,v in SOURCEID_BROKER.get_all_source_ids("Trigger").items() if isinstance(v, ReadoutUnitDescriptor ) and v==RU_DESCRIPTOR} -# if len(tps) != 1: -# raise RuntimeError(f"Could not retrieve unique element from source id map {tps}") - -# tpg_mods, tpg_queues = create_tp_dlhs( -# dlh_list=dlhs_mods, -# DATA_REQUEST_TIMEOUT=DATA_REQUEST_TIMEOUT, -# FRAGMENT_SEND_TIMEOUT=FRAGMENT_SEND_TIMEOUT, -# tpset_sid = next(iter(tps)) -# ) -# modules += tpg_mods -# queues += tpg_queues - -# # Create the Module graphs -# mgraph = ModuleGraph(modules, queues=queues) - -# # Add endpoints and frame producers to DRO data handlers -# add_dro_eps_and_fps( -# mgraph=mgraph, -# dlh_list=dlhs_mods, -# RUIDX=RU_DESCRIPTOR.label -# ) - -# if TPG_ENABLED: -# # Add endpoints and frame producers to TP data handlers -# add_tpg_eps_and_fps( -# mgraph=mgraph, -# # dlh_list=dlhs_mods, -# tpg_dlh_list=tpg_mods, -# RUIDX=RU_DESCRIPTOR.label -# ) - -# # Create the application -# readout_app = App(mgraph, host=RU_DESCRIPTOR.host_name) - -# # All done -# return readout_app - - - ### # Create Fake dataproducers Application ### -def create_fake_reaout_app( +def create_fake_readout_app( RU_DESCRIPTOR, CLOCK_SPEED_HZ ) -> App: diff --git a/python/daqconf/apps/tprtc_gen.py b/python/daqconf/apps/tprtc_gen.py index 939bc249..233a2a3b 100644 --- a/python/daqconf/apps/tprtc_gen.py +++ b/python/daqconf/apps/tprtc_gen.py @@ -31,14 +31,7 @@ #=============================================================================== def get_tprtc_app( - timing, - # MASTER_DEVICE_NAME="", - # TIMING_PARTITION_ID=0, - # TRIGGER_MASK=0xff, - # RATE_CONTROL_ENABLED=True, - # SPILL_GATE_ENABLED=False, - # TIMING_SESSION="", - # HOST="localhost", + timing, DEBUG=False ): diff --git a/python/daqconf/apps/tpwriter_gen.py b/python/daqconf/apps/tpwriter_gen.py index 0a17509d..32a5ef5d 100644 --- a/python/daqconf/apps/tpwriter_gen.py +++ b/python/daqconf/apps/tpwriter_gen.py @@ -26,20 +26,27 @@ QUEUE_POP_WAIT_MS = 100 def get_tpwriter_app( - OUTPUT_PATH=".", - APP_NAME="tpwriter", - OPERATIONAL_ENVIRONMENT="swtest", - FILE_LABEL = "swtest", - MAX_FILE_SIZE=4*1024*1024*1024, - DATA_RATE_SLOWDOWN_FACTOR=1, - CLOCK_SPEED_HZ=62500000, - SRC_GEO_ID_MAP='', - SOURCE_IDX=998, - HOST="localhost", - DEBUG=False): - + detector, + dataflow, + daq_common, + app_name, + file_label, + source_id, + SRC_GEO_ID_MAP, + DEBUG=False + ): """Generate the json configuration for the readout and DF process""" + # Temp vars + OUTPUT_PATH = dataflow.tpset_output_path + APP_NAME = app_name + OPERATIONAL_ENVIRONMENT = detector.op_env + MAX_FILE_SIZE = dataflow.tpset_output_file_size + DATA_RATE_SLOWDOWN_FACTOR = daq_common.data_rate_slowdown_factor + CLOCK_SPEED_HZ = detector.clock_speed_hz + SOURCE_IDX=source_id + HOST=dataflow.host_tpw + ONE_SECOND_INTERVAL_TICKS = CLOCK_SPEED_HZ / DATA_RATE_SLOWDOWN_FACTOR modules = [] @@ -77,4 +84,11 @@ def get_tpwriter_app( tpw_app = App(modulegraph=mgraph, host=HOST) + tpw_app.mounted_dirs += [{ + 'name': 'raw-data', + 'physical_location':OUTPUT_PATH, + 'in_pod_location':OUTPUT_PATH, + 'read_only': False + }] + return tpw_app diff --git a/python/daqconf/apps/trigger_gen.py b/python/daqconf/apps/trigger_gen.py index 61b66241..90b58698 100644 --- a/python/daqconf/apps/trigger_gen.py +++ b/python/daqconf/apps/trigger_gen.py @@ -78,46 +78,12 @@ def get_trigger_app( daq_common, tp_infos, trigger_data_request_timeout, - # CLOCK_SPEED_HZ: int = 62_500_000, - # DATA_RATE_SLOWDOWN_FACTOR: float = 1, - # TP_CONFIG: dict = {}, - # TOLERATE_INCOMPLETENESS=False, - # COMPLETENESS_TOLERANCE=1, - - # ACTIVITY_PLUGIN: str = 'TriggerActivityMakerPrescalePlugin', - # ACTIVITY_CONFIG: dict = dict(prescale=10000), - - # CANDIDATE_PLUGIN: str = 'TriggerCandidateMakerPrescalePlugin', - # CANDIDATE_CONFIG: int = dict(prescale=10), - USE_HSI_INPUT = True, - # TTCM_S1: int = 1, - # TTCM_S2: int = 2, - # TRIGGER_WINDOW_BEFORE_TICKS: int = 1000, - # TRIGGER_WINDOW_AFTER_TICKS: int = 1000, - # HSI_TRIGGER_TYPE_PASSTHROUGH: bool = False, - - # USE_CUSTOM_MAKER: bool = False, - # CTCM_TYPES: list = [4], - # CTCM_INTERVAL: list = [62500000], - - # MLT_MERGE_OVERLAPPING_TCS: bool = False, - # MLT_BUFFER_TIMEOUT: int = 100, - # MLT_SEND_TIMED_OUT_TDS: bool = False, - # MLT_MAX_TD_LENGTH_MS: int = 1000, - # MLT_IGNORE_TC: list = [], - # MLT_USE_READOUT_MAP: bool = False, - # MLT_READOUT_MAP: dict = {}, - USE_CHANNEL_FILTER: bool = True, - - # CHANNEL_MAP_NAME = "ProtoDUNESP1ChannelMap", - # DATA_REQUEST_TIMEOUT = 1000, - # HOST="localhost", DEBUG=False ): - # To cleanup + # Temp variables, To cleanup DATA_RATE_SLOWDOWN_FACTOR = daq_common.data_rate_slowdown_factor CLOCK_SPEED_HZ = detector.clock_speed_hz TP_CONFIG = tp_infos diff --git a/python/daqconf/core/conf_utils.py b/python/daqconf/core/conf_utils.py index 68d449a9..107b6522 100644 --- a/python/daqconf/core/conf_utils.py +++ b/python/daqconf/core/conf_utils.py @@ -200,10 +200,12 @@ def make_network_connection(the_system, endpoint_name, data_type, in_apps, out_a if len(in_apps) > 1: raise ValueError(f"Connection with name {endpoint_name} has multiple receivers, which is unsupported for a network connection!") + sender_app = in_apps[0] port = the_system.next_unassigned_port() if not use_connectivity_service or use_k8s else '*' - address_sender = f'tcp://{{{in_apps[0]}}}:{port}' if not use_k8s else f'tcp://{in_apps[0]}:{port}' - conn_id = conn.ConnectionId(uid=endpoint_name, data_type=data_type) - the_system.connections[in_apps[0]] += [conn.Connection(id=conn_id, connection_type="kSendRecv", uri=address_sender)] + address_sender = f'tcp://{{{sender_app}}}:{port}' if not use_k8s else f'tcp://{sender_app}:{port}' + conn_uid = f"{sender_app}.{endpoint_name}" + conn_id = conn.ConnectionId(uid=conn_uid, data_type=data_type) + the_system.connections[sender_app] += [conn.Connection(id=conn_id, connection_type="kSendRecv", uri=address_sender)] if not use_connectivity_service: for app in set(out_apps): the_system.connections[app] += [conn.Connection(id=conn_id, connection_type="kSendRecv", uri=address_sender)] @@ -317,17 +319,18 @@ def make_system_connections(the_system, verbose=False, use_k8s=False, use_connec subscribers += [endpoint["app"]] else: publishers += [endpoint["app"]] - if endpoint['endpoint'].external_name not in pubsub_connectionids: + conn_uid = f"{endpoint['app']}.{endpoint['endpoint'].external_name}" + if conn_uid not in pubsub_connectionids: port = the_system.next_unassigned_port() if not use_connectivity_service or use_k8s else '*' address = f'tcp://{{{endpoint["app"]}}}:{port}' if not use_k8s else f'tcp://{endpoint["app"]}:{port}' - conn_id =conn.ConnectionId( uid=endpoint['endpoint'].external_name, data_type=endpoint['endpoint'].data_type) - pubsub_connectionids[endpoint['endpoint'].external_name] = conn.Connection(id=conn_id, + conn_id =conn.ConnectionId( uid=conn_uid, data_type=endpoint['endpoint'].data_type) + pubsub_connectionids[conn_uid] = conn.Connection(id=conn_id, connection_type="kPubSub", uri=address ) - topic_connectionuids += [endpoint['endpoint'].external_name] + topic_connectionuids += [conn_uid] if endpoint['app'] not in publisher_uids.keys(): publisher_uids[endpoint["app"]] = [] - publisher_uids[endpoint["app"]] += [endpoint['endpoint'].external_name] + publisher_uids[endpoint["app"]] += [conn_uid] if len(subscribers) == 0 and check_endpoints: raise ValueError(f"Data Type {topic} has no subscribers!") diff --git a/python/daqconf/core/config_file.py b/python/daqconf/core/config_file.py index 5389a107..435bf3c2 100755 --- a/python/daqconf/core/config_file.py +++ b/python/daqconf/core/config_file.py @@ -42,7 +42,7 @@ def parse_json(filename, schemed_object): filepath = Path(filename) - basepath = filepath.parent + # basepath = filepath.parent # First pass, load the main json file with open(filepath, 'r') as f: diff --git a/schema/daqconf/confgen.jsonnet b/schema/daqconf/confgen.jsonnet index 205dd8b3..c6eb236e 100755 --- a/schema/daqconf/confgen.jsonnet +++ b/schema/daqconf/confgen.jsonnet @@ -140,7 +140,7 @@ local cs = { ], doc="Exception to the default NUMA ID for FELIX cards"), numa_exceptions: s.sequence( "NUMAExceptions", self.numa_exception, doc="Exceptions to the default NUMA ID"), - + numa_config: s.record("numa_config", [ s.field( "default_id", self.count, default=0, doc="Default NUMA ID for FELIX cards"), s.field( "default_latency_numa_aware", self.flag, default=false, doc="Default for Latency Buffer NUMA awareness"), @@ -258,7 +258,7 @@ local cs = { s.field( "trigger_window_before_ticks",self.count, default=1000, doc="Trigger window before marker. Former -b"), s.field( "trigger_window_after_ticks", self.count, default=1000, doc="Trigger window after marker. Former -a"), s.field( "host_trigger", self.host, default='localhost', doc='Host to run the trigger app on'), - s.field( "host_tpw", self.host, default='localhost', doc='Host to run the TPWriter app on'), + // s.field( "host_tpw", self.host, default='localhost', doc='Host to run the TPWriter app on'), # trigger options s.field( "completeness_tolerance", self.count, default=1, doc="Maximum number of inactive queues we will tolerate."), s.field( "tolerate_incompleteness", self.flag, default=false, doc="Flag to tell trigger to tolerate inactive queues."), @@ -302,6 +302,7 @@ local cs = { s.field( "apps", self.dataflowapps, default=[], doc="Configuration for the dataflow apps (see dataflowapp for options)"), s.field( "token_count",self.count, default=10, doc="Number of tokens the dataflow apps give to the DFO. Former -c"), // Trigger + s.field( "host_tpw", self.host, default='localhost', doc='Host to run the TPWriter app on'), s.field( "enable_tpset_writing", self.flag, default=false, doc="Enable the writing of TPs to disk (only works with enable_tpg or enable_firmware_tpg)"), s.field( "tpset_output_path", self.path,default='.', doc="Output directory for TPSet stream files"), s.field( "tpset_output_file_size",self.count, default=4*1024*1024*1024, doc="The size threshold when TPSet stream files are closed (in bytes)"), diff --git a/scripts/daqconf_multiru_gen b/scripts/daqconf_multiru_gen index 65463611..4b858594 100755 --- a/scripts/daqconf_multiru_gen +++ b/scripts/daqconf_multiru_gen @@ -26,8 +26,145 @@ moo.io.default_load_path = get_moo_model_path() # Load configuration types import moo.otypes -moo.otypes.load_types('detchannelmaps/hardwaremapservice.jsonnet') -import dunedaq.detchannelmaps.hardwaremapservice as hwms +# moo.otypes.load_types('detchannelmaps/hardwaremapservice.jsonnet') +# import dunedaq.detchannelmaps.hardwaremapservice as hwms + + +def expand_conf(config_data, debug=False): + """Expands the moo configuration record into sub-records, + re-casting its members into the corresponding moo objects. + + Args: + config_data (_type_): Configuration object + debug (bool, optional): Enable verbose reports. Defaults to False. + + Returns: + _type_: _description_ + """ + + import dunedaq.daqconf.confgen as confgen + + ## Hack, we shouldn't need to do that, in the future it should be, boot = config_data.boot + boot = confgen.boot(**config_data.boot) + if debug: console.log(f"boot configuration object: {boot.pod()}") + + detector = confgen.detector(**config_data.detector) + if debug: console.log(f"detector configuration object: {detector.pod()}") + + daq_common = confgen.daq_common(**config_data.daq_common) + if debug: console.log(f"daq_common configuration object: {daq_common.pod()}") + + timing = confgen.timing(**config_data.timing) + if debug: console.log(f"timing configuration object: {timing.pod()}") + + hsi = confgen.hsi(**config_data.hsi) + if debug: console.log(f"hsi configuration object: {hsi.pod()}") + + ctb_hsi = confgen.ctb_hsi(**config_data.ctb_hsi) + if debug: console.log(f"ctb_hsi configuration object: {ctb_hsi.pod()}") + + readout = confgen.readout(**config_data.readout) + if debug: console.log(f"readout configuration object: {readout.pod()}") + + trigger = confgen.trigger(**config_data.trigger) + if debug: console.log(f"trigger configuration object: {trigger.pod()}") + + dataflow = confgen.dataflow(**config_data.dataflow) + if debug: console.log(f"dataflow configuration object: {dataflow.pod()}") + + dqm = confgen.dqm(**config_data.dqm) + if debug: console.log(f"dqm configuration object: {dqm.pod()}") + + dpdk_sender = confgen.dpdk_sender(**config_data.dpdk_sender) + if debug: console.log(f"dpdk_sender configuration object: {dpdk_sender.pod()}") + + return ( + boot, + detector, + daq_common, + timing, + hsi, + ctb_hsi, + readout, + trigger, + dataflow, + dqm, + dpdk_sender + ) + +def validate_conf(boot, readout, dataflow, timing, hsi, dqm): + """Validate the consistency of confgen parameters + + Args: + boot (_type_): _description_ + readout (_type_): _description_ + dataflow (_type_): _description_ + timing (_type_): _description_ + hsi (_type_): _description_ + dqm (_type_): _description_ + + Raises: + Exception: _description_ + Exception: _description_ + Exception: _description_ + Exception: _description_ + Exception: _description_ + Exception: _description_ + Exception: _description_ + """ + if readout.enable_tpg and readout.use_fake_data_producers: + raise Exception("Fake data producers don't support software tpg") + + if readout.use_fake_data_producers and dqm.enable_dqm: + raise Exception("DQM can't be used with fake data producers") + + if dataflow.enable_tpset_writing and not readout.enable_tpg: + raise Exception("TP writing can only be used when either software or firmware TPG is enabled") + + if hsi.use_timing_hsi and not hsi.hsi_device_name: + raise Exception("If --use-hsi-hw flag is set to true, --hsi-device-name must be specified!") + + if timing.control_timing_partition and not timing.timing_partition_master_device_name: + raise Exception("If --control-timing-partition flag is set to true, --timing-partition-master-device-name must be specified!") + + if hsi.control_hsi_hw and not hsi.use_timing_hsi: + raise Exception("Timing HSI hardware control can only be enabled if timing HSI hardware is used!") + + if boot.process_manager == 'k8s' and not boot.k8s_image: + raise Exception("You need to define k8s_image if running with k8s") + + +def create_df_apps( + dataflow, + sourceid_broker + ): + + import dunedaq.daqconf.confgen as confgen + + if len(dataflow.apps) == 0: + console.log(f"No Dataflow apps defined, adding default dataflow0") + dataflow.apps = [confgen.dataflowapp()] + + host_df = [] + appconfig_df = {} + df_app_names = [] + for d in dataflow.apps: + console.log(f"Parsing dataflow app config {d}") + + ## Hack, we shouldn't need to do that, in the future, it should be appconfig = d + appconfig = confgen.dataflowapp(**d) + + dfapp = appconfig.app_name + if dfapp in df_app_names: + appconfig_df[dfapp].update(appconfig) + else: + df_app_names.append(dfapp) + appconfig_df[dfapp] = appconfig + appconfig_df[dfapp].source_id = sourceid_broker.get_next_source_id("TRBuilder") + sourceid_broker.register_source_id("TRBuilder", appconfig_df[dfapp].source_id, None) + host_df += [appconfig.host_df] + return host_df, appconfig_df, df_app_names + # Add -h as default help option CONTEXT_SETTINGS = dict(help_option_names=['-h', '--help']) @@ -84,48 +221,23 @@ def cli( if debug: console.log(f"Configuration for daqconf: {config_data.pod()}") + ( + boot, + detector, + daq_common, + timing, + hsi, + ctb_hsi, + readout, + trigger, + dataflow, + dqm, + dpdk_sender + ) = expand_conf(config_data, debug) - # Get our config objects - # Loading this one another time... (first time in config_file.generate_cli_from_schema) - moo.otypes.load_types('daqconf/confgen.jsonnet') - import dunedaq.daqconf.confgen as confgen - - ## Hack, we shouldn't need to do that, in the future it should be, boot = config_data.boot - boot = confgen.boot(**config_data.boot) - if debug: console.log(f"boot configuration object: {boot.pod()}") - - detector = confgen.detector(**config_data.detector) - if debug: console.log(f"detector configuration object: {detector.pod()}") - - daq_common = confgen.daq_common(**config_data.daq_common) - if debug: console.log(f"daq_common configuration object: {daq_common.pod()}") - - timing = confgen.timing(**config_data.timing) - if debug: console.log(f"timing configuration object: {timing.pod()}") - - hsi = confgen.hsi(**config_data.hsi) - if debug: console.log(f"hsi configuration object: {hsi.pod()}") - - ctb_hsi = confgen.ctb_hsi(**config_data.ctb_hsi) - if debug: console.log(f"ctb_hsi configuration object: {ctb_hsi.pod()}") - - readout = confgen.readout(**config_data.readout) - if debug: console.log(f"readout configuration object: {readout.pod()}") - - trigger = confgen.trigger(**config_data.trigger) - if debug: console.log(f"trigger configuration object: {trigger.pod()}") - - dataflow = confgen.dataflow(**config_data.dataflow) - if debug: console.log(f"dataflow configuration object: {dataflow.pod()}") - - dqm = confgen.dqm(**config_data.dqm) - if debug: console.log(f"dqm configuration object: {dqm.pod()}") - - dpdk_sender = confgen.dpdk_sender(**config_data.dpdk_sender) - if debug: console.log(f"dpdk_sender configuration object: {dpdk_sender.pod()}") - - - # Update with command-line options + # + # Update command-line options config parameters + # if force_pm is not None: boot.process_manager = force_pm console.log(f"boot.boot.process_manager set to {boot.process_manager}") @@ -152,6 +264,34 @@ def cli( file_label = file_label if file_label is not None else detector.op_env + # + # Configuration consistency checks + # + # if readout.enable_tpg and readout.use_fake_data_producers: + # raise Exception("Fake data producers don't support software tpg") + + # if readout.use_fake_data_producers and dqm.enable_dqm: + # raise Exception("DQM can't be used with fake data producers") + + # if dataflow.enable_tpset_writing and not readout.enable_tpg: + # raise Exception("TP writing can only be used when either software or firmware TPG is enabled") + + # if hsi.use_timing_hsi and not hsi.hsi_device_name: + # raise Exception("If --use-hsi-hw flag is set to true, --hsi-device-name must be specified!") + + # if timing.control_timing_partition and not timing.timing_partition_master_device_name: + # raise Exception("If --control-timing-partition flag is set to true, --timing-partition-master-device-name must be specified!") + + # if hsi.control_hsi_hw and not hsi.use_timing_hsi: + # raise Exception("Timing HSI hardware control can only be enabled if timing HSI hardware is used!") + + # if use_k8s and not boot.k8s_image: + # raise Exception("You need to define k8s_image if running with k8s") + + + + validate_conf(boot, readout, dataflow, timing, hsi, dqm) + console.log("Loading dataflow config generator") @@ -160,7 +300,7 @@ def cli( console.log("Loading dqm config generator") from daqconf.apps.dqm_gen import get_dqm_app console.log("Loading readout config generator") - from daqconf.apps.readout_gen import create_fake_reaout_app, ReadoutAppGenerator + from daqconf.apps.readout_gen import create_fake_readout_app, ReadoutAppGenerator console.log("Loading trigger config generator") from daqconf.apps.trigger_gen import get_trigger_app console.log("Loading DFO config generator") @@ -182,30 +322,38 @@ def cli( sourceid_broker = SourceIDBroker() sourceid_broker.debug = debug - if len(dataflow.apps) == 0: - console.log(f"No Dataflow apps defined, adding default dataflow0") - dataflow.apps = [confgen.dataflowapp()] - host_df = [] - appconfig_df ={} - df_app_names = [] - for d in dataflow.apps: - console.log(f"Parsing dataflow app config {d}") + # import dunedaq.daqconf.confgen as confgen - ## Hack, we shouldn't need to do that, in the future, it should be appconfig = d - appconfig = confgen.dataflowapp(**d) + # if len(dataflow.apps) == 0: + # console.log(f"No Dataflow apps defined, adding default dataflow0") + # dataflow.apps = [confgen.dataflowapp()] - dfapp = appconfig.app_name - if dfapp in df_app_names: - appconfig_df[dfapp].update(appconfig) - else: - df_app_names.append(dfapp) - appconfig_df[dfapp] = appconfig - appconfig_df[dfapp].source_id = sourceid_broker.get_next_source_id("TRBuilder") - sourceid_broker.register_source_id("TRBuilder", appconfig_df[dfapp].source_id, None) - host_df += [appconfig.host_df] + # host_df = [] + # appconfig_df ={} + # df_app_names = [] + # for d in dataflow.apps: + # console.log(f"Parsing dataflow app config {d}") + + # ## Hack, we shouldn't need to do that, in the future, it should be appconfig = d + # appconfig = confgen.dataflowapp(**d) + + # dfapp = appconfig.app_name + # if dfapp in df_app_names: + # appconfig_df[dfapp].update(appconfig) + # else: + # df_app_names.append(dfapp) + # appconfig_df[dfapp] = appconfig + # appconfig_df[dfapp].source_id = sourceid_broker.get_next_source_id("TRBuilder") + # sourceid_broker.register_source_id("TRBuilder", appconfig_df[dfapp].source_id, None) + # host_df += [appconfig.host_df] + #-------------------------------------------------------------------------- + # Create dataflow applications + #-------------------------------------------------------------------------- + host_df, appconfig_df, df_app_names = create_df_apps(dataflow=dataflow, sourceid_broker=sourceid_broker) + readout.default_data_file = resolve_asset_file(readout.default_data_file, debug) data_file_map = {} @@ -225,19 +373,13 @@ def cli( the_system = System() - # Load the hw map file here to extract ru hosts, cards, slr, links, forntend types, sourceIDs and geoIDs + # Load the readout map file here to extract ru hosts, cards, slr, links, forntend types, sourceIDs and geoIDs # The ru apps are determined by the combinations of hostname and card_id, the SourceID determines the # DLH (with physical slr+link information), the detId acts as system_type allows to infer the frontend_type - # hw_map_service = HardwareMapService(readout.detector_readout_map_file) - # serialized_hw_map = hw_map_service.get_hardware_ma84p_json() - # hw_map = hwms.HardwareMap(serialized_hw_map) - # console.log(f"{hw_map}") - - # # Get the list of RU processes - # dro_infos = hw_map_service.get_all_dro_info() - - # Load the Detector Readout map + #-------------------------------------------------------------------------- + # Load Detector Readout map + #-------------------------------------------------------------------------- dro_map = dromap.DetReadoutMapService() dro_map.load(readout.detector_readout_map_file) @@ -251,48 +393,8 @@ def cli( for ru_name, ru_desc in ru_descs.items(): console.log(f"Will generate a RU process on {ru_name} ({ru_desc.iface}, {ru_desc.kind}), {len(ru_desc.streams)} streams active") -# total_number_of_data_producers = 0 -# if use_ssp: -# total_number_of_data_producers = number_of_data_producers * len(host_ru) -# console.log(f"Will setup {number_of_data_producers} SSP channels per host, for a total of {total_number_of_data_producers}") -# else: -# total_number_of_data_producers = number_of_data_producers * len(host_ru) -# console.log(f"Will setup {number_of_data_producers} TPC channels per host, for a total of {total_number_of_data_producers}") -# -# if readout.enable_tpg and frontend_type != 'wib': -# raise Exception("Software TPG is only available for the wib at the moment!") - if readout.enable_tpg and readout.use_fake_data_producers: - raise Exception("Fake data producers don't support software tpg") - - if readout.use_fake_data_producers and dqm.enable_dqm: - raise Exception("DQM can't be used with fake data producers") - - if dataflow.enable_tpset_writing and not readout.enable_tpg: - raise Exception("TP writing can only be used when either software or firmware TPG is enabled") - -# if (len(region_id) != len(host_ru)) and (len(region_id) != 0): -# raise Exception("--region-id should be specified once for each --host-ru, or not at all!") - - # TODO, Eric Flumerfelt 22-June-2022: Fix if/when multiple frontend types are supported. (Use https://click.palletsprojects.com/en/8.1.x/options/#multi-value-options for RU host/frontend/region config?) -# if len(region_id) == 0: -# region_id_temp = [] -# for reg in range(len(host_ru)): -# region_id_temp.append(reg) -# region_id = tuple(region_id_temp) - - if hsi.use_timing_hsi and not hsi.hsi_device_name: - raise Exception("If --use-hsi-hw flag is set to true, --hsi-device-name must be specified!") - - if timing.control_timing_partition and not timing.timing_partition_master_device_name: - raise Exception("If --control-timing-partition flag is set to true, --timing-partition-master-device-name must be specified!") - - if hsi.control_hsi_hw and not hsi.use_timing_hsi: - raise Exception("Timing HSI hardware control can only be enabled if timing HSI hardware is used!") - - if use_k8s and not boot.k8s_image: - raise Exception("You need to define k8s_image if running with k8s") max_expected_tr_sequences = 1 for df_config in appconfig_df.values(): @@ -326,9 +428,9 @@ def cli( dfo_stop_timeout = max(DFO_TIMEOUT_SAFETY_FACTOR * trigger_record_building_timeout, MINIMUM_DFO_TIMEOUT) - # + #-------------------------------------------------------------------------- # CTB - # + #-------------------------------------------------------------------------- if ctb_hsi.use_ctb_hsi: ctb_llt_source_id = sourceid_broker.get_next_source_id("HW_Signals_Interface") sourceid_broker.register_source_id("HW_Signals_Interface", ctb_llt_source_id, None) @@ -337,125 +439,68 @@ def cli( sourceid_broker.register_source_id("HW_Signals_Interface", ctb_hlt_source_id, None) the_system.apps["ctbhsi"] = get_ctb_hsi_app( + ctb_hsi, nickname = "ctb", LLT_SOURCE_ID=ctb_llt_source_id, HLT_SOURCE_ID=ctb_hlt_source_id, - HOST=ctb_hsi.host_ctb_hsi, - HLT_LIST=ctb_hsi.hlt_triggers, - BEAM_LLT_LIST=ctb_hsi.beam_llt_triggers, - CRT_LLT_LIST=ctb_hsi.crt_llt_triggers, - PDS_LLT_LIST=ctb_hsi.pds_llt_triggers, - FAKE_TRIG_1=ctb_hsi.fake_trig_1, - FAKE_TRIG_2=ctb_hsi.fake_trig_2 ) if debug: console.log("ctb hsi cmd data:", the_system.apps["ctbhsi"]) - # + #-------------------------------------------------------------------------- # Real HSI - # + #-------------------------------------------------------------------------- if hsi.use_timing_hsi: timing_hsi_source_id = sourceid_broker.get_next_source_id("HW_Signals_Interface") sourceid_broker.register_source_id("HW_Signals_Interface", timing_hsi_source_id, None) the_system.apps["timinghsi"] = get_timing_hsi_app( - detector = detector, hsi = hsi, + detector = detector, source_id = timing_hsi_source_id, - - # CLOCK_SPEED_HZ = detector.clock_speed_hz, - # TRIGGER_RATE_HZ = trigger.trigger_rate_hz, - # CONTROL_HSI_HARDWARE=hsi.control_hsi_hw, - # CONNECTIONS_FILE=hsi.hsi_hw_connections_file, - # READOUT_PERIOD_US = hsi.hsi_readout_period, - # HSI_DEVICE_NAME = hsi.hsi_device_name, - # HARDWARE_STATE_RECOVERY_ENABLED = hsi.enable_hardware_state_recovery, - # HSI_ENDPOINT_ADDRESS = hsi.hsi_endpoint_address, - # HSI_ENDPOINT_PARTITION = hsi.hsi_endpoint_partition, - # HSI_RE_MASK=hsi.hsi_re_mask, - # HSI_FE_MASK=hsi.hsi_fe_mask, - # HSI_INV_MASK=hsi.hsi_inv_mask, - # HSI_SOURCE=hsi.hsi_source, - # HSI_SOURCE_ID=timing_hsi_source_id, - # TIMING_SESSION=timing.timing_session_name, - # HOST=hsi.host_timing_hsi, DEBUG=debug ) if debug: console.log("timing hsi cmd data:", the_system.apps["timinghsi"]) - # + #-------------------------------------------------------------------------- # Fake HSI - # + #-------------------------------------------------------------------------- if hsi.use_fake_hsi: fake_hsi_source_id = sourceid_broker.get_next_source_id("HW_Signals_Interface") sourceid_broker.register_source_id("HW_Signals_Interface", fake_hsi_source_id, None) the_system.apps["fakehsi"] = get_fake_hsi_app( - detector = detector, hsi = hsi, + detector = detector, daq_common = daq_common, source_id = fake_hsi_source_id, - - # CLOCK_SPEED_HZ = detector.clock_speed_hz, - # DATA_RATE_SLOWDOWN_FACTOR = daq_common.data_rate_slowdown_factor, - # TRIGGER_RATE_HZ = trigger.trigger_rate_hz, - # HSI_SOURCE_ID=fake_hsi_source_id, - # MEAN_SIGNAL_MULTIPLICITY = hsi.mean_hsi_signal_multiplicity, - # SIGNAL_EMULATION_MODE = hsi.hsi_signal_emulation_mode, - # ENABLED_SIGNALS = hsi.enabled_hsi_signals, - # HOST=hsi.host_fake_hsi, DEBUG=debug) if debug: console.log("fake hsi cmd data:", the_system.apps["fakehsi"]) # the_system.apps["hsi"] = util.App(modulegraph=mgraph_hsi, host=hsi.host_hsi) + #-------------------------------------------------------------------------- + # Timing controller + #-------------------------------------------------------------------------- if timing.control_timing_partition: the_system.apps["tprtc"] = get_tprtc_app( timing, - # MASTER_DEVICE_NAME=timing.timing_partition_master_device_name, - # TIMING_PARTITION_ID=timing.timing_partition_id, - # TRIGGER_MASK=timing.timing_partition_trigger_mask, - # RATE_CONTROL_ENABLED=timing.timing_partition_rate_control_enabled, - # SPILL_GATE_ENABLED=timing.timing_partition_spill_gate_enabled, - # TIMING_SESSION=timing.timing_session_name, - # HOST=timing.host_tprtc, DEBUG=debug ) + #-------------------------------------------------------------------------- + # Trigger + #-------------------------------------------------------------------------- the_system.apps['trigger'] = get_trigger_app( - trigger, - detector, - daq_common, - tp_infos, - trigger_data_request_timeout, - # DATA_RATE_SLOWDOWN_FACTOR = daq_common.data_rate_slowdown_factor, - # CLOCK_SPEED_HZ = detector.clock_speed_hz, - # TP_CONFIG = tp_infos, - # TOLERATE_INCOMPLETENESS=trigger.tolerate_incompleteness, - # COMPLETENESS_TOLERANCE=trigger.completeness_tolerance, - # ACTIVITY_PLUGIN = trigger.trigger_activity_plugin, - # ACTIVITY_CONFIG = trigger.trigger_activity_config, - # CANDIDATE_PLUGIN = trigger.trigger_candidate_plugin, - # CANDIDATE_CONFIG = trigger.trigger_candidate_config, - # TTCM_S1=trigger.ttcm_s1, - # TTCM_S2=trigger.ttcm_s2, - # TRIGGER_WINDOW_BEFORE_TICKS = trigger.trigger_window_before_ticks, - # TRIGGER_WINDOW_AFTER_TICKS = trigger.trigger_window_after_ticks, - # HSI_TRIGGER_TYPE_PASSTHROUGH = trigger.hsi_trigger_type_passthrough, - # MLT_MERGE_OVERLAPPING_TCS = trigger.mlt_merge_overlapping_tcs, - # MLT_BUFFER_TIMEOUT = trigger.mlt_buffer_timeout, - # MLT_MAX_TD_LENGTH_MS = trigger.mlt_max_td_length_ms, - # MLT_SEND_TIMED_OUT_TDS = trigger.mlt_send_timed_out_tds, - # MLT_IGNORE_TC = trigger.mlt_ignore_tc, - # MLT_USE_READOUT_MAP = trigger.mlt_use_readout_map, - # MLT_READOUT_MAP = trigger.mlt_td_readout_map, - # USE_CUSTOM_MAKER = trigger.use_custom_maker, - # CTCM_TYPES = trigger.ctcm_trigger_types, - # CTCM_INTERVAL = trigger.ctcm_trigger_intervals, - # CHANNEL_MAP_NAME = detector.tpg_channel_map, - # DATA_REQUEST_TIMEOUT=trigger_data_request_timeout, - # HOST=trigger.host_trigger, + trigger=trigger, + detector=detector, + daq_common=daq_common, + tp_infos=tp_infos, + trigger_data_request_timeout=trigger_data_request_timeout, DEBUG=debug) + #-------------------------------------------------------------------------- + # DFO + #-------------------------------------------------------------------------- the_system.apps['dfo'] = get_dfo_app( FREE_COUNT = max(1, dataflow.token_count / 2), BUSY_COUNT = dataflow.token_count, @@ -469,23 +514,25 @@ def cli( ru_app_names=[] dqm_app_names = [] - # - # Readout applications generatioo - # + #-------------------------------------------------------------------------- + # Readout generatioo + #-------------------------------------------------------------------------- roapp_gen = ReadoutAppGenerator(readout, detector, daq_common) for ru_i,(ru_name, ru_desc) in enumerate(ru_descs.items()): + #-------------------------------------------------------------------------- + # Readout applications + #-------------------------------------------------------------------------- if readout.use_fake_data_producers == False: the_system.apps[ru_name] = roapp_gen.generate( RU_DESCRIPTOR=ru_desc, SOURCEID_BROKER=sourceid_broker, data_file_map=data_file_map, - tpg_channel_map=detector.tpg_channel_map, data_timeout_requests=readout_data_request_timeout ) else: - the_system.apps[ru_name] = create_fake_reaout_app( + the_system.apps[ru_name] = create_fake_readout_app( RU_DESCRIPTOR = ru_desc, CLOCK_SPEED_HZ = detector.clock_speed_hz, ) @@ -494,6 +541,9 @@ def cli( if debug: console.log(f"{ru_name} app: {the_system.apps[ru_name]}") + #-------------------------------------------------------------------------- + # DQM frontend applications + #-------------------------------------------------------------------------- if dqm.enable_dqm: dqm_name = "dqm" + ru_name dqm_app_names.append(dqm_name) @@ -507,25 +557,29 @@ def cli( # ru_name_with_underscore = f"ru{host}_{ru_id.iface}" the_system.apps[dqm_name] = get_dqm_app( - DQM_IMPL=dqm.impl, - DATA_RATE_SLOWDOWN_FACTOR=daq_common.data_rate_slowdown_factor, - CLOCK_SPEED_HZ=detector.clock_speed_hz, - MAX_NUM_FRAMES=dqm.max_num_frames, + dqm=dqm, + detector=detector, + daq_common=daq_common, + # DQM_IMPL=dqm.impl, + # DATA_RATE_SLOWDOWN_FACTOR=daq_common.data_rate_slowdown_factor, + # CLOCK_SPEED_HZ=detector.clock_speed_hz, + # MAX_NUM_FRAMES=dqm.max_num_frames, DQMIDX = ru_i, - KAFKA_ADDRESS=dqm.kafka_address, - KAFKA_TOPIC=dqm.kafka_topic, - CMAP=dqm.cmap, - RAW_PARAMS=dqm.raw_params, - RMS_PARAMS=dqm.rms_params, - STD_PARAMS=dqm.std_params, - FOURIER_CHANNEL_PARAMS=dqm.fourier_channel_params, - FOURIER_PLANE_PARAMS=dqm.fourier_plane_params, + # KAFKA_ADDRESS=dqm.kafka_address, + # KAFKA_TOPIC=dqm.kafka_topic, + # CMAP=dqm.cmap, + # RAW_PARAMS=dqm.raw_params, + # RMS_PARAMS=dqm.rms_params, + # STD_PARAMS=dqm.std_params, + # FOURIER_CHANNEL_PARAMS=dqm.fourier_channel_params, + # FOURIER_PLANE_PARAMS=dqm.fourier_plane_params, LINKS=dqm_links, RU_APPNAME=ru_name, TRB_DQM_SOURCEID_OFFSET=trb_dqm_sourceid_offset, - HOST=dqm.host_dqm[ru_i % len(dqm.host_dqm)], + # HOST=dqm.host_dqm[ru_i % len(dqm.host_dqm)], RU_STREAMS=ru_desc.streams, - DEBUG=debug) + DEBUG=debug + ) if debug: console.log(f"{dqm_name} app: {the_system.apps[dqm_name]}") @@ -533,35 +587,24 @@ def cli( idx = 0 - # + #-------------------------------------------------------------------------- # Dataflow applications generatioo - # + #-------------------------------------------------------------------------- for app_name,df_config in appconfig_df.items(): dfidx = df_config.source_id the_system.apps[app_name] = get_dataflow_app( + df_config = df_config, + dataflow = dataflow, + detector = detector, HOSTIDX=dfidx, - OUTPUT_PATHS = df_config.output_paths, APP_NAME=app_name, - OPERATIONAL_ENVIRONMENT = detector.op_env, FILE_LABEL = file_label, - DATA_STORE_MODE=df_config.data_store_mode, - MAX_FILE_SIZE = df_config.max_file_size, - MAX_TRIGGER_RECORD_WINDOW = df_config.max_trigger_record_window, MAX_EXPECTED_TR_SEQUENCES = max_expected_tr_sequences, - TOKEN_COUNT = dataflow.token_count, TRB_TIMEOUT = trigger_record_building_timeout, - HOST=df_config.host_df, HAS_DQM=dqm.enable_dqm, SRC_GEO_ID_MAP=dro_map.get_src_geo_map(), DEBUG=debug ) - if use_k8s: - the_system.apps[app_name].mounted_dirs += [{ - 'name': f'raw-data-{i}', - 'physical_location': opath, - 'in_pod_location': opath, - 'read_only': False, - } for i,opath in enumerate(set(df_config.output_paths))] if dqm.enable_dqm: @@ -588,50 +631,44 @@ def cli( DF_RATE=dqm.df_rate * len(host_df), DF_ALGS=dqm.df_algs, DF_TIME_WINDOW=trigger.trigger_window_before_ticks + trigger.trigger_window_after_ticks, - # DRO_CONFIG=dro_config, # This is coming from the readout loop RU_STREAMS=ru_desc.streams, # This is coming from the readout loop DEBUG=debug) if debug: console.log(f"{dqm_name} app: {the_system.apps[dqm_name]}") idx += 1 - # - # TPSet Writer applications generatioo - # + #-------------------------------------------------------------------------- + # TPSet Writer applications generation + #-------------------------------------------------------------------------- if dataflow.enable_tpset_writing: tpw_name=f'tpwriter' dfidx = sourceid_broker.get_next_source_id("TRBuilder") sourceid_broker.register_source_id("TRBuilder", dfidx, None) the_system.apps[tpw_name] = get_tpwriter_app( - OUTPUT_PATH = dataflow.tpset_output_path, - APP_NAME = tpw_name, - OPERATIONAL_ENVIRONMENT = detector.op_env, - FILE_LABEL = file_label, - MAX_FILE_SIZE = dataflow.tpset_output_file_size, - DATA_RATE_SLOWDOWN_FACTOR = daq_common.data_rate_slowdown_factor, - CLOCK_SPEED_HZ = detector.clock_speed_hz, - SRC_GEO_ID_MAP=dro_map.get_src_geo_map(), - SOURCE_IDX=dfidx, - HOST=trigger.host_tpw, - DEBUG=debug) - if use_k8s: ## TODO schema - the_system.apps[tpw_name].mounted_dirs += [{ - 'name': 'raw-data', - 'physical_location':dataflow.tpset_output_path, - 'in_pod_location':dataflow.tpset_output_path, - 'read_only': False - }] + dataflow=dataflow, + detector=detector, + daq_common=daq_common, + app_name=tpw_name, + file_label=file_label, + source_id=dfidx, + DEBUG=debug + ) if debug: console.log(f"{tpw_name} app: {the_system.apps[tpw_name]}") - all_apps_except_ru = [] - all_apps_except_ru_and_df = [] if dpdk_sender.enable_dpdk_sender: the_system.apps["dpdk_sender"] = get_dpdk_sender_app( HOST=dpdk_sender.host_dpdk_sender[0], ) + #-------------------------------------------------------------------------- + # App generation completed + #-------------------------------------------------------------------------- + + all_apps_except_ru = [] + all_apps_except_ru_and_df = [] + for name,app in the_system.apps.items(): if app.name=="__app": app.name=name @@ -663,19 +700,6 @@ def cli( if debug: console.log(f"After set_mlt_links, mlt_links is {mlt_links}") - # HACK HACK HACK P. Rodrigues 2022-03-04 We decided not to request - # TPs from readout for the 2.10 release. It would be nice to - # achieve this by just not adding fragment producers for the - # relevant links in readout_gen.py, but then the necessary input - # and output queues for the DataLinkHandler modules are not - # created. So instead we do it this roundabout way: the fragment - # producers are all created, they are added to the MLT's list of - # links to read out from (in set_mlt_links above), and then - # removed here. We rely on a convention that TP links have element - # value >= 1000. - # - # This code should be removed after 2.10, when we will have - # decided how to handle raw TP data as fragments mlt_links=the_system.apps["trigger"].modulegraph.get_module("mlt").conf.links if debug: @@ -696,31 +720,22 @@ def cli( } ################################################################################## - # Make boot.json config from daqconf.core.conf_utils import make_system_command_datas, write_json_files # HACK: Make sure RUs start after trigger forced_deps = [] - # for i,host in enumerate(ru_confs): - # ru_name = ru_app_names[i] for name in ru_app_names: forced_deps.append(['hsi', ru_name]) if dataflow.enable_tpset_writing: forced_deps.append(['tpwriter', ru_name]) if dqm.enable_dqm: - # for i,host in enumerate(ru_confs): - # dqm_name = dqm_app_names[i] for dqm_name in dqm_app_names: forced_deps.append([dqm_name, 'dfo']) - # for i,host in enumerate(host_df): - # dqm_name = dqm_df_app_names[i] - - for dqm_name in dqm_df_app_names: forced_deps.append([dqm_name, 'dfo']) @@ -756,7 +771,6 @@ def cli( if readout.thread_pinning_file != "": - resolved_thread_pinning_file = Path(os.path.expandvars(readout.thread_pinning_file)).expanduser() if not resolved_thread_pinning_file.is_absolute(): resolved_thread_pinning_file = config_file.parent / resolved_thread_pinning_file @@ -779,6 +793,8 @@ def cli( if not dry_run: + import dunedaq.daqconf.confgen as confgen + write_json_files(app_command_datas, system_command_datas, output_dir, verbose=debug) console.log(f"MDAapp config generated in {output_dir}") From 3e16f0703a8f7c0b90705160fe1e25e02f8733a0 Mon Sep 17 00:00:00 2001 From: Alessandro Thea Date: Sun, 18 Jun 2023 23:16:33 +0200 Subject: [PATCH 25/90] Rolling back endpoint changes --- python/daqconf/core/conf_utils.py | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/python/daqconf/core/conf_utils.py b/python/daqconf/core/conf_utils.py index 107b6522..68d449a9 100644 --- a/python/daqconf/core/conf_utils.py +++ b/python/daqconf/core/conf_utils.py @@ -200,12 +200,10 @@ def make_network_connection(the_system, endpoint_name, data_type, in_apps, out_a if len(in_apps) > 1: raise ValueError(f"Connection with name {endpoint_name} has multiple receivers, which is unsupported for a network connection!") - sender_app = in_apps[0] port = the_system.next_unassigned_port() if not use_connectivity_service or use_k8s else '*' - address_sender = f'tcp://{{{sender_app}}}:{port}' if not use_k8s else f'tcp://{sender_app}:{port}' - conn_uid = f"{sender_app}.{endpoint_name}" - conn_id = conn.ConnectionId(uid=conn_uid, data_type=data_type) - the_system.connections[sender_app] += [conn.Connection(id=conn_id, connection_type="kSendRecv", uri=address_sender)] + address_sender = f'tcp://{{{in_apps[0]}}}:{port}' if not use_k8s else f'tcp://{in_apps[0]}:{port}' + conn_id = conn.ConnectionId(uid=endpoint_name, data_type=data_type) + the_system.connections[in_apps[0]] += [conn.Connection(id=conn_id, connection_type="kSendRecv", uri=address_sender)] if not use_connectivity_service: for app in set(out_apps): the_system.connections[app] += [conn.Connection(id=conn_id, connection_type="kSendRecv", uri=address_sender)] @@ -319,18 +317,17 @@ def make_system_connections(the_system, verbose=False, use_k8s=False, use_connec subscribers += [endpoint["app"]] else: publishers += [endpoint["app"]] - conn_uid = f"{endpoint['app']}.{endpoint['endpoint'].external_name}" - if conn_uid not in pubsub_connectionids: + if endpoint['endpoint'].external_name not in pubsub_connectionids: port = the_system.next_unassigned_port() if not use_connectivity_service or use_k8s else '*' address = f'tcp://{{{endpoint["app"]}}}:{port}' if not use_k8s else f'tcp://{endpoint["app"]}:{port}' - conn_id =conn.ConnectionId( uid=conn_uid, data_type=endpoint['endpoint'].data_type) - pubsub_connectionids[conn_uid] = conn.Connection(id=conn_id, + conn_id =conn.ConnectionId( uid=endpoint['endpoint'].external_name, data_type=endpoint['endpoint'].data_type) + pubsub_connectionids[endpoint['endpoint'].external_name] = conn.Connection(id=conn_id, connection_type="kPubSub", uri=address ) - topic_connectionuids += [conn_uid] + topic_connectionuids += [endpoint['endpoint'].external_name] if endpoint['app'] not in publisher_uids.keys(): publisher_uids[endpoint["app"]] = [] - publisher_uids[endpoint["app"]] += [conn_uid] + publisher_uids[endpoint["app"]] += [endpoint['endpoint'].external_name] if len(subscribers) == 0 and check_endpoints: raise ValueError(f"Data Type {topic} has no subscribers!") From 2b6d084d71836929b45e232b8575dfeff0ecf70f Mon Sep 17 00:00:00 2001 From: Kurt Biery Date: Sun, 18 Jun 2023 16:54:07 -0500 Subject: [PATCH 26/90] very preliminary changes to get the FD-specific build to run successfully; these changes may need to be changed to make them more robust, appropriate, etc. --- python/daqconf/apps/dqm_gen.py | 4 +-- python/daqconf/apps/readout_gen.py | 54 +++++++++++++++--------------- 2 files changed, 29 insertions(+), 29 deletions(-) diff --git a/python/daqconf/apps/dqm_gen.py b/python/daqconf/apps/dqm_gen.py index b358ecaf..e8198728 100644 --- a/python/daqconf/apps/dqm_gen.py +++ b/python/daqconf/apps/dqm_gen.py @@ -65,8 +65,8 @@ def get_dqm_app(DQM_IMPL='', FRONTEND_TYPE = "pds_list" elif FRONTEND_TYPE== "VD_Top_TPC": FRONTEND_TYPE = "tde" - elif FRONTEND_TYPE== "ND_LAr": - FRONTEND_TYPE = "pacman" + #elif FRONTEND_TYPE== "ND_LAr": + # FRONTEND_TYPE = "pacman" if DQM_IMPL == 'cern': KAFKA_ADDRESS = "monkafka.cern.ch:30092" diff --git a/python/daqconf/apps/readout_gen.py b/python/daqconf/apps/readout_gen.py index e13653ef..8d24d744 100644 --- a/python/daqconf/apps/readout_gen.py +++ b/python/daqconf/apps/readout_gen.py @@ -18,7 +18,7 @@ # moo.otypes.load_types('dtpctrellibs/dtpcontroller.jsonnet') moo.otypes.load_types('readoutlibs/sourceemulatorconfig.jsonnet') moo.otypes.load_types('readoutlibs/readoutconfig.jsonnet') -moo.otypes.load_types('lbrulibs/pacmancardreader.jsonnet') +#moo.otypes.load_types('lbrulibs/pacmancardreader.jsonnet') moo.otypes.load_types('dfmodules/fakedataprod.jsonnet') moo.otypes.load_types("dpdklibs/nicreader.jsonnet") @@ -32,7 +32,7 @@ import dunedaq.flxlibs.felixcardreader as flxcr # import dunedaq.dtpctrllibs.dtpcontroller as dtpctrl import dunedaq.readoutlibs.readoutconfig as rconf -import dunedaq.lbrulibs.pacmancardreader as pcr +#import dunedaq.lbrulibs.pacmancardreader as pcr # import dunedaq.dfmodules.triggerrecordbuilder as trb import dunedaq.dfmodules.fakedataprod as fdp import dunedaq.dpdklibs.nicreader as nrc @@ -98,19 +98,19 @@ def compute_data_types( queue_frag_type = "TDEFrame" fakedata_time_tick=4472*32 fakedata_frame_size=8972 - # Near detector types - elif det_str == "NDLAr_TPC": - fe_type = "pacman" - fakedata_frag_type = "PACMAN" - queue_frag_type = "PACMANFrame" - fakedata_time_tick=None - fakedata_frame_size=None - elif det_str == "NDLAr_PDS": - fe_type = "mpd" - fakedata_frag_type = "MPD" - queue_frag_type = "MPDFrame" - fakedata_time_tick=None - fakedata_frame_size=None + ## Near detector types + #elif det_str == "NDLAr_TPC": + # fe_type = "pacman" + # fakedata_frag_type = "PACMAN" + # queue_frag_type = "PACMANFrame" + # fakedata_time_tick=None + # fakedata_frame_size=None + #elif det_str == "NDLAr_PDS": + # fe_type = "mpd" + # fakedata_frag_type = "MPD" + # queue_frag_type = "MPDFrame" + # fakedata_time_tick=None + # fakedata_frame_size=None else: raise ValueError(f"No match for {det_str}, {clk_freq_hz}, {kind}") @@ -155,7 +155,7 @@ def create_fake_cardreader( modules = [DAQModule(name = "fake_source", - plugin = "FakeCardReader", + plugin = "FDFakeCardReader", conf = conf)] queues = [ Queue( @@ -431,15 +431,15 @@ def create_pacman_cardreader( Create a Pacman Cardeader """ - reader_name = "nd_reader" - if FRONTEND_TYPE == 'pacman': - reader_name = "pacman_source" - - elif FRONTEND_TYPE == 'mpd': - reader_name = "mpd_source" - - else: - raise RuntimeError(f"Pacman Cardreader for {FRONTEND_TYPE} not supported") + #reader_name = "nd_reader" + #if FRONTEND_TYPE == 'pacman': + # reader_name = "pacman_source" +# +# elif FRONTEND_TYPE == 'mpd': +# reader_name = "mpd_source" +# +# else: +# raise RuntimeError(f"Pacman Cardreader for {FRONTEND_TYPE} not supported") modules = [DAQModule( name=reader_name, @@ -493,7 +493,7 @@ def create_det_dhl( geo_id = stream.geo_id modules += [DAQModule( name = f"datahandler_{stream.src_id}", - plugin = "DataLinkHandler", + plugin = "FDDataLinkHandler", conf = rconf.Conf( readoutmodelconf= rconf.ReadoutModelConf( source_queue_timeout_ms= QUEUE_POP_WAIT_MS, @@ -611,7 +611,7 @@ def create_tp_dlhs( # Create the TP link handler modules = [ DAQModule(name = f"tp_datahandler_{tpset_sid}", - plugin = "DataLinkHandler", + plugin = "FDDataLinkHandler", conf = rconf.Conf( readoutmodelconf = rconf.ReadoutModelConf( source_queue_timeout_ms = QUEUE_POP_WAIT_MS, From b04aca827742c198f190d013d2e63ce3817dc056 Mon Sep 17 00:00:00 2001 From: Michal Rigan Date: Mon, 19 Jun 2023 07:38:43 -0500 Subject: [PATCH 27/90] mlt trigger bitwords changes --- python/daqconf/apps/trigger_gen.py | 24 +++++++++--------------- schema/daqconf/confgen.jsonnet | 4 ++-- 2 files changed, 11 insertions(+), 17 deletions(-) diff --git a/python/daqconf/apps/trigger_gen.py b/python/daqconf/apps/trigger_gen.py index ceb6d61b..46a86ed7 100644 --- a/python/daqconf/apps/trigger_gen.py +++ b/python/daqconf/apps/trigger_gen.py @@ -74,26 +74,20 @@ def get_buffer_conf(source_id, data_request_timeout): enable_raw_recording = False)) #=============================================================================== +### Function that converts trigger word strings to trigger word integers given TC type. Uses functions from trgdataformats. def get_trigger_bitwords(bitwords): - count_bitwords = 0 - count_flags = 0 - # process map - map_bits = trgbs.get_trigger_candidate_type_names() # create bitwords flags final_bit_flags = [] for bitword in bitwords: - tmp_bit = [] + tmp_bits = [] for bit_name in bitword: - count_bitwords += 1 - for map_bit in map_bits: - if bit_name == map_bit.name: - tmp_bit.append(map_bit.value) - count_flags +=1 - break - final_bit_flags.append(tmp_bit) - if (count_bitwords != count_flags): - raise RuntimeError(f'One or more of provided MLT trigger bitwords is incorrect! Please recheck the names...') - + bit_value = trgbs.string_to_fragment_type_value(bit_name) + if bit_value == -1: + raise RuntimeError(f'One or more of provided MLT trigger bitwords is incorrect! Please recheck the names...') + else: + tmp_bits.append(bit_value) + final_bit_flags.append(tmp_bits) + return final_bit_flags #=============================================================================== diff --git a/schema/daqconf/confgen.jsonnet b/schema/daqconf/confgen.jsonnet index 6915ca87..4c5bd44f 100755 --- a/schema/daqconf/confgen.jsonnet +++ b/schema/daqconf/confgen.jsonnet @@ -31,8 +31,8 @@ local cs = { tc_intervals: s.sequence( "TCIntervals", self.tc_interval, doc="List of TC intervals used by CTCM"), readout_time: s.number( "ROTime", "i8", doc="A readout time in ticks"), channel_list: s.sequence( "ChannelList", self.count, doc="List of offline channels to be masked out from the TPHandler"), - bitword: s.string( "Bitword", doc="123"), - bitword_list: s.sequence( "BitwordList", self.bitword, doc="123"), + bitword: s.string( "Bitword", doc="A string representing the TC type name, to be set in the trigger bitword."), + bitword_list: s.sequence( "BitwordList", self.bitword, doc="A sequence of bitword (TC type bits) forming a bitword."), bitwords: s.sequence( "Bitwords", self.bitword_list, doc="List of bitwords to use when forming trigger decisions in MLT" ), numa_exception: s.record( "NUMAException", [ From d50cbfb1b3d51357b8a2dfe02ef059c91818477c Mon Sep 17 00:00:00 2001 From: Kurt Biery Date: Tue, 20 Jun 2023 09:05:21 -0500 Subject: [PATCH 28/90] Added comments to each of the places that were changed so that we can find them later. --- python/daqconf/apps/dqm_gen.py | 1 + python/daqconf/apps/readout_gen.py | 41 +++++++++++++++++------------- 2 files changed, 25 insertions(+), 17 deletions(-) diff --git a/python/daqconf/apps/dqm_gen.py b/python/daqconf/apps/dqm_gen.py index e8198728..fddedada 100644 --- a/python/daqconf/apps/dqm_gen.py +++ b/python/daqconf/apps/dqm_gen.py @@ -65,6 +65,7 @@ def get_dqm_app(DQM_IMPL='', FRONTEND_TYPE = "pds_list" elif FRONTEND_TYPE== "VD_Top_TPC": FRONTEND_TYPE = "tde" + # 20-Jun-2023, KAB: quick fix to get FD-specific nightly build to run #elif FRONTEND_TYPE== "ND_LAr": # FRONTEND_TYPE = "pacman" diff --git a/python/daqconf/apps/readout_gen.py b/python/daqconf/apps/readout_gen.py index 8d24d744..bb9d80dc 100644 --- a/python/daqconf/apps/readout_gen.py +++ b/python/daqconf/apps/readout_gen.py @@ -18,6 +18,7 @@ # moo.otypes.load_types('dtpctrellibs/dtpcontroller.jsonnet') moo.otypes.load_types('readoutlibs/sourceemulatorconfig.jsonnet') moo.otypes.load_types('readoutlibs/readoutconfig.jsonnet') +# 20-Jun-2023, KAB: quick fix to get FD-specific nightly build to run #moo.otypes.load_types('lbrulibs/pacmancardreader.jsonnet') moo.otypes.load_types('dfmodules/fakedataprod.jsonnet') moo.otypes.load_types("dpdklibs/nicreader.jsonnet") @@ -32,6 +33,7 @@ import dunedaq.flxlibs.felixcardreader as flxcr # import dunedaq.dtpctrllibs.dtpcontroller as dtpctrl import dunedaq.readoutlibs.readoutconfig as rconf +# 20-Jun-2023, KAB: quick fix to get FD-specific nightly build to run #import dunedaq.lbrulibs.pacmancardreader as pcr # import dunedaq.dfmodules.triggerrecordbuilder as trb import dunedaq.dfmodules.fakedataprod as fdp @@ -98,6 +100,7 @@ def compute_data_types( queue_frag_type = "TDEFrame" fakedata_time_tick=4472*32 fakedata_frame_size=8972 + # 20-Jun-2023, KAB: quick fix to get FD-specific nightly build to run ## Near detector types #elif det_str == "NDLAr_TPC": # fe_type = "pacman" @@ -155,6 +158,7 @@ def create_fake_cardreader( modules = [DAQModule(name = "fake_source", + # 20-Jun-2023, KAB: quick fix to get FD-specific nightly build to run plugin = "FDFakeCardReader", conf = conf)] queues = [ @@ -431,15 +435,15 @@ def create_pacman_cardreader( Create a Pacman Cardeader """ - #reader_name = "nd_reader" - #if FRONTEND_TYPE == 'pacman': - # reader_name = "pacman_source" -# -# elif FRONTEND_TYPE == 'mpd': -# reader_name = "mpd_source" -# -# else: -# raise RuntimeError(f"Pacman Cardreader for {FRONTEND_TYPE} not supported") + reader_name = "nd_reader" + if FRONTEND_TYPE == 'pacman': + reader_name = "pacman_source" + + elif FRONTEND_TYPE == 'mpd': + reader_name = "mpd_source" + + else: + raise RuntimeError(f"Pacman Cardreader for {FRONTEND_TYPE} not supported") modules = [DAQModule( name=reader_name, @@ -493,6 +497,7 @@ def create_det_dhl( geo_id = stream.geo_id modules += [DAQModule( name = f"datahandler_{stream.src_id}", + # 20-Jun-2023, KAB: quick fix to get FD-specific nightly build to run plugin = "FDDataLinkHandler", conf = rconf.Conf( readoutmodelconf= rconf.ReadoutModelConf( @@ -611,6 +616,7 @@ def create_tp_dlhs( # Create the TP link handler modules = [ DAQModule(name = f"tp_datahandler_{tpset_sid}", + # 20-Jun-2023, KAB: quick fix to get FD-specific nightly build to run plugin = "FDDataLinkHandler", conf = rconf.Conf( readoutmodelconf = rconf.ReadoutModelConf( @@ -824,15 +830,16 @@ def create_readout_app( cr_mods += dpdk_mods cr_queues += dpdk_queues - elif RU_DESCRIPTOR.kind == 'eth' and RU_DESCRIPTOR.streams[0].parameters.protocol == "zmq": + # 20-Jun-2023, KAB: quick fix to get FD-specific nightly build to run + #elif RU_DESCRIPTOR.kind == 'eth' and RU_DESCRIPTOR.streams[0].parameters.protocol == "zmq": - pac_mods, pac_queues = create_pacman_cardreader( - FRONTEND_TYPE=FRONTEND_TYPE, - QUEUE_FRAGMENT_TYPE=QUEUE_FRAGMENT_TYPE, - RU_DESCRIPTOR=RU_DESCRIPTOR - ) - cr_mods += pac_mods - cr_queues += pac_queues + # pac_mods, pac_queues = create_pacman_cardreader( + # FRONTEND_TYPE=FRONTEND_TYPE, + # QUEUE_FRAGMENT_TYPE=QUEUE_FRAGMENT_TYPE, + # RU_DESCRIPTOR=RU_DESCRIPTOR + # ) + # cr_mods += pac_mods + # cr_queues += pac_queues modules += cr_mods queues += cr_queues From d41102973994637b6d798a1b9611aa7f80e7cbc2 Mon Sep 17 00:00:00 2001 From: Jonathan Hancock Date: Wed, 21 Jun 2023 13:43:59 +0200 Subject: [PATCH 29/90] The JSON is now displayed in a Tree widget, meaning nodes can be expanded and collapsed for clarity. Temporary background colours removed. --- scripts/daqconf_viewer | 56 ++++++++++++++++++++++++++++++++------ scripts/daqconf_viewer.css | 2 -- 2 files changed, 47 insertions(+), 11 deletions(-) diff --git a/scripts/daqconf_viewer b/scripts/daqconf_viewer index 4ef514b0..f88e8ba9 100644 --- a/scripts/daqconf_viewer +++ b/scripts/daqconf_viewer @@ -1,12 +1,15 @@ import asyncio import httpx import json +import sys + +from rich.text import Text from textual import log, events from textual.app import App, ComposeResult from textual.containers import Horizontal, Content, Container, Vertical from textual.widget import Widget -from textual.widgets import Button, Header, Footer, Static, Input, Label, ListView, ListItem +from textual.widgets import Button, Header, Footer, Static, Input, Label, ListView, ListItem, Tree from textual.reactive import reactive, Reactive from textual.message import Message, MessageTarget from textual.screen import Screen @@ -37,7 +40,8 @@ class Configs(Static): async def update_configs(self) -> None: async with httpx.AsyncClient() as client: r = await client.get(f'{self.hostname}/listConfigs', auth=auth, timeout=60) - self.conflist = r.json()['configs'] + unsorted = r.json()['configs'] + self.conflist = sorted(unsorted, key=str.lower) def watch_conflist(self, conflist:list[str]): label_list = [LabelItem(c) for c in conflist] @@ -100,27 +104,61 @@ class Display(Vertical): self.version = None def compose(self) -> ComposeResult: - yield Static() + yield Tree("Root") async def get_json(self, conf, ver) -> None: self.confname = conf self.version = ver - if self.confname and self.version: + if self.confname != None and self.version != None: async with httpx.AsyncClient() as client: payload = {'name': self.confname, 'version': self.version} r = await client.get(f'{self.hostname}/retrieveVersion', auth=auth, params=payload, timeout=60) self.confdata = r.json() - #TODO the output could possibly be made nice with rich.print_json. It should also be able to collapse sections. + def json_into_tree(cls, node, json_data): + """Takes a JSON, and puts it into the tree.""" + from rich.highlighter import ReprHighlighter + + highlighter = ReprHighlighter() + + def add_node(name, node, data) -> None: + """Adds a node to the tree. + Args: + name (str): Name of the node. + node (TreeNode): Parent node. + data (object): Data associated with the node. + """ + if isinstance(data, dict): + node.set_label(Text(f"{{}} {name}")) + for key, value in data.items(): + new_node = node.add("") + add_node(key, new_node, value) + elif isinstance(data, list): + node.set_label(Text(f"[] {name}")) + for index, value in enumerate(data): + new_node = node.add("") + add_node(str(index), new_node, value) + else: + node.allow_expand = False + if name: + label = Text.assemble( + Text.from_markup(f"[b]{name}[/b]="), highlighter(repr(data)) + ) + else: + label = Text(repr(data)) + node.set_label(label) + + add_node("", node, json_data) + def watch_confdata(self, confdata:dict) -> None: - box = self.query_one(Static) - json_str = json.dumps(confdata, indent=2) - box.update(json_str) + tree = self.query_one(Tree) + tree.clear() + self.json_into_tree(tree.root, confdata) + tree.root.expand() class ConfViewer(App): CSS_PATH = "daqconf_viewer.css" - #BINDINGS = [("d", "toggle_dark", "Toggle dark mode")] def __init__(self, **kwargs): super().__init__(**kwargs) diff --git a/scripts/daqconf_viewer.css b/scripts/daqconf_viewer.css index 63642cb4..930a696a 100644 --- a/scripts/daqconf_viewer.css +++ b/scripts/daqconf_viewer.css @@ -15,14 +15,12 @@ Screen { row-span: 2; column-span: 3; height: 100%; - background: green; } #display { row-span: 8; column-span: 3; height: 100%; - background: purple; overflow-y: auto; } From 5d7355ede0584c742d2f8bbc13333dfcf1d4097b Mon Sep 17 00:00:00 2001 From: Jonathan Hancock Date: Wed, 21 Jun 2023 15:48:10 +0200 Subject: [PATCH 30/90] Made daqconf_viewer executable --- scripts/daqconf_viewer | 15 ++++++++++++++- scripts/daqconf_viewer.css | 7 ++++++- 2 files changed, 20 insertions(+), 2 deletions(-) mode change 100644 => 100755 scripts/daqconf_viewer diff --git a/scripts/daqconf_viewer b/scripts/daqconf_viewer old mode 100644 new mode 100755 index f88e8ba9..3ccabf11 --- a/scripts/daqconf_viewer +++ b/scripts/daqconf_viewer @@ -1,8 +1,10 @@ +#!/usr/bin/env python3 import asyncio import httpx import json import sys +#from json_diff import Comparator from rich.text import Text from textual import log, events @@ -104,7 +106,8 @@ class Display(Vertical): self.version = None def compose(self) -> ComposeResult: - yield Tree("Root") + yield Tree("Root", id='conftree') + yield Container(Button("Get Diff", id='diff_btn', variant='primary')) async def get_json(self, conf, ver) -> None: self.confname = conf @@ -156,9 +159,19 @@ class Display(Vertical): self.json_into_tree(tree.root, confdata) tree.root.expand() + def on_button_pressed(self) -> None: + app.push_screen(DiffScreen()) + +class DiffScreen(Screen): + pass class ConfViewer(App): CSS_PATH = "daqconf_viewer.css" + #For some reason, importing json_diff makes these two modules start printing to stdout when a HTTP request is sent. + #The following code supresses all non-critical messages (hopefully all of them). + import logging + logging.getLogger('asyncio').setLevel(logging.CRITICAL) + logging.getLogger('httpx').setLevel(logging.CRITICAL) def __init__(self, **kwargs): super().__init__(**kwargs) diff --git a/scripts/daqconf_viewer.css b/scripts/daqconf_viewer.css index 930a696a..75378fdf 100644 --- a/scripts/daqconf_viewer.css +++ b/scripts/daqconf_viewer.css @@ -15,13 +15,18 @@ Screen { row-span: 2; column-span: 3; height: 100%; + overflow-x: auto; } #display { row-span: 8; column-span: 3; height: 100%; - overflow-y: auto; + align-horizontal: center; +} + +#conftree { + height:90%; } #verticalconf { From bd8fe29738a01126b38f1e72a2422e1a3c2f37ce Mon Sep 17 00:00:00 2001 From: Alessandro Thea Date: Wed, 21 Jun 2023 22:33:04 +0200 Subject: [PATCH 31/90] Consolidating changes --- python/daqconf/apps/dqm_gen.py | 56 +++++++++--------------------- python/daqconf/apps/readout_gen.py | 9 +++-- scripts/daqconf_multiru_gen | 40 +++++++++++---------- 3 files changed, 43 insertions(+), 62 deletions(-) diff --git a/python/daqconf/apps/dqm_gen.py b/python/daqconf/apps/dqm_gen.py index 10bbaaa9..7a64b828 100644 --- a/python/daqconf/apps/dqm_gen.py +++ b/python/daqconf/apps/dqm_gen.py @@ -25,31 +25,27 @@ from daqconf.core.daqmodule import DAQModule from daqconf.core.app import App,ModuleGraph -from detdataformats._daq_detdataformats_py import * +from detdataformats import * # Time to wait on pop() QUEUE_POP_WAIT_MS = 100 def get_dqm_app( - dqm, - detector, - daq_common, - - # DQM_IMPL='', - # DATA_RATE_SLOWDOWN_FACTOR=1, - # CLOCK_SPEED_HZ=62500000, + DQM_IMPL='', + DATA_RATE_SLOWDOWN_FACTOR=1, + CLOCK_SPEED_HZ=62500000, DQMIDX=0, - # MAX_NUM_FRAMES=32768, - # KAFKA_ADDRESS='', - # KAFKA_TOPIC='', - # CMAP='HD', - # RAW_PARAMS=[60, 50], - # RMS_PARAMS=[10, 1000], - # STD_PARAMS=[10, 1000], - # FOURIER_CHANNEL_PARAMS=[600, 100], - # FOURIER_PLANE_PARAMS=[60, 1000], + MAX_NUM_FRAMES=32768, + KAFKA_ADDRESS='', + KAFKA_TOPIC='', + CMAP='HD', + RAW_PARAMS=[60, 50], + RMS_PARAMS=[10, 1000], + STD_PARAMS=[10, 1000], + FOURIER_CHANNEL_PARAMS=[600, 100], + FOURIER_PLANE_PARAMS=[60, 1000], LINKS=[], - # HOST="localhost", + HOST="localhost", MODE="readout", DF_RATE=10, DF_ALGS='raw std fourier_plane', @@ -61,27 +57,8 @@ def get_dqm_app( DEBUG=False, ): - - DQM_IMPL=dqm.impl - DATA_RATE_SLOWDOWN_FACTOR=daq_common.data_rate_slowdown_factor - CLOCK_SPEED_HZ=detector.clock_speed_hz - MAX_NUM_FRAMES=dqm.max_num_frames - # DQMIDX = ru_i - KAFKA_ADDRESS=dqm.kafka_address - KAFKA_TOPIC=dqm.kafka_topic - CMAP=dqm.cmap - RAW_PARAMS=dqm.raw_params - RMS_PARAMS=dqm.rms_params - STD_PARAMS=dqm.std_params - FOURIER_CHANNEL_PARAMS=dqm.fourier_channel_params - FOURIER_PLANE_PARAMS=dqm.fourier_plane_params - # LINKS=dqm_links - # RU_APPNAME=ru_name - # TRB_DQM_SOURCEID_OFFSET=trb_dqm_sourceid_offset - HOST=dqm.host_dqm[DQMIDX % len(dqm.host_dqm)] - # RU_STREAMS=ru_desc.streams - FRONTEND_TYPE = DetID.subdetector_to_string(DetID.Subdetector(RU_STREAMS[0].geo_id.det_id)) + if ((FRONTEND_TYPE== "HD_TPC" or FRONTEND_TYPE== "VD_Bottom_TPC") and CLOCK_SPEED_HZ== 50000000): FRONTEND_TYPE = "wib" elif ((FRONTEND_TYPE== "HD_TPC" or FRONTEND_TYPE== "VD_Bottom_TPC") and CLOCK_SPEED_HZ== 62500000): @@ -101,7 +78,6 @@ def get_dqm_app( modules = [] if MODE == 'readout': - modules += [DAQModule(name='trb_dqm', plugin='TriggerRecordBuilder', conf=trb.ConfParams( @@ -140,7 +116,7 @@ def get_dqm_app( mgraph = ModuleGraph(modules) if MODE == 'readout': - mgraph.add_endpoint(f"timesync_.*", "dqmprocessor.timesync_input", "TimeSync", Direction.IN, is_pubsub=True) + mgraph.add_endpoint(f"timesync_{RU_APPNAME}_.*", "dqmprocessor.timesync_input", "TimeSync", Direction.IN, is_pubsub=True) mgraph.connect_modules("dqmprocessor.trigger_decision_output", "trb_dqm.trigger_decision_input", "TriggerDecision", 'trigger_decision_q_dqm') mgraph.connect_modules('trb_dqm.trigger_record_output', 'dqmprocessor.trigger_record_input', "TriggerRecord", 'trigger_record_q_dqm', toposort=False) else: diff --git a/python/daqconf/apps/readout_gen.py b/python/daqconf/apps/readout_gen.py index a6b43875..d5c8bd20 100644 --- a/python/daqconf/apps/readout_gen.py +++ b/python/daqconf/apps/readout_gen.py @@ -706,6 +706,7 @@ def add_dro_eps_and_fps( self, mgraph: ModuleGraph, dlh_list: list, + RUIDX: str, ) -> None: """Adds detector readout endpoints and fragment producers""" for dlh in dlh_list: @@ -722,7 +723,7 @@ def add_dro_eps_and_fps( fragments_out = f"datahandler_{dro_sid}.fragment_queue" ) mgraph.add_endpoint( - f"timesync_ru_{dro_sid}", + f"timesync_ru{RUIDX}_{dro_sid}", f"datahandler_{dro_sid}.timesync_output", "TimeSync", Direction.OUT, is_pubsub=True, @@ -738,7 +739,7 @@ def add_tpg_eps_and_fps( self, mgraph: ModuleGraph, tpg_dlh_list: list, - + RUIDX: str, ) -> None: """Adds detector readout endpoints and fragment producers""" @@ -749,7 +750,7 @@ def add_tpg_eps_and_fps( # Add enpointis with this source id for timesync and TPSets mgraph.add_endpoint( - f"timesync_tp_{tpset_sid}", + f"timesync_tp_dlh_ru{RUIDX}_{tpset_sid}", f"tp_datahandler_{tpset_sid}.timesync_output", "TimeSync", Direction.OUT, @@ -901,6 +902,7 @@ def generate( self.add_dro_eps_and_fps( mgraph=mgraph, dlh_list=dlhs_mods, + RUIDX=RU_DESCRIPTOR.label ) if TPG_ENABLED: @@ -908,6 +910,7 @@ def generate( self.add_tpg_eps_and_fps( mgraph=mgraph, tpg_dlh_list=tpg_mods, + RUIDX=RU_DESCRIPTOR.label ) # Create the application diff --git a/scripts/daqconf_multiru_gen b/scripts/daqconf_multiru_gen index 4b858594..8dc20473 100755 --- a/scripts/daqconf_multiru_gen +++ b/scripts/daqconf_multiru_gen @@ -354,12 +354,13 @@ def cli( #-------------------------------------------------------------------------- host_df, appconfig_df, df_app_names = create_df_apps(dataflow=dataflow, sourceid_broker=sourceid_broker) - + # Expand paths/assetfiles readout.default_data_file = resolve_asset_file(readout.default_data_file, debug) data_file_map = {} for entry in readout.data_files: data_file_map[entry["detector_id"]] = resolve_asset_file(entry["data_file"], debug) + # and output paths (Why does it need to be expanded in k8s mode only? or at all?) if use_k8s: console.log(f'Using k8s') dataflow.tpset_output_path = abspath(dataflow.tpset_output_path) @@ -369,6 +370,9 @@ def cli( new_output_path += [abspath(op)] df_app.output_paths = new_output_path + #-------------------------------------------------------------------------- + # Generation starts here + #-------------------------------------------------------------------------- console.log(f"Generating configs for hosts trigger={trigger.host_trigger} DFO={dataflow.host_dfo} dataflow={host_df} timing_hsi={hsi.host_timing_hsi} fake_hsi={hsi.host_fake_hsi} ctb_hsi={ctb_hsi.host_ctb_hsi} dqm={dqm.host_dqm}") the_system = System() @@ -557,39 +561,37 @@ def cli( # ru_name_with_underscore = f"ru{host}_{ru_id.iface}" the_system.apps[dqm_name] = get_dqm_app( - dqm=dqm, - detector=detector, - daq_common=daq_common, - # DQM_IMPL=dqm.impl, - # DATA_RATE_SLOWDOWN_FACTOR=daq_common.data_rate_slowdown_factor, - # CLOCK_SPEED_HZ=detector.clock_speed_hz, - # MAX_NUM_FRAMES=dqm.max_num_frames, + DQM_IMPL=dqm.impl, + DATA_RATE_SLOWDOWN_FACTOR=daq_common.data_rate_slowdown_factor, + CLOCK_SPEED_HZ=detector.clock_speed_hz, + MAX_NUM_FRAMES=dqm.max_num_frames, DQMIDX = ru_i, - # KAFKA_ADDRESS=dqm.kafka_address, - # KAFKA_TOPIC=dqm.kafka_topic, - # CMAP=dqm.cmap, - # RAW_PARAMS=dqm.raw_params, - # RMS_PARAMS=dqm.rms_params, - # STD_PARAMS=dqm.std_params, - # FOURIER_CHANNEL_PARAMS=dqm.fourier_channel_params, - # FOURIER_PLANE_PARAMS=dqm.fourier_plane_params, + KAFKA_ADDRESS=dqm.kafka_address, + KAFKA_TOPIC=dqm.kafka_topic, + CMAP=dqm.cmap, + RAW_PARAMS=dqm.raw_params, + RMS_PARAMS=dqm.rms_params, + STD_PARAMS=dqm.std_params, + FOURIER_CHANNEL_PARAMS=dqm.fourier_channel_params, + FOURIER_PLANE_PARAMS=dqm.fourier_plane_params, LINKS=dqm_links, RU_APPNAME=ru_name, TRB_DQM_SOURCEID_OFFSET=trb_dqm_sourceid_offset, - # HOST=dqm.host_dqm[ru_i % len(dqm.host_dqm)], + HOST=dqm.host_dqm[ru_i % len(dqm.host_dqm)], RU_STREAMS=ru_desc.streams, DEBUG=debug ) if debug: console.log(f"{dqm_name} app: {the_system.apps[dqm_name]}") - dqm_df_app_names = [] - idx = 0 #-------------------------------------------------------------------------- # Dataflow applications generatioo #-------------------------------------------------------------------------- + dqm_df_app_names = [] + idx = 0 + for app_name,df_config in appconfig_df.items(): dfidx = df_config.source_id the_system.apps[app_name] = get_dataflow_app( From f7ea13d3ea8cdc24b652090e6b141df78e781a8f Mon Sep 17 00:00:00 2001 From: Jonathan Hancock Date: Fri, 23 Jun 2023 20:37:36 +0200 Subject: [PATCH 32/90] Added a second screen to the app, selecting a config there shows the diff with the one you selected on the first screen. --- scripts/daqconf_viewer | 115 +++++++++++++++++++++++++++++-------- scripts/daqconf_viewer.css | 37 ++++++++---- 2 files changed, 116 insertions(+), 36 deletions(-) diff --git a/scripts/daqconf_viewer b/scripts/daqconf_viewer index 3ccabf11..940591a2 100755 --- a/scripts/daqconf_viewer +++ b/scripts/daqconf_viewer @@ -4,8 +4,8 @@ import httpx import json import sys -#from json_diff import Comparator from rich.text import Text +from difflib import context_diff, ndiff, unified_diff from textual import log, events from textual.app import App, ComposeResult @@ -17,6 +17,7 @@ from textual.message import Message, MessageTarget from textual.screen import Screen auth = ("fooUsr", "barPass") +oldconf = None class LabelItem(ListItem): def __init__(self, label: str) -> None: @@ -53,9 +54,8 @@ class Configs(Static): the_list.append(item) def on_list_view_selected(self, event: ListView.Selected): - '''The query gets all children of the app, then we choose Versions.''' confname = event.item.label - versions = self.app.query_one(Horizontal) + versions = self.screen.query_one(Horizontal) versions.new_conf(confname) class Versions(Horizontal): @@ -77,22 +77,21 @@ class Versions(Horizontal): async with httpx.AsyncClient() as client: payload = {'name': self.current_conf} r = await client.get(f'{self.hostname}/listVersions', auth=auth, params=payload, timeout=60) - self.vlist = r.json()['versions'] #This is a list of ints + self.vlist = r.json()['versions'] #This is a list of ints def watch_vlist(self, vlist:list[int]) -> None: old_buttons = self.query(Button) for b in old_buttons: b.remove() for v in vlist: - b_id = 'v' + str(v) #An id can't be just a number for some reason + b_id = 'v' + str(v) #An id can't be just a number for some reason self.mount(Button(str(v), id=b_id, classes='vbuttons', variant='primary')) - #TODO make buttons smaller, and force them to be in the centre (current position is a coincidence due to the margin) async def on_button_pressed (self, event: Button.Pressed) -> None: button_id = event.button.id version = int(button_id[1:]) - for v in self.app.query(Vertical): - if isinstance(v, Display): + for v in self.screen.query(Vertical): + if isinstance(v, Display) or isinstance(v, DiffDisplay): await v.get_json(self.current_conf, version) break @@ -106,8 +105,7 @@ class Display(Vertical): self.version = None def compose(self) -> ComposeResult: - yield Tree("Root", id='conftree') - yield Container(Button("Get Diff", id='diff_btn', variant='primary')) + yield Tree("", id='conftree') async def get_json(self, conf, ver) -> None: self.confname = conf @@ -154,38 +152,105 @@ class Display(Vertical): add_node("", node, json_data) def watch_confdata(self, confdata:dict) -> None: - tree = self.query_one(Tree) - tree.clear() - self.json_into_tree(tree.root, confdata) - tree.root.expand() + if confdata: + tree = self.query_one(Tree) + tree.clear() + self.json_into_tree(tree.root, confdata) + tree.root.expand() + + def on_button_pressed(self, event: Button.Pressed) -> None: + if self.confdata: + self.app.mount(DiffScreen(self.hostname, self.confdata, id='diffscreen')) + +class DiffDisplay(Vertical): + confdata = reactive(None) + + def __init__(self, hostname, **kwargs): + super().__init__(**kwargs) + self.hostname = hostname + self.confname = None + self.version = None + + def compose(self) -> ComposeResult: + yield Static(id='diffbox') + + async def get_json(self, conf, ver) -> None: + self.confname = conf + self.version = ver + if self.confname != None and self.version != None: + async with httpx.AsyncClient() as client: + payload = {'name': self.confname, 'version': self.version} + r = await client.get(f'{self.hostname}/retrieveVersion', auth=auth, params=payload, timeout=60) + self.confdata = r.json() + + def watch_confdata(self, confdata:dict) -> None: + '''Turns the jsons into a string format with newlines, then generates a diff of the two.''' + if confdata: + a = json.dumps(oldconf, sort_keys=True, indent=4).splitlines(keepends=True)[:30] + b = json.dumps(confdata, sort_keys=True, indent=4).splitlines(keepends=True)[:30] + delta = unified_diff(a,b) + diff = Text() + for d in delta: + sym = d[0] + match sym: + case '+': + t = Text(d, style='green') + case '-': + t = Text(d, style='red') + case '@': + t = Text(d, style='gold1') + case _: + t = Text(d) + diff += t + + box = self.query_one(Static) + box.update(diff) def on_button_pressed(self) -> None: - app.push_screen(DiffScreen()) + self.remove() class DiffScreen(Screen): - pass + BINDINGS = [("d", "app.pop_screen", "Return")] + def __init__(self, hostname, **kwargs): + super().__init__(**kwargs) + self.hostname = hostname + + def compose(self) -> ComposeResult: + yield Configs(hostname=self.hostname, classes='greencontainer configs', id='diffconfigs') + yield Versions(hostname=self.hostname, classes='greencontainer versions', id='diffversions') + yield DiffDisplay(hostname=self.hostname, classes='greencontainer display', id='diffdisplay') + + yield Header(show_clock=True) + yield Footer() + class ConfViewer(App): CSS_PATH = "daqconf_viewer.css" - #For some reason, importing json_diff makes these two modules start printing to stdout when a HTTP request is sent. - #The following code supresses all non-critical messages (hopefully all of them). - import logging - logging.getLogger('asyncio').setLevel(logging.CRITICAL) - logging.getLogger('httpx').setLevel(logging.CRITICAL) + BINDINGS = [("d", "make_diff()", "Diff")] def __init__(self, **kwargs): super().__init__(**kwargs) self.hostname = "http://np04-srv-023:31011" + def on_mount(self) -> None: + self.install_screen(DiffScreen(hostname=self.hostname), name="diff") + def compose(self) -> ComposeResult: - """Create child widgets for the app.""" - yield Configs(hostname=self.hostname, classes='container', id='configs') - yield Versions(hostname=self.hostname, classes='container', id='versions') - yield Display(hostname=self.hostname, classes='container', id='display') + yield Configs(hostname=self.hostname, classes='redcontainer configs', id='regconfigs') + yield Versions(hostname=self.hostname, classes='redcontainer versions', id='regversions') + yield Display(hostname=self.hostname, classes='redcontainer display', id='regdisplay') yield Header(show_clock=True) yield Footer() + def action_make_diff(self): + '''Saves the current config to a global variable, then pushes the diff screen.''' + dis = self.query_one(Display) + if dis.confdata != None: + global oldconf + oldconf = dis.confdata + self.push_screen('diff') + if __name__ == "__main__": app = ConfViewer() app.run() diff --git a/scripts/daqconf_viewer.css b/scripts/daqconf_viewer.css index 75378fdf..9889bcb9 100644 --- a/scripts/daqconf_viewer.css +++ b/scripts/daqconf_viewer.css @@ -1,40 +1,55 @@ /* 4 columns 10 rows */ Screen { layout: grid; + layers: below above; grid-size: 4 10; grid-gutter: 1; + height: 100%; +} + +#conftree { + height:90%; } -#configs { +#verticalconf { + height: 20; +} + +#diffscreen{ + layer: above; + align: center middle; + background: cadetblue; + height: 100%; + width: 100%; +} + +.configs { row-span: 10; column-span: 1; height: 100%; } -#versions { +.versions { row-span: 2; column-span: 3; height: 100%; overflow-x: auto; + align-vertical: middle; } -#display { +.display { row-span: 8; column-span: 3; height: 100%; align-horizontal: center; } -#conftree { - height:90%; -} - -#verticalconf { - height: 20; +.redcontainer{ + border: wide red; } -.container{ - border: wide red; +.greencontainer{ + border: wide green; } .vbuttons { From f546bc67a2077debd91b74f37e88ccd9f66baa40 Mon Sep 17 00:00:00 2001 From: Jonathan Hancock Date: Fri, 23 Jun 2023 20:56:29 +0200 Subject: [PATCH 33/90] Excluded the ID field from the diff since it is guaranteed to be different every time. The diff would previously only check the first 30 lines of the JSON (this was for testing purposes), this is no longer the case. --- scripts/daqconf_viewer | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/scripts/daqconf_viewer b/scripts/daqconf_viewer index 940591a2..9b7d2254 100755 --- a/scripts/daqconf_viewer +++ b/scripts/daqconf_viewer @@ -1,5 +1,6 @@ #!/usr/bin/env python3 import asyncio +import copy import httpx import json import sys @@ -183,11 +184,15 @@ class DiffDisplay(Vertical): r = await client.get(f'{self.hostname}/retrieveVersion', auth=auth, params=payload, timeout=60) self.confdata = r.json() - def watch_confdata(self, confdata:dict) -> None: + async def watch_confdata(self, confdata:dict) -> None: '''Turns the jsons into a string format with newlines, then generates a diff of the two.''' if confdata: - a = json.dumps(oldconf, sort_keys=True, indent=4).splitlines(keepends=True)[:30] - b = json.dumps(confdata, sort_keys=True, indent=4).splitlines(keepends=True)[:30] + j1 = copy.deepcopy(oldconf) + j2 = copy.deepcopy(confdata) + if "_id" in j1: del j1["_id"] #We don't want to include the ID in the diff since it's always different. + if "_id" in j2: del j2["_id"] + a = json.dumps(j1, sort_keys=True, indent=4).splitlines(keepends=True) + b = json.dumps(j2, sort_keys=True, indent=4).splitlines(keepends=True) delta = unified_diff(a,b) diff = Text() for d in delta: From 72a492ac976a5b713a09c5016e09211f08e4af79 Mon Sep 17 00:00:00 2001 From: Alessandro Thea Date: Sun, 25 Jun 2023 09:01:28 +0200 Subject: [PATCH 34/90] Cleanup --- config/daqconf_full_config.json | 463 +++++++++++++++++------------ python/daqconf/apps/readout_gen.py | 14 - scripts/daqconf_multiru_gen | 57 +--- 3 files changed, 273 insertions(+), 261 deletions(-) diff --git a/config/daqconf_full_config.json b/config/daqconf_full_config.json index 68648d1e..9f30f880 100644 --- a/config/daqconf_full_config.json +++ b/config/daqconf_full_config.json @@ -1,196 +1,273 @@ { - "boot": { - "base_command_port": 3333, - "disable_trace": false, - "opmon_impl": "local", - "ers_impl": "local", - "pocket_url": "127.0.0.1", - "image": "", - "use_k8s": false, - "op_env": "swtest", - "data_request_timeout_ms": 1000 - }, - "dataflow": { - "host_dfo": "localhost", - "apps": [ - { - "app_name": "dataflow0", + "boot": { + "base_command_port": 3333, + "capture_env_vars": [ + "TIMING_SHARE", + "DETCHANNELMAPS_SHARE" + ], + "connectivity_service_host": "np04-srv-016", + "connectivity_service_interval": 1000, + "connectivity_service_port": 15000, + "connectivity_service_threads": 2, + "disable_trace": false, + "ers_impl": "local", + "k8s_image": "dunedaq/c8-minimal", + "k8s_rte": "auto", + "opmon_impl": "local", + "pocket_url": "127.0.0.1", + "process_manager": "ssh", + "start_connectivity_service": true, + "use_connectivity_service": true + }, + "ctb_hsi": { + "beam_llt_triggers": [], + "crt_llt_triggers": [], + "fake_trig_1": { + "beam_mode": true, + "description": "Random trigger that can optionally be set to fire only during beam spill", + "enable": false, + "fixed_freq": true, + "period": 100000 + }, + "fake_trig_2": { + "beam_mode": true, + "description": "Random trigger that can optionally be set to fire only during beam spill", + "enable": false, + "fixed_freq": true, + "period": 100000 + }, + "hlt_triggers": [], + "host_ctb_hsi": "localhost", + "pds_llt_triggers": [], + "use_ctb_hsi": false + }, + "daq_common": { + "data_rate_slowdown_factor": 1, + "data_request_timeout_ms": 1000, + "use_data_network": false + }, + "dataflow": { + "apps": [ + { + "app_name": "dataflow0", + "data_store_mode": "all-per-file", + "host_df": "localhost", + "max_file_size": 4294967296, + "max_trigger_record_window": 0, + "output_paths": [ + "." + ] + } + ], + "enable_tpset_writing": false, + "host_dfo": "localhost", + "host_tpw": "localhost", "token_count": 10, - "output_paths": [ "." ], - "host_df": "localhost", - "max_file_size": 4294967296, - "max_trigger_record_window": 0 - } - ] - }, - "dqm": { - "enable_dqm": false, - "impl": "local", - "cmap": "HD", - "host_dqm": [ "localhost" ], - "raw_params": [ 60, 50 ], - "std_params": [ 10, 1000 ], - "rms_params": [ 0, 1000 ], - "fourier_channel_params": [ 0, 0 ], - "fourier_plane_params": [ 600, 1000 ], - "df_rate": 10, - "df_algs": "raw std fourier_plane", - "max_num_frames": 32768, - "kafka_address": "", - "kafka_topic": "DQM" - }, - "hsi": { - "use_timing_hsi": false, - "host_timing_hsi": "localhost", - "hsi_hw_connections_file": "${TIMING_SHARE}/config/etc/connections.xml", - "hsi_device_name": "", - "hsi_readout_period": 1000, - "control_hsi_hw": false, - "hsi_endpoint_address": 1, - "hsi_endpoint_partition": 0, - "hsi_re_mask": 0, - "hsi_fe_mask": 0, - "hsi_inv_mask": 0, - "hsi_source": 1, - "use_fake_hsi": true, - "host_fake_hsi": "localhost", - "hsi_device_id": 0, - "mean_hsi_signal_multiplicity": 1, - "hsi_signal_emulation_mode": 0, - "enabled_hsi_signals": 1 - }, - "ctb_hsi": { - "use_ctb_hsi": false, - "host_ctb_hsi": "localhost", - "hlt_triggers": [ - { "id":"HLT_4", - "description": "TEST HLT", - "enable":true, - "minc" : "0x1", - "mexc" : "0x0", - "prescale" : "0x0" - } - ], - "beam_llt_triggers": [], - "crt_llt_triggers":[], - "pds_llt_triggers": [], - "fake_trig_1": { - "description": "Fake 1Hz LLT trigger", - "enable": true, - "fixed_freq": true, - "beam_mode": false, - "period": 62500000 - }, - "fake_trig_2": { - "description": "Fake 1Hz LLT trigger", - "enable": true, - "fixed_freq": true, - "beam_mode": false, - "period": 62500000 - } - }, - "readout": { - "detector_readout_map_file": "./DetectorReadoutMap.json", - "emulator_mode": false, - "thread_pinning_file": "", - "data_rate_slowdown_factor": 1, - "clock_speed_hz": 62500000, - "default_data_file": "asset://?name=wib2-frames.bin&label=DuneWIB&subsystem=readout", - "data_files": [], - "latency_buffer_size": 499968, - "enable_tpg": false, - "enable_raw_recording": false, - "raw_recording_output_dir": ".", - "use_fake_data_producers": false, - "readout_sends_tp_fragments": false, - "eal_args": "-l 0-1 -n 3 -- -m [0:1].0 -j", - "numa_config": { - "default_id": 0, - "exceptions": [] + "tpset_output_file_size": 4294967296, + "tpset_output_path": "." + }, + "detector": { + "clock_speed_hz": 62500000, + "op_env": "swtest", + "tpg_channel_map": "PD2HDChannelMap" + }, + "dpdk_sender": { + "eal_args": "-l 0-1 -n 3 -- -m [0:1].0 -j", + "enable_dpdk_sender": false, + "host_dpdk_sender": [ + "np04-srv-021" + ] + }, + "dqm": { + "cmap": "HD", + "df_algs": "raw std fourier_plane", + "df_rate": 10, + "enable_dqm": false, + "fourier_channel_params": [ + 0, + 0 + ], + "fourier_plane_params": [ + 600, + 1000 + ], + "host_dqm": [ + "localhost" + ], + "impl": "local", + "kafka_address": "", + "kafka_topic": "DQM", + "max_num_frames": 32768, + "raw_params": [ + 60, + 50 + ], + "rms_params": [ + 0, + 1000 + ], + "std_params": [ + 10, + 1000 + ] + }, + "hsi": { + "control_hsi_hw": false, + "enable_hardware_state_recovery": true, + "enabled_hsi_signals": 1, + "host_fake_hsi": "localhost", + "host_timing_hsi": "localhost", + "hsi_device_id": 0, + "hsi_device_name": "", + "hsi_endpoint_address": 1, + "hsi_endpoint_partition": 0, + "hsi_fe_mask": 0, + "hsi_hw_connections_file": "${TIMING_SHARE}/config/etc/connections.xml", + "hsi_inv_mask": 0, + "hsi_re_mask": 0, + "hsi_readout_period": 1000, + "hsi_signal_emulation_mode": 0, + "hsi_source": 1, + "mean_hsi_signal_multiplicity": 1, + "use_fake_hsi": true, + "use_timing_hsi": false + }, + "readout": { + "data_files": [], + "default_data_file": "/cvmfs/dunedaq.opensciencegrid.org/assets/files/9/f/1/frames.bin", + "detector_readout_map_file": "./DetectorReadoutMap.json", + "dpdk_eal_args": "-l 0-1 -n 3 -- -m [0:1].0 -j", + "dpdk_rxqueues_per_lcore": 1, + "emulated_data_times_start_with_now": false, + "emulator_mode": false, + "enable_raw_recording": false, + "enable_tpg": false, + "fragment_send_timeout_ms": 10, + "latency_buffer_size": 499968, + "numa_config": { + "default_id": 0, + "default_latency_numa_aware": false, + "default_latency_preallocation": false, + "exceptions": [] + }, + "raw_recording_output_dir": ".", + "thread_pinning_file": "", + "tpg_algorithm": "SimpleThreshold", + "tpg_channel_mask": [], + "tpg_threshold": 120, + "use_fake_cards": false, + "use_fake_data_producers": false + }, + "timing": { + "control_timing_partition": false, + "host_tprtc": "localhost", + "timing_partition_id": 0, + "timing_partition_master_device_name": "", + "timing_partition_rate_control_enabled": false, + "timing_partition_spill_gate_enabled": false, + "timing_partition_trigger_mask": 255, + "timing_session_name": "" + }, + "trigger": { + "completeness_tolerance": 1, + "ctcm_trigger_intervals": [ + 10000000 + ], + "ctcm_trigger_types": [ + 4 + ], + "host_trigger": "localhost", + "hsi_trigger_type_passthrough": false, + "mlt_buffer_timeout": 100, + "mlt_ignore_tc": [], + "mlt_max_td_length_ms": 1000, + "mlt_merge_overlapping_tcs": true, + "mlt_send_timed_out_tds": true, + "mlt_td_readout_map": { + "c0": { + "candidate_type": 0, + "time_after": 1001, + "time_before": 1000 + }, + "c1": { + "candidate_type": 1, + "time_after": 1001, + "time_before": 1000 + }, + "c2": { + "candidate_type": 2, + "time_after": 1001, + "time_before": 1000 + }, + "c3": { + "candidate_type": 3, + "time_after": 1001, + "time_before": 1000 + }, + "c4": { + "candidate_type": 4, + "time_after": 1001, + "time_before": 1000 + }, + "c5": { + "candidate_type": 5, + "time_after": 1001, + "time_before": 1000 + }, + "c6": { + "candidate_type": 6, + "time_after": 1001, + "time_before": 1000 + }, + "c7": { + "candidate_type": 7, + "time_after": 1001, + "time_before": 1000 + }, + "c8": { + "candidate_type": 8, + "time_after": 1001, + "time_before": 1000 + }, + "c9": { + "candidate_type": 9, + "time_after": 1001, + "time_before": 1000 + } + }, + "mlt_use_readout_map": false, + "tolerate_incompleteness": false, + "trigger_activity_config": { + "adc_threshold": 10000, + "adj_tolerance": 4, + "adjacency_threshold": 6, + "n_channels_threshold": 8, + "prescale": 100, + "print_tp_info": false, + "trigger_on_adc": false, + "trigger_on_adjacency": true, + "trigger_on_n_channels": false, + "window_length": 10000 + }, + "trigger_activity_plugin": "TriggerActivityMakerPrescalePlugin", + "trigger_candidate_config": { + "adc_threshold": 10000, + "adj_tolerance": 4, + "adjacency_threshold": 6, + "n_channels_threshold": 8, + "prescale": 100, + "print_tp_info": false, + "trigger_on_adc": false, + "trigger_on_adjacency": true, + "trigger_on_n_channels": false, + "window_length": 10000 + }, + "trigger_candidate_plugin": "TriggerCandidateMakerPrescalePlugin", + "trigger_rate_hz": 1.0, + "trigger_window_after_ticks": 1000, + "trigger_window_before_ticks": 1000, + "ttcm_s1": 1, + "ttcm_s2": 2, + "use_custom_maker": false } - }, - "timing": { - "timing_session_name": "timing", - "host_tprtc": "localhost", - "control_timing_partition": false, - "timing_partition_master_device_name": "", - "timing_partition_id": 0, - "timing_partition_trigger_mask": 255, - "timing_partition_rate_control_enabled": false, - "timing_partition_spill_gate_enabled": false - }, - "trigger": { - "trigger_rate_hz": 1, - "trigger_window_before_ticks": 1000, - "trigger_window_after_ticks": 1000, - "host_trigger": "localhost", - "host_tpw": "localhost", - "ttcm_s1": 1, - "ttcm_s2": 2, - "trigger_activity_plugin": "TriggerActivityMakerPrescalePlugin", - "trigger_activity_config": { "prescale": 100 }, - "trigger_candidate_plugin": "TriggerCandidateMakerPrescalePlugin", - "trigger_candidate_config": { "prescale": 100 }, - "hsi_trigger_type_passthrough": false, - "enable_tpset_writing": false, - "tpset_output_path": ".", - "tpset_output_file_size": 4294967296, - "tpg_channel_map": "ProtoDUNESP1ChannelMap", - "mlt_merge_overlapping_tcs": true, - "mlt_buffer_timeout": 100, - "mlt_send_timed_out_tds": true, - "mlt_max_td_length_ms": 1000, - "mlt_ignore_tc": [], - "use_custom_maker": false, - "ctcm_trigger_types": [4], - "ctcm_trigger_intervals": [62500000], - "mlt_use_readout_map": false, - "mlt_td_readout_map": { - "c0": { - "time_before":1000, - "time_after": 1001 - }, - "c1": { - "time_before":1000, - "time_after": 1001 - }, - "c2": { - "time_before":1000, - "time_after": 1001 - }, - "c3": { - "time_before":1000, - "time_after": 1001 - }, - "c4": { - "time_before":1000, - "time_after": 1001 - }, - "c5": { - "time_before":1000, - "time_after": 1001 - }, - "c6": { - "time_before":1000, - "time_after": 1001 - }, - "c7": { - "time_before":1000, - "time_after": 1001 - }, - "c8": { - "time_before":1000, - "time_after": 1001 - }, - "c9": { - "time_before":1000, - "time_after": 1001 - } - } - }, - "dpdk_sender": { - "enable_dpdk_sender": false, - "host_dpdk_sender": [ "np04-srv-021" ], - "eal_args": "-l 0-1 -n 3 -- -m [0:1].0 -j" - } -} +} \ No newline at end of file diff --git a/python/daqconf/apps/readout_gen.py b/python/daqconf/apps/readout_gen.py index 13aed5f6..4c687ab5 100644 --- a/python/daqconf/apps/readout_gen.py +++ b/python/daqconf/apps/readout_gen.py @@ -10,32 +10,18 @@ # Load configuration types import moo.otypes -# moo.otypes.load_types('rcif/cmd.jsonnet') -# moo.otypes.load_types('appfwk/cmd.jsonnet') -# moo.otypes.load_types('appfwk/app.jsonnet') moo.otypes.load_types('flxlibs/felixcardreader.jsonnet') -# moo.otypes.load_types('dtpctrellibs/dtpcontroller.jsonnet') moo.otypes.load_types('readoutlibs/sourceemulatorconfig.jsonnet') moo.otypes.load_types('readoutlibs/readoutconfig.jsonnet') -# 20-Jun-2023, KAB: quick fix to get FD-specific nightly build to run -#moo.otypes.load_types('lbrulibs/pacmancardreader.jsonnet') moo.otypes.load_types('dfmodules/fakedataprod.jsonnet') moo.otypes.load_types("dpdklibs/nicreader.jsonnet") # Import new types -# import dunedaq.cmdlib.cmd as basecmd # AddressedCmd, -# import dunedaq.rcif.cmd as rccmd # AddressedCmd, -# import dunedaq.appfwk.cmd as cmd # AddressedCmd, -# import dunedaq.appfwk.app as app # AddressedCmd, import dunedaq.readoutlibs.sourceemulatorconfig as sec import dunedaq.flxlibs.felixcardreader as flxcr -# import dunedaq.dtpctrllibs.dtpcontroller as dtpctrl import dunedaq.readoutlibs.readoutconfig as rconf -# 20-Jun-2023, KAB: quick fix to get FD-specific nightly build to run -#import dunedaq.lbrulibs.pacmancardreader as pcr -# import dunedaq.dfmodules.triggerrecordbuilder as trb import dunedaq.dfmodules.fakedataprod as fdp import dunedaq.dpdklibs.nicreader as nrc diff --git a/scripts/daqconf_multiru_gen b/scripts/daqconf_multiru_gen index 81be68d8..6fa89301 100755 --- a/scripts/daqconf_multiru_gen +++ b/scripts/daqconf_multiru_gen @@ -216,7 +216,7 @@ def cli( debug_dir.mkdir(parents=True) config_data = config[0] - config_file = Path(config[1]) + config_file = Path(config[1] if config[1] is not None else "daqconf_default.json") if debug: console.log(f"Configuration for daqconf: {config_data.pod()}") @@ -264,31 +264,6 @@ def cli( file_label = file_label if file_label is not None else detector.op_env - # - # Configuration consistency checks - # - # if readout.enable_tpg and readout.use_fake_data_producers: - # raise Exception("Fake data producers don't support software tpg") - - # if readout.use_fake_data_producers and dqm.enable_dqm: - # raise Exception("DQM can't be used with fake data producers") - - # if dataflow.enable_tpset_writing and not readout.enable_tpg: - # raise Exception("TP writing can only be used when either software or firmware TPG is enabled") - - # if hsi.use_timing_hsi and not hsi.hsi_device_name: - # raise Exception("If --use-hsi-hw flag is set to true, --hsi-device-name must be specified!") - - # if timing.control_timing_partition and not timing.timing_partition_master_device_name: - # raise Exception("If --control-timing-partition flag is set to true, --timing-partition-master-device-name must be specified!") - - # if hsi.control_hsi_hw and not hsi.use_timing_hsi: - # raise Exception("Timing HSI hardware control can only be enabled if timing HSI hardware is used!") - - # if use_k8s and not boot.k8s_image: - # raise Exception("You need to define k8s_image if running with k8s") - - validate_conf(boot, readout, dataflow, timing, hsi, dqm) @@ -322,33 +297,6 @@ def cli( sourceid_broker = SourceIDBroker() sourceid_broker.debug = debug - - # import dunedaq.daqconf.confgen as confgen - - # if len(dataflow.apps) == 0: - # console.log(f"No Dataflow apps defined, adding default dataflow0") - # dataflow.apps = [confgen.dataflowapp()] - - # host_df = [] - # appconfig_df ={} - # df_app_names = [] - # for d in dataflow.apps: - # console.log(f"Parsing dataflow app config {d}") - - # ## Hack, we shouldn't need to do that, in the future, it should be appconfig = d - # appconfig = confgen.dataflowapp(**d) - - # dfapp = appconfig.app_name - # if dfapp in df_app_names: - # appconfig_df[dfapp].update(appconfig) - # else: - # df_app_names.append(dfapp) - # appconfig_df[dfapp] = appconfig - # appconfig_df[dfapp].source_id = sourceid_broker.get_next_source_id("TRBuilder") - # sourceid_broker.register_source_id("TRBuilder", appconfig_df[dfapp].source_id, None) - # host_df += [appconfig.host_df] - - #-------------------------------------------------------------------------- # Create dataflow applications #-------------------------------------------------------------------------- @@ -385,7 +333,8 @@ def cli( # Load Detector Readout map #-------------------------------------------------------------------------- dro_map = dromap.DetReadoutMapService() - dro_map.load(readout.detector_readout_map_file) + if readout.detector_readout_map_file: + dro_map.load(readout.detector_readout_map_file) ru_descs = dro_map.get_ru_descriptors() From ef696b0f0c3834c8df904c94708a451835a591bd Mon Sep 17 00:00:00 2001 From: Alessandro Thea Date: Mon, 26 Jun 2023 09:11:18 +0200 Subject: [PATCH 35/90] tweaks --- python/daqconf/core/conf_utils.py | 2 +- python/daqconf/core/sourceid.py | 4 ++-- scripts/daqconf_multiru_gen | 18 +++++++++++++----- 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/python/daqconf/core/conf_utils.py b/python/daqconf/core/conf_utils.py index 68d449a9..2dd4f989 100644 --- a/python/daqconf/core/conf_utils.py +++ b/python/daqconf/core/conf_utils.py @@ -828,7 +828,7 @@ def write_json_files(app_command_datas, system_command_datas, json_dir, verbose= console.rule("JSON file creation") data_dir = json_dir / 'data' - data_dir.mkdir(parents=True) + data_dir.mkdir(parents=True, exist_ok=True) # Apps for app_name, command_data in app_command_datas.items(): diff --git a/python/daqconf/core/sourceid.py b/python/daqconf/core/sourceid.py index 14b597b7..4bee3bfd 100644 --- a/python/daqconf/core/sourceid.py +++ b/python/daqconf/core/sourceid.py @@ -8,8 +8,8 @@ console = Console() -from daqdataformats._daq_daqdataformats_py import SourceID -from detchannelmaps._daq_detchannelmaps_py import * +from daqdataformats import SourceID +from detchannelmaps import * TAID = namedtuple('TAID', ['detector', 'crate']) TPID = namedtuple('TPID', ['detector', 'crate']) diff --git a/scripts/daqconf_multiru_gen b/scripts/daqconf_multiru_gen index 6fa89301..a7e2ebfd 100755 --- a/scripts/daqconf_multiru_gen +++ b/scripts/daqconf_multiru_gen @@ -6,6 +6,10 @@ import os.path from rich.console import Console from os.path import exists, abspath, dirname, basename from pathlib import Path + +console = Console() +# console.log("daqconf - loading base modules") + from daqconf.core.system import System from daqconf.core.metadata import write_metadata_file, write_config_file from daqconf.core.sourceid import SourceIDBroker #, get_tpg_mode @@ -15,16 +19,16 @@ from daqconf.core.assets import resolve_asset_file from detdataformats import * import daqconf.detreadoutmap as dromap +# console.log("daqconf - base modules loaded") -console = Console() # Set moo schema search path -from dunedaq.env import get_moo_model_path -import moo.io -moo.io.default_load_path = get_moo_model_path() +# from dunedaq.env import get_moo_model_path +# import moo.io +# moo.io.default_load_path = get_moo_model_path() # Load configuration types -import moo.otypes +# import moo.otypes # moo.otypes.load_types('detchannelmaps/hardwaremapservice.jsonnet') # import dunedaq.detchannelmaps.hardwaremapservice as hwms @@ -196,6 +200,7 @@ def cli( json_dir ): + console.log("Commandline parsing completed") if only_check_args: return @@ -776,8 +781,11 @@ def cli( the_system.apps[name].export(debug_dir / f"{name}.dot") if __name__ == '__main__': + # console.log("daqconf - started") try: cli(show_default=True, standalone_mode=True) except Exception as e: console.print_exception() raise SystemExit(-1) + # console.log("daqconf - finished") + From 0f8503f88384ff8905d91a11c1154133e9547d89 Mon Sep 17 00:00:00 2001 From: Alessandro Thea Date: Mon, 26 Jun 2023 16:24:19 +0200 Subject: [PATCH 36/90] confgen.jsonnet split into sub jsonnets --- python/daqconf/core/config_file.py | 11 +- schema/daqconf/bootgen.jsonnet | 54 +++ schema/daqconf/confgen.jsonnet | 588 +++++++++++++++------------- schema/daqconf/daqcommongen.jsonnet | 44 +++ schema/daqconf/dataflowgen.jsonnet | 63 +++ schema/daqconf/detectorgen.jsonnet | 46 +++ schema/daqconf/dqmgen.jsonnet | 57 +++ schema/daqconf/hsigen.jsonnet | 64 +++ schema/daqconf/readoutgen.jsonnet | 91 +++++ schema/daqconf/timinggen.jsonnet | 50 +++ schema/daqconf/triggergen.jsonnet | 147 +++++++ scripts/daqconf_multiru_gen | 72 ++-- 12 files changed, 976 insertions(+), 311 deletions(-) create mode 100644 schema/daqconf/bootgen.jsonnet create mode 100644 schema/daqconf/daqcommongen.jsonnet create mode 100644 schema/daqconf/dataflowgen.jsonnet create mode 100644 schema/daqconf/detectorgen.jsonnet create mode 100644 schema/daqconf/dqmgen.jsonnet create mode 100644 schema/daqconf/hsigen.jsonnet create mode 100644 schema/daqconf/readoutgen.jsonnet create mode 100644 schema/daqconf/timinggen.jsonnet create mode 100644 schema/daqconf/triggergen.jsonnet diff --git a/python/daqconf/core/config_file.py b/python/daqconf/core/config_file.py index 435bf3c2..3866e18a 100755 --- a/python/daqconf/core/config_file.py +++ b/python/daqconf/core/config_file.py @@ -171,7 +171,16 @@ def add_decorator(function): module_name = schema_file.replace('.jsonnet', '').replace('/', '.') config_module = importlib.import_module(f'dunedaq.{module_name}') schema_object = getattr(config_module, schema_object_name) - extra_schemas = [getattr(config_module, obj)() for obj in args] + extra_schemas = [] + for obj_name in args: + if '.' in obj_name: + i = obj_name.rfind('.') + ex_module_name, ex_schema_object_name = obj_name[:i], obj_name[i+1:] + extra_module = importlib.import_module(f'dunedaq.{ex_module_name}') + extra_schemas += [getattr(extra_module, ex_schema_object_name)()] + else: + # extra_schemas = [getattr(config_module, obj)() for obj in args] + extra_schemas = [getattr(config_module, obj_name)()] def configure(ctx, param, filename): return parse_config_file(filename, schema_object()) diff --git a/schema/daqconf/bootgen.jsonnet b/schema/daqconf/bootgen.jsonnet new file mode 100644 index 00000000..69a6b397 --- /dev/null +++ b/schema/daqconf/bootgen.jsonnet @@ -0,0 +1,54 @@ +// This is the configuration schema for daqconf_multiru_gen +// + +local moo = import "moo.jsonnet"; + +local s = moo.oschema.schema("dunedaq.daqconf.bootgen"); +local nc = moo.oschema.numeric_constraints; + +local cs = { + port: s.number( "Port", "i4", doc="A TCP/IP port number"), + count: s.number( "count", "i8", doc="A count of things"), + flag: s.boolean( "Flag", doc="Parameter that can be used to enable or disable functionality"), + monitoring_dest: s.enum( "MonitoringDest", ["local", "cern", "pocket"]), + path: s.string( "Path", doc="Location on a filesystem"), + paths: s.sequence( "Paths", self.path, doc="Multiple paths"), + host: s.string( "Host", moo.re.dnshost, doc="A hostname"), + hosts: s.sequence( "Hosts", self.host, "Multiple hosts"), + string: s.string( "Str", doc="Generic string"), + strings: s.sequence( "Strings", self.string, doc="List of strings"), + tpg_channel_map: s.enum( "TPGChannelMap", ["VDColdboxChannelMap", "ProtoDUNESP1ChannelMap", "PD2HDChannelMap", "HDColdboxChannelMap"]), + readout_time: s.number( "ROTime", "i8", doc="A readout time in ticks"), + pm_choice: s.enum( "PMChoice", ["k8s", "ssh"], doc="Process Manager choice: ssh or Kubernetes"), + rte_choice: s.enum( "RTEChoice", ["auto", "release", "devarea"], doc="Kubernetes DAQ application RTE choice"), + + + + boot: s.record("boot", [ + // s.field( "op_env", self.string, default='swtest', doc="Operational environment - used for raw data filename prefix and HDF5 Attribute inside the files"), + s.field( "base_command_port", self.port, default=3333, doc="Base port of application command endpoints"), + + # Obscure + s.field( "capture_env_vars", self.strings, default=['TIMING_SHARE', 'DETCHANNELMAPS_SHARE'], doc="List of variables to capture from the environment"), + s.field( "disable_trace", self.flag, false, doc="Do not enable TRACE (default TRACE_FILE is /tmp/trace_buffer_${HOSTNAME}_${USER})"), + s.field( "opmon_impl", self.monitoring_dest, default='local', doc="Info collector service implementation to use"), + s.field( "ers_impl", self.monitoring_dest, default='local', doc="ERS destination (Kafka used for cern and pocket)"), + s.field( "pocket_url", self.host, default='127.0.0.1', doc="URL for connecting to Pocket services"), + s.field( "process_manager", self.pm_choice, default="ssh", doc="Choice of process manager"), + + # K8S + s.field( "k8s_image", self.string, default="dunedaq/c8-minimal", doc="Which docker image to use"), + s.field( "k8s_rte", self.rte_choice, default="auto", doc="0 - Use an RTE script if not in a dev environment, 1 - Always use RTE, 2 - never use RTE"), + + # Connectivity Service + s.field( "use_connectivity_service", self.flag, default=true, doc="Whether to use the ConnectivityService to manage connections"), + s.field( "start_connectivity_service", self.flag, default=true, doc="Whether to use the ConnectivityService to manage connections"), + s.field( "connectivity_service_threads", self.count, default=2, doc="Number of threads for the gunicorn server that serves connection info"), + s.field( "connectivity_service_host", self.host, default='localhost', doc="Hostname for the ConnectivityService"), + s.field( "connectivity_service_port", self.port, default=15000, doc="Port for the ConnectivityService"), + s.field( "connectivity_service_interval", self.count, default=1000, doc="Publish interval for the ConnectivityService") + ]), +}; + + +moo.oschema.sort_select(cs) diff --git a/schema/daqconf/confgen.jsonnet b/schema/daqconf/confgen.jsonnet index c6eb236e..f9af8200 100755 --- a/schema/daqconf/confgen.jsonnet +++ b/schema/daqconf/confgen.jsonnet @@ -6,9 +6,37 @@ local moo = import "moo.jsonnet"; local sctb = import "ctbmodules/ctbmodule.jsonnet"; local ctbmodule = moo.oschema.hier(sctb).dunedaq.ctbmodules.ctbmodule; +local sboot = import "daqconf/bootgen.jsonnet"; +local bootgen = moo.oschema.hier(sboot).dunedaq.daqconf.bootgen; + +local sdetector = import "daqconf/detectorgen.jsonnet"; +local detectorgen = moo.oschema.hier(sdetector).dunedaq.daqconf.detectorgen; + +local sdaqcommon = import "daqconf/daqcommongen.jsonnet"; +local daqcommongen = moo.oschema.hier(sdaqcommon).dunedaq.daqconf.daqcommongen; + +local stiming = import "daqconf/timinggen.jsonnet"; +local timinggen = moo.oschema.hier(stiming).dunedaq.daqconf.timinggen; + +local shsi = import "daqconf/hsigen.jsonnet"; +local hsigen = moo.oschema.hier(shsi).dunedaq.daqconf.hsigen; + +local sreadout = import "daqconf/readoutgen.jsonnet"; +local readoutgen = moo.oschema.hier(sreadout).dunedaq.daqconf.readoutgen; + +local strigger = import "daqconf/triggergen.jsonnet"; +local triggergen = moo.oschema.hier(strigger).dunedaq.daqconf.triggergen; + +local sdataflow = import "daqconf/dataflowgen.jsonnet"; +local dataflowgen = moo.oschema.hier(sdataflow).dunedaq.daqconf.dataflowgen; + +local sdqm = import "daqconf/dqmgen.jsonnet"; +local dqmgen = moo.oschema.hier(sdqm).dunedaq.daqconf.dqmgen; + local s = moo.oschema.schema("dunedaq.daqconf.confgen"); local nc = moo.oschema.numeric_constraints; // A temporary schema construction context. + local cs = { port: s.number( "Port", "i4", doc="A TCP/IP port number"), freq: s.number( "Frequency", "u4", doc="A frequency"), @@ -38,79 +66,79 @@ local cs = { - boot: s.record("boot", [ - // s.field( "op_env", self.string, default='swtest', doc="Operational environment - used for raw data filename prefix and HDF5 Attribute inside the files"), - s.field( "base_command_port", self.port, default=3333, doc="Base port of application command endpoints"), - - # Obscure - s.field( "capture_env_vars", self.strings, default=['TIMING_SHARE', 'DETCHANNELMAPS_SHARE'], doc="List of variables to capture from the environment"), - s.field( "disable_trace", self.flag, false, doc="Do not enable TRACE (default TRACE_FILE is /tmp/trace_buffer_${HOSTNAME}_${USER})"), - s.field( "opmon_impl", self.monitoring_dest, default='local', doc="Info collector service implementation to use"), - s.field( "ers_impl", self.monitoring_dest, default='local', doc="ERS destination (Kafka used for cern and pocket)"), - s.field( "pocket_url", self.host, default='127.0.0.1', doc="URL for connecting to Pocket services"), - s.field( "process_manager", self.pm_choice, default="ssh", doc="Choice of process manager"), - - # K8S - s.field( "k8s_image", self.string, default="dunedaq/c8-minimal", doc="Which docker image to use"), - s.field( "k8s_rte", self.rte_choice, default="auto", doc="0 - Use an RTE script if not in a dev environment, 1 - Always use RTE, 2 - never use RTE"), - - # Connectivity Service - s.field( "use_connectivity_service", self.flag, default=true, doc="Whether to use the ConnectivityService to manage connections"), - s.field( "start_connectivity_service", self.flag, default=true, doc="Whether to use the ConnectivityService to manage connections"), - s.field( "connectivity_service_threads", self.count, default=2, doc="Number of threads for the gunicorn server that serves connection info"), - s.field( "connectivity_service_host", self.host, default='localhost', doc="Hostname for the ConnectivityService"), - s.field( "connectivity_service_port", self.port, default=15000, doc="Port for the ConnectivityService"), - s.field( "connectivity_service_interval", self.count, default=1000, doc="Publish interval for the ConnectivityService") - ]), - - - daq_common : s.record("daq_common", [ - s.field( "data_request_timeout_ms", self.count, default=1000, doc="The baseline data request timeout that will be used by modules in the Readout and Trigger subsystems (i.e. any module that produces data fragments). Downstream timeouts, such as the trigger-record-building timeout, are derived from this."), - s.field( "use_data_network", self.flag, default = false, doc="Whether to use the data network (Won't work with k8s)"), - s.field( "data_rate_slowdown_factor",self.count, default=1, doc="Factor by which to suppress data generation. Former -s"), - ], doc="Cmmon daq_common settings"), - - detector : s.record("detector", [ - s.field( "op_env", self.string, default='swtest', doc="Operational environment - used for HDF5 Attribute inside the files"), - s.field( "clock_speed_hz", self.freq, default=62500000), - s.field( "tpg_channel_map", self.tpg_channel_map, default="PD2HDChannelMap", doc="Channel map for TPG"), - ], doc="Global common settings"), - - timing: s.record("timing", [ - s.field( "timing_session_name", self.string, default="", doc="Name of the global timing session to use, for timing commands"), - s.field( "host_tprtc", self.host, default='localhost', doc='Host to run the timing partition controller app on'), - # timing hw partition options - s.field( "control_timing_partition", self.flag, default=false, doc='Flag to control whether we are controlling timing partition in master hardware'), - s.field( "timing_partition_master_device_name", self.string, default="", doc='Timing partition master hardware device name'), - s.field( "timing_partition_id", self.count, default=0, doc='Timing partition id'), - s.field( "timing_partition_trigger_mask", self.count, default=255, doc='Timing partition trigger mask'), - s.field( "timing_partition_rate_control_enabled", self.flag, default=false, doc='Timing partition rate control enabled'), - s.field( "timing_partition_spill_gate_enabled", self.flag, default=false, doc='Timing partition spill gate enabled'), - ]), + // boot: s.record("boot", [ + // // s.field( "op_env", self.string, default='swtest', doc="Operational environment - used for raw data filename prefix and HDF5 Attribute inside the files"), + // s.field( "base_command_port", self.port, default=3333, doc="Base port of application command endpoints"), - hsi: s.record("hsi", [ - # timing hsi options - s.field( "use_timing_hsi", self.flag, default=false, doc='Flag to control whether real hardware timing HSI config is generated. Default is false'), - s.field( "host_timing_hsi", self.host, default='localhost', doc='Host to run the HSI app on'), - s.field( "hsi_hw_connections_file", self.path, default="${TIMING_SHARE}/config/etc/connections.xml", doc='Real timing hardware only: path to hardware connections file'), - s.field( "enable_hardware_state_recovery", self.flag, default=true, doc="Enable (or not) hardware state recovery"), - s.field( "hsi_device_name", self.string, default="", doc='Real HSI hardware only: device name of HSI hw'), - s.field( "hsi_readout_period", self.count, default=1e3, doc='Real HSI hardware only: Period between HSI hardware polling [us]'), - s.field( "control_hsi_hw", self.flag, default=false, doc='Flag to control whether we are controlling hsi hardware'), - s.field( "hsi_endpoint_address", self.count, default=1, doc='Timing address of HSI endpoint'), - s.field( "hsi_endpoint_partition", self.count, default=0, doc='Timing partition of HSI endpoint'), - s.field( "hsi_re_mask",self.count, default=0, doc='Rising-edge trigger mask'), - s.field( "hsi_fe_mask", self.count, default=0, doc='Falling-edge trigger mask'), - s.field( "hsi_inv_mask",self.count, default=0, doc='Invert-edge mask'), - s.field( "hsi_source",self.count, default=1, doc='HSI signal source; 0 - hardware, 1 - emulation (trigger timestamp bits)'), - # fake hsi options - s.field( "use_fake_hsi", self.flag, default=true, doc='Flag to control whether fake or real hardware HSI config is generated. Default is true'), - s.field( "host_fake_hsi", self.host, default='localhost', doc='Host to run the HSI app on'), - s.field( "hsi_device_id", self.count, default=0, doc='Fake HSI only: device ID of fake HSIEvents'), - s.field( "mean_hsi_signal_multiplicity", self.count, default=1, doc='Fake HSI only: rate of individual HSI signals in emulation mode 1'), - s.field( "hsi_signal_emulation_mode", self.count, default=0, doc='Fake HSI only: HSI signal emulation mode'), - s.field( "enabled_hsi_signals", self.count, default=1, doc='Fake HSI only: bit mask of enabled fake HSI signals') - ]), + // # Obscure + // s.field( "capture_env_vars", self.strings, default=['TIMING_SHARE', 'DETCHANNELMAPS_SHARE'], doc="List of variables to capture from the environment"), + // s.field( "disable_trace", self.flag, false, doc="Do not enable TRACE (default TRACE_FILE is /tmp/trace_buffer_${HOSTNAME}_${USER})"), + // s.field( "opmon_impl", self.monitoring_dest, default='local', doc="Info collector service implementation to use"), + // s.field( "ers_impl", self.monitoring_dest, default='local', doc="ERS destination (Kafka used for cern and pocket)"), + // s.field( "pocket_url", self.host, default='127.0.0.1', doc="URL for connecting to Pocket services"), + // s.field( "process_manager", self.pm_choice, default="ssh", doc="Choice of process manager"), + + // # K8S + // s.field( "k8s_image", self.string, default="dunedaq/c8-minimal", doc="Which docker image to use"), + // s.field( "k8s_rte", self.rte_choice, default="auto", doc="0 - Use an RTE script if not in a dev environment, 1 - Always use RTE, 2 - never use RTE"), + + // # Connectivity Service + // s.field( "use_connectivity_service", self.flag, default=true, doc="Whether to use the ConnectivityService to manage connections"), + // s.field( "start_connectivity_service", self.flag, default=true, doc="Whether to use the ConnectivityService to manage connections"), + // s.field( "connectivity_service_threads", self.count, default=2, doc="Number of threads for the gunicorn server that serves connection info"), + // s.field( "connectivity_service_host", self.host, default='localhost', doc="Hostname for the ConnectivityService"), + // s.field( "connectivity_service_port", self.port, default=15000, doc="Port for the ConnectivityService"), + // s.field( "connectivity_service_interval", self.count, default=1000, doc="Publish interval for the ConnectivityService") + // ]), + + + // daq_common : s.record("daq_common", [ + // s.field( "data_request_timeout_ms", self.count, default=1000, doc="The baseline data request timeout that will be used by modules in the Readout and Trigger subsystems (i.e. any module that produces data fragments). Downstream timeouts, such as the trigger-record-building timeout, are derived from this."), + // s.field( "use_data_network", self.flag, default = false, doc="Whether to use the data network (Won't work with k8s)"), + // s.field( "data_rate_slowdown_factor",self.count, default=1, doc="Factor by which to suppress data generation. Former -s"), + // ], doc="Cmmon daq_common settings"), + + // detector : s.record("detector", [ + // s.field( "op_env", self.string, default='swtest', doc="Operational environment - used for HDF5 Attribute inside the files"), + // s.field( "clock_speed_hz", self.freq, default=62500000), + // s.field( "tpg_channel_map", self.tpg_channel_map, default="PD2HDChannelMap", doc="Channel map for TPG"), + // ], doc="Global common settings"), + + // timing: s.record("timing", [ + // s.field( "timing_session_name", self.string, default="", doc="Name of the global timing session to use, for timing commands"), + // s.field( "host_tprtc", self.host, default='localhost', doc='Host to run the timing partition controller app on'), + // # timing hw partition options + // s.field( "control_timing_partition", self.flag, default=false, doc='Flag to control whether we are controlling timing partition in master hardware'), + // s.field( "timing_partition_master_device_name", self.string, default="", doc='Timing partition master hardware device name'), + // s.field( "timing_partition_id", self.count, default=0, doc='Timing partition id'), + // s.field( "timing_partition_trigger_mask", self.count, default=255, doc='Timing partition trigger mask'), + // s.field( "timing_partition_rate_control_enabled", self.flag, default=false, doc='Timing partition rate control enabled'), + // s.field( "timing_partition_spill_gate_enabled", self.flag, default=false, doc='Timing partition spill gate enabled'), + // ]), + + // hsi: s.record("hsi", [ + // # timing hsi options + // s.field( "use_timing_hsi", self.flag, default=false, doc='Flag to control whether real hardware timing HSI config is generated. Default is false'), + // s.field( "host_timing_hsi", self.host, default='localhost', doc='Host to run the HSI app on'), + // s.field( "hsi_hw_connections_file", self.path, default="${TIMING_SHARE}/config/etc/connections.xml", doc='Real timing hardware only: path to hardware connections file'), + // s.field( "enable_hardware_state_recovery", self.flag, default=true, doc="Enable (or not) hardware state recovery"), + // s.field( "hsi_device_name", self.string, default="", doc='Real HSI hardware only: device name of HSI hw'), + // s.field( "hsi_readout_period", self.count, default=1e3, doc='Real HSI hardware only: Period between HSI hardware polling [us]'), + // s.field( "control_hsi_hw", self.flag, default=false, doc='Flag to control whether we are controlling hsi hardware'), + // s.field( "hsi_endpoint_address", self.count, default=1, doc='Timing address of HSI endpoint'), + // s.field( "hsi_endpoint_partition", self.count, default=0, doc='Timing partition of HSI endpoint'), + // s.field( "hsi_re_mask",self.count, default=0, doc='Rising-edge trigger mask'), + // s.field( "hsi_fe_mask", self.count, default=0, doc='Falling-edge trigger mask'), + // s.field( "hsi_inv_mask",self.count, default=0, doc='Invert-edge mask'), + // s.field( "hsi_source",self.count, default=1, doc='HSI signal source; 0 - hardware, 1 - emulation (trigger timestamp bits)'), + // # fake hsi options + // s.field( "use_fake_hsi", self.flag, default=true, doc='Flag to control whether fake or real hardware HSI config is generated. Default is true'), + // s.field( "host_fake_hsi", self.host, default='localhost', doc='Host to run the HSI app on'), + // s.field( "hsi_device_id", self.count, default=0, doc='Fake HSI only: device ID of fake HSIEvents'), + // s.field( "mean_hsi_signal_multiplicity", self.count, default=1, doc='Fake HSI only: rate of individual HSI signals in emulation mode 1'), + // s.field( "hsi_signal_emulation_mode", self.count, default=0, doc='Fake HSI only: HSI signal emulation mode'), + // s.field( "enabled_hsi_signals", self.count, default=1, doc='Fake HSI only: bit mask of enabled fake HSI signals') + // ]), ctb_hsi: s.record("ctb_hsi", [ # ctb options @@ -124,228 +152,228 @@ local cs = { s.field( "fake_trig_2", ctbmodule.Randomtrigger, ctbmodule.Randomtrigger) ]), - data_file_entry: s.record("data_file_entry", [ - s.field( "data_file", self.path, default='./frames.bin', doc="File containing data frames to be replayed by the fake cards. Former -d. Uses the asset manager, can also be 'asset://checksum/somelonghash', or 'file://somewhere/frames.bin' or 'frames.bin'"), - s.field( "detector_id", self.count, default=3, doc="Detector ID that this file applies to"), - ]), - data_files: s.sequence("data_files", self.data_file_entry), - - numa_exception: s.record( "NUMAException", [ - s.field( "host", self.host, default='localhost', doc="Host of exception"), - s.field( "card", self.count, default=0, doc="Card ID of exception"), - s.field( "numa_id", self.count, default=0, doc="NUMA ID of exception"), - s.field( "felix_card_id", self.count, default=-1, doc="CARD ID override, -1 indicates no override"), - s.field( "latency_buffer_numa_aware", self.flag, default=false, doc="Enable NUMA-aware mode for the Latency Buffer"), - s.field( "latency_buffer_preallocation", self.flag, default=false, doc="Enable Latency Buffer preallocation"), - ], doc="Exception to the default NUMA ID for FELIX cards"), - - numa_exceptions: s.sequence( "NUMAExceptions", self.numa_exception, doc="Exceptions to the default NUMA ID"), + // data_file_entry: s.record("data_file_entry", [ + // s.field( "data_file", self.path, default='./frames.bin', doc="File containing data frames to be replayed by the fake cards. Former -d. Uses the asset manager, can also be 'asset://checksum/somelonghash', or 'file://somewhere/frames.bin' or 'frames.bin'"), + // s.field( "detector_id", self.count, default=3, doc="Detector ID that this file applies to"), + // ]), + // data_files: s.sequence("data_files", self.data_file_entry), + + // numa_exception: s.record( "NUMAException", [ + // s.field( "host", self.host, default='localhost', doc="Host of exception"), + // s.field( "card", self.count, default=0, doc="Card ID of exception"), + // s.field( "numa_id", self.count, default=0, doc="NUMA ID of exception"), + // s.field( "felix_card_id", self.count, default=-1, doc="CARD ID override, -1 indicates no override"), + // s.field( "latency_buffer_numa_aware", self.flag, default=false, doc="Enable NUMA-aware mode for the Latency Buffer"), + // s.field( "latency_buffer_preallocation", self.flag, default=false, doc="Enable Latency Buffer preallocation"), + // ], doc="Exception to the default NUMA ID for FELIX cards"), + + // numa_exceptions: s.sequence( "NUMAExceptions", self.numa_exception, doc="Exceptions to the default NUMA ID"), - numa_config: s.record("numa_config", [ - s.field( "default_id", self.count, default=0, doc="Default NUMA ID for FELIX cards"), - s.field( "default_latency_numa_aware", self.flag, default=false, doc="Default for Latency Buffer NUMA awareness"), - s.field( "default_latency_preallocation", self.flag, default=false, doc="Default for Latency Buffer Preallocation"), - s.field( "exceptions", self.numa_exceptions, default=[], doc="Exceptions to the default NUMA ID"), - ]), + // numa_config: s.record("numa_config", [ + // s.field( "default_id", self.count, default=0, doc="Default NUMA ID for FELIX cards"), + // s.field( "default_latency_numa_aware", self.flag, default=false, doc="Default for Latency Buffer NUMA awareness"), + // s.field( "default_latency_preallocation", self.flag, default=false, doc="Default for Latency Buffer Preallocation"), + // s.field( "exceptions", self.numa_exceptions, default=[], doc="Exceptions to the default NUMA ID"), + // ]), - readout: s.record("readout", [ - s.field( "detector_readout_map_file", self.path, default='./DetectorReadoutMap.json', doc="File containing detector hardware map for configuration to run"), - s.field( "use_fake_data_producers", self.flag, default=false, doc="Use fake data producers that respond with empty fragments immediately instead of (fake) cards and DLHs"), - // s.field( "memory_limit_gb", self.count, default=64, doc="Application memory limit in GB") - // Fake cards - s.field( "use_fake_cards", self.flag, default=false, doc="Use fake cards"), - s.field( "emulated_data_times_start_with_now", self.flag, default=false, doc="If active, the timestamp of the first emulated data frame is set to the current wallclock time"), - s.field( "default_data_file", self.path, default='asset://?label=ProtoWIB&subsystem=readout', doc="File containing data frames to be replayed by the fake cards. Former -d. Uses the asset manager, can also be 'asset://?checksum=somelonghash', or 'file://somewhere/frames.bin' or 'frames.bin'"), - s.field( "data_files", self.data_files, default=[], doc="Files to use by detector type"), - // DPDK - s.field( "dpdk_eal_args", self.string, default='-l 0-1 -n 3 -- -m [0:1].0 -j', doc='Args passed to the EAL in DPDK'), - s.field( "dpdk_rxqueues_per_lcore", self.count, default=1, doc='Number of rx queues per core'), - // FLX - s.field( "numa_config", self.numa_config, default=self.numa_config, doc='Configuration of FELIX NUMA IDs'), - // DLH - s.field( "emulator_mode", self.flag, default=false, doc="If active, timestamps of data frames are overwritten when processed by the readout. This is necessary if the felix card does not set correct timestamps. Former -e"), - s.field( "thread_pinning_file", self.path, default="", doc="A thread pinning configuration file that gets executed after conf."), - // s.field( "data_rate_slowdown_factor",self.count, default=1, doc="Factor by which to suppress data generation. Former -s"), - s.field( "latency_buffer_size", self.count, default=499968, doc="Size of the latency buffers (in number of elements)"), - s.field( "fragment_send_timeout_ms", self.count, default=10, doc="The send timeout that will be used in the readout modules when sending fragments downstream (i.e. to the TRB)."), - s.field( "enable_tpg", self.flag, default=false, doc="Enable TPG"), - s.field( "tpg_threshold", self.count, default=120, doc="Select TPG threshold"), - s.field( "tpg_algorithm", self.string, default="SimpleThreshold", doc="Select TPG algorithm (SimpleThreshold, AbsRS)"), - s.field( "tpg_channel_mask", self.channel_list, default=[], doc="List of offline channels to be masked out from the TPHandler"), - s.field( "enable_raw_recording", self.flag, default=false, doc="Add queues and modules necessary for the record command"), - s.field( "raw_recording_output_dir", self.path, default='.', doc="Output directory where recorded data is written to. Data for each link is written to a separate file") - ]), + // readout: s.record("readout", [ + // s.field( "detector_readout_map_file", self.path, default='./DetectorReadoutMap.json', doc="File containing detector hardware map for configuration to run"), + // s.field( "use_fake_data_producers", self.flag, default=false, doc="Use fake data producers that respond with empty fragments immediately instead of (fake) cards and DLHs"), + // // s.field( "memory_limit_gb", self.count, default=64, doc="Application memory limit in GB") + // // Fake cards + // s.field( "use_fake_cards", self.flag, default=false, doc="Use fake cards"), + // s.field( "emulated_data_times_start_with_now", self.flag, default=false, doc="If active, the timestamp of the first emulated data frame is set to the current wallclock time"), + // s.field( "default_data_file", self.path, default='asset://?label=ProtoWIB&subsystem=readout', doc="File containing data frames to be replayed by the fake cards. Former -d. Uses the asset manager, can also be 'asset://?checksum=somelonghash', or 'file://somewhere/frames.bin' or 'frames.bin'"), + // s.field( "data_files", self.data_files, default=[], doc="Files to use by detector type"), + // // DPDK + // s.field( "dpdk_eal_args", self.string, default='-l 0-1 -n 3 -- -m [0:1].0 -j', doc='Args passed to the EAL in DPDK'), + // s.field( "dpdk_rxqueues_per_lcore", self.count, default=1, doc='Number of rx queues per core'), + // // FLX + // s.field( "numa_config", self.numa_config, default=self.numa_config, doc='Configuration of FELIX NUMA IDs'), + // // DLH + // s.field( "emulator_mode", self.flag, default=false, doc="If active, timestamps of data frames are overwritten when processed by the readout. This is necessary if the felix card does not set correct timestamps. Former -e"), + // s.field( "thread_pinning_file", self.path, default="", doc="A thread pinning configuration file that gets executed after conf."), + // // s.field( "data_rate_slowdown_factor",self.count, default=1, doc="Factor by which to suppress data generation. Former -s"), + // s.field( "latency_buffer_size", self.count, default=499968, doc="Size of the latency buffers (in number of elements)"), + // s.field( "fragment_send_timeout_ms", self.count, default=10, doc="The send timeout that will be used in the readout modules when sending fragments downstream (i.e. to the TRB)."), + // s.field( "enable_tpg", self.flag, default=false, doc="Enable TPG"), + // s.field( "tpg_threshold", self.count, default=120, doc="Select TPG threshold"), + // s.field( "tpg_algorithm", self.string, default="SimpleThreshold", doc="Select TPG algorithm (SimpleThreshold, AbsRS)"), + // s.field( "tpg_channel_mask", self.channel_list, default=[], doc="List of offline channels to be masked out from the TPHandler"), + // s.field( "enable_raw_recording", self.flag, default=false, doc="Add queues and modules necessary for the record command"), + // s.field( "raw_recording_output_dir", self.path, default='.', doc="Output directory where recorded data is written to. Data for each link is written to a separate file") + // ]), - trigger_algo_config: s.record("trigger_algo_config", [ - s.field("prescale", self.count, default=100), - s.field("window_length", self.count, default=10000), - s.field("adjacency_threshold", self.count, default=6), - s.field("adj_tolerance", self.count, default=4), - s.field("trigger_on_adc", self.flag, default=false), - s.field("trigger_on_n_channels", self.flag, default=false), - s.field("trigger_on_adjacency", self.flag, default=true), - s.field("adc_threshold", self.count, default=10000), - s.field("n_channels_threshold", self.count, default=8), - s.field("print_tp_info", self.flag, default=false), - ]), + // trigger_algo_config: s.record("trigger_algo_config", [ + // s.field("prescale", self.count, default=100), + // s.field("window_length", self.count, default=10000), + // s.field("adjacency_threshold", self.count, default=6), + // s.field("adj_tolerance", self.count, default=4), + // s.field("trigger_on_adc", self.flag, default=false), + // s.field("trigger_on_n_channels", self.flag, default=false), + // s.field("trigger_on_adjacency", self.flag, default=true), + // s.field("adc_threshold", self.count, default=10000), + // s.field("n_channels_threshold", self.count, default=8), + // s.field("print_tp_info", self.flag, default=false), + // ]), - c0_readout: s.record("c0_readout", [ - s.field("candidate_type", self.tc_type, default=0, doc="The TC type, 0=Unknown"), - s.field("time_before", self.readout_time, default=1000, doc="Time to readout before TC time [ticks]"), - s.field("time_after", self.readout_time, default=1001, doc="Time to readout after TC time [ticks]"), - ]), - c1_readout: s.record("c1_readout", [ - s.field("candidate_type", self.tc_type, default=1, doc="The TC type, 1=Timing"), - s.field("time_before", self.readout_time, default=1000, doc="Time to readout before TC time [ticks]"), - s.field("time_after", self.readout_time, default=1001, doc="Time to readout after TC time [ticks]"), - ]), - c2_readout: s.record("c2_readout", [ - s.field("candidate_type", self.tc_type, default=2, doc="The TC type, 2=TPCLowE"), - s.field("time_before", self.readout_time, default=1000, doc="Time to readout before TC time [ticks]"), - s.field("time_after", self.readout_time, default=1001, doc="Time to readout after TC time [ticks]"), - ]), - c3_readout: s.record("c3_readout", [ - s.field("candidate_type", self.tc_type, default=3, doc="The TC type, 3=Supernova"), - s.field("time_before", self.readout_time, default=1000, doc="Time to readout before TC time [ticks]"), - s.field("time_after", self.readout_time, default=1001, doc="Time to readout after TC time [ticks]"), - ]), - c4_readout: s.record("c4_readout", [ - s.field("candidate_type", self.tc_type, default=4, doc="The TC type, 4=Random"), - s.field("time_before", self.readout_time, default=1000, doc="Time to readout before TC time [ticks]"), - s.field("time_after", self.readout_time, default=1001, doc="Time to readout after TC time [ticks]"), - ]), - c5_readout: s.record("c5_readout", [ - s.field("candidate_type", self.tc_type, default=5, doc="The TC type, 5=Prescale"), - s.field("time_before", self.readout_time, default=1000, doc="Time to readout before TC time [ticks]"), - s.field("time_after", self.readout_time, default=1001, doc="Time to readout after TC time [ticks]"), - ]), - c6_readout: s.record("c6_readout", [ - s.field("candidate_type", self.tc_type, default=6, doc="The TC type, 6=ADCSimpleWindow"), - s.field("time_before", self.readout_time, default=1000, doc="Time to readout before TC time [ticks]"), - s.field("time_after", self.readout_time, default=1001, doc="Time to readout after TC time [ticks]"), - ]), - c7_readout: s.record("c7_readout", [ - s.field("candidate_type", self.tc_type, default=7, doc="The TC type, 7=HorizontalMuon"), - s.field("time_before", self.readout_time, default=1000, doc="Time to readout before TC time [ticks]"), - s.field("time_after", self.readout_time, default=1001, doc="Time to readout after TC time [ticks]"), - ]), - c8_readout: s.record("c8_readout", [ - s.field("candidate_type", self.tc_type, default=8, doc="The TC type, 8=MichelElectron"), - s.field("time_before", self.readout_time, default=1000, doc="Time to readout before TC time [ticks]"), - s.field("time_after", self.readout_time, default=1001, doc="Time to readout after TC time [ticks]"), - ]), - c9_readout: s.record("c9_readout", [ - s.field("candidate_type", self.tc_type, default=9, doc="The TC type, 9=LowEnergyEvent"), - s.field("time_before", self.readout_time, default=1000, doc="Time to readout before TC time [ticks]"), - s.field("time_after", self.readout_time, default=1001, doc="Time to readout after TC time [ticks]"), - ]), + // c0_readout: s.record("c0_readout", [ + // s.field("candidate_type", self.tc_type, default=0, doc="The TC type, 0=Unknown"), + // s.field("time_before", self.readout_time, default=1000, doc="Time to readout before TC time [ticks]"), + // s.field("time_after", self.readout_time, default=1001, doc="Time to readout after TC time [ticks]"), + // ]), + // c1_readout: s.record("c1_readout", [ + // s.field("candidate_type", self.tc_type, default=1, doc="The TC type, 1=Timing"), + // s.field("time_before", self.readout_time, default=1000, doc="Time to readout before TC time [ticks]"), + // s.field("time_after", self.readout_time, default=1001, doc="Time to readout after TC time [ticks]"), + // ]), + // c2_readout: s.record("c2_readout", [ + // s.field("candidate_type", self.tc_type, default=2, doc="The TC type, 2=TPCLowE"), + // s.field("time_before", self.readout_time, default=1000, doc="Time to readout before TC time [ticks]"), + // s.field("time_after", self.readout_time, default=1001, doc="Time to readout after TC time [ticks]"), + // ]), + // c3_readout: s.record("c3_readout", [ + // s.field("candidate_type", self.tc_type, default=3, doc="The TC type, 3=Supernova"), + // s.field("time_before", self.readout_time, default=1000, doc="Time to readout before TC time [ticks]"), + // s.field("time_after", self.readout_time, default=1001, doc="Time to readout after TC time [ticks]"), + // ]), + // c4_readout: s.record("c4_readout", [ + // s.field("candidate_type", self.tc_type, default=4, doc="The TC type, 4=Random"), + // s.field("time_before", self.readout_time, default=1000, doc="Time to readout before TC time [ticks]"), + // s.field("time_after", self.readout_time, default=1001, doc="Time to readout after TC time [ticks]"), + // ]), + // c5_readout: s.record("c5_readout", [ + // s.field("candidate_type", self.tc_type, default=5, doc="The TC type, 5=Prescale"), + // s.field("time_before", self.readout_time, default=1000, doc="Time to readout before TC time [ticks]"), + // s.field("time_after", self.readout_time, default=1001, doc="Time to readout after TC time [ticks]"), + // ]), + // c6_readout: s.record("c6_readout", [ + // s.field("candidate_type", self.tc_type, default=6, doc="The TC type, 6=ADCSimpleWindow"), + // s.field("time_before", self.readout_time, default=1000, doc="Time to readout before TC time [ticks]"), + // s.field("time_after", self.readout_time, default=1001, doc="Time to readout after TC time [ticks]"), + // ]), + // c7_readout: s.record("c7_readout", [ + // s.field("candidate_type", self.tc_type, default=7, doc="The TC type, 7=HorizontalMuon"), + // s.field("time_before", self.readout_time, default=1000, doc="Time to readout before TC time [ticks]"), + // s.field("time_after", self.readout_time, default=1001, doc="Time to readout after TC time [ticks]"), + // ]), + // c8_readout: s.record("c8_readout", [ + // s.field("candidate_type", self.tc_type, default=8, doc="The TC type, 8=MichelElectron"), + // s.field("time_before", self.readout_time, default=1000, doc="Time to readout before TC time [ticks]"), + // s.field("time_after", self.readout_time, default=1001, doc="Time to readout after TC time [ticks]"), + // ]), + // c9_readout: s.record("c9_readout", [ + // s.field("candidate_type", self.tc_type, default=9, doc="The TC type, 9=LowEnergyEvent"), + // s.field("time_before", self.readout_time, default=1000, doc="Time to readout before TC time [ticks]"), + // s.field("time_after", self.readout_time, default=1001, doc="Time to readout after TC time [ticks]"), + // ]), - tc_readout_map: s.record("tc_readout_map", [ - s.field("c0", self.c0_readout, default=self.c0_readout, doc="TC readout for TC type 0"), - s.field("c1", self.c1_readout, default=self.c1_readout, doc="TC readout for TC type 1"), - s.field("c2", self.c2_readout, default=self.c2_readout, doc="TC readout for TC type 2"), - s.field("c3", self.c3_readout, default=self.c3_readout, doc="TC readout for TC type 3"), - s.field("c4", self.c4_readout, default=self.c4_readout, doc="TC readout for TC type 4"), - s.field("c5", self.c5_readout, default=self.c5_readout, doc="TC readout for TC type 5"), - s.field("c6", self.c6_readout, default=self.c6_readout, doc="TC readout for TC type 6"), - s.field("c7", self.c7_readout, default=self.c7_readout, doc="TC readout for TC type 7"), - s.field("c8", self.c8_readout, default=self.c8_readout, doc="TC readout for TC type 8"), - s.field("c9", self.c9_readout, default=self.c9_readout, doc="TC readout for TC type 9"), - ]), + // tc_readout_map: s.record("tc_readout_map", [ + // s.field("c0", self.c0_readout, default=self.c0_readout, doc="TC readout for TC type 0"), + // s.field("c1", self.c1_readout, default=self.c1_readout, doc="TC readout for TC type 1"), + // s.field("c2", self.c2_readout, default=self.c2_readout, doc="TC readout for TC type 2"), + // s.field("c3", self.c3_readout, default=self.c3_readout, doc="TC readout for TC type 3"), + // s.field("c4", self.c4_readout, default=self.c4_readout, doc="TC readout for TC type 4"), + // s.field("c5", self.c5_readout, default=self.c5_readout, doc="TC readout for TC type 5"), + // s.field("c6", self.c6_readout, default=self.c6_readout, doc="TC readout for TC type 6"), + // s.field("c7", self.c7_readout, default=self.c7_readout, doc="TC readout for TC type 7"), + // s.field("c8", self.c8_readout, default=self.c8_readout, doc="TC readout for TC type 8"), + // s.field("c9", self.c9_readout, default=self.c9_readout, doc="TC readout for TC type 9"), + // ]), - trigger: s.record("trigger",[ - s.field( "trigger_rate_hz", self.rate, default=1.0, doc='Fake HSI only: rate at which fake HSIEvents are sent. 0 - disable HSIEvent generation. Former -t'), - s.field( "trigger_window_before_ticks",self.count, default=1000, doc="Trigger window before marker. Former -b"), - s.field( "trigger_window_after_ticks", self.count, default=1000, doc="Trigger window after marker. Former -a"), - s.field( "host_trigger", self.host, default='localhost', doc='Host to run the trigger app on'), - // s.field( "host_tpw", self.host, default='localhost', doc='Host to run the TPWriter app on'), - # trigger options - s.field( "completeness_tolerance", self.count, default=1, doc="Maximum number of inactive queues we will tolerate."), - s.field( "tolerate_incompleteness", self.flag, default=false, doc="Flag to tell trigger to tolerate inactive queues."), - s.field( "ttcm_s1", self.count,default=1, doc="Timing trigger candidate maker accepted HSI signal ID 1"), - s.field( "ttcm_s2", self.count, default=2, doc="Timing trigger candidate maker accepted HSI signal ID 2"), - s.field( "trigger_activity_plugin", self.string, default='TriggerActivityMakerPrescalePlugin', doc="Trigger activity algorithm plugin"), - s.field( "trigger_activity_config", self.trigger_algo_config, default=self.trigger_algo_config,doc="Trigger activity algorithm config (string containing python dictionary)"), - s.field( "trigger_candidate_plugin", self.string, default='TriggerCandidateMakerPrescalePlugin', doc="Trigger candidate algorithm plugin"), - s.field( "trigger_candidate_config", self.trigger_algo_config, default=self.trigger_algo_config, doc="Trigger candidate algorithm config (string containing python dictionary)"), - s.field( "hsi_trigger_type_passthrough", self.flag, default=false, doc="Option to override trigger type in the MLT"), - // s.field( "enable_tpset_writing", self.flag, default=false, doc="Enable the writing of TPs to disk (only works with enable_tpg or enable_firmware_tpg)"), - // s.field( "tpset_output_path", self.path,default='.', doc="Output directory for TPSet stream files"), - // s.field( "tpset_output_file_size",self.count, default=4*1024*1024*1024, doc="The size threshold when TPSet stream files are closed (in bytes)"), - // s.field( "tpg_channel_map", self.tpg_channel_map, default="ProtoDUNESP1ChannelMap", doc="Channel map for TPG"), - s.field( "mlt_merge_overlapping_tcs", self.flag, default=true, doc="Option to turn off merging of overlapping TCs when forming TDs in MLT"), - s.field( "mlt_buffer_timeout", self.count, default=100, doc="Timeout (buffer) to wait for new overlapping TCs before sending TD"), - s.field( "mlt_send_timed_out_tds", self.flag, default=true, doc="Option to drop TD if TC comes out of timeout window"), - s.field( "mlt_max_td_length_ms", self.count, default=1000, doc="Maximum allowed time length [ms] for a readout window of a single TD"), - s.field( "mlt_ignore_tc", self.tc_types, default=[], doc="Optional list of TC types to be ignored in MLT"), - s.field( "mlt_use_readout_map", self.flag, default=false, doc="Option to use custom readout map in MLT"), - s.field( "mlt_td_readout_map", self.tc_readout_map, default=self.tc_readout_map, doc="The readout windows assigned to TDs in MLT, based on TC type."), - s.field( "use_custom_maker", self.flag, default=false, doc="Option to use a Custom Trigger Candidate Maker (plugin)"), - s.field( "ctcm_trigger_types", self.tc_types, default=[4], doc="Optional list of TC types to be used by the Custom Trigger Candidate Maker (plugin)"), - s.field( "ctcm_trigger_intervals", self.tc_intervals, default=[10000000], doc="Optional list of intervals (clock ticks) for the TC types to be used by the Custom Trigger Candidate Maker (plugin)"), - ]), + // trigger: s.record("trigger",[ + // s.field( "trigger_rate_hz", self.rate, default=1.0, doc='Fake HSI only: rate at which fake HSIEvents are sent. 0 - disable HSIEvent generation. Former -t'), + // s.field( "trigger_window_before_ticks",self.count, default=1000, doc="Trigger window before marker. Former -b"), + // s.field( "trigger_window_after_ticks", self.count, default=1000, doc="Trigger window after marker. Former -a"), + // s.field( "host_trigger", self.host, default='localhost', doc='Host to run the trigger app on'), + // // s.field( "host_tpw", self.host, default='localhost', doc='Host to run the TPWriter app on'), + // # trigger options + // s.field( "completeness_tolerance", self.count, default=1, doc="Maximum number of inactive queues we will tolerate."), + // s.field( "tolerate_incompleteness", self.flag, default=false, doc="Flag to tell trigger to tolerate inactive queues."), + // s.field( "ttcm_s1", self.count,default=1, doc="Timing trigger candidate maker accepted HSI signal ID 1"), + // s.field( "ttcm_s2", self.count, default=2, doc="Timing trigger candidate maker accepted HSI signal ID 2"), + // s.field( "trigger_activity_plugin", self.string, default='TriggerActivityMakerPrescalePlugin', doc="Trigger activity algorithm plugin"), + // s.field( "trigger_activity_config", self.trigger_algo_config, default=self.trigger_algo_config,doc="Trigger activity algorithm config (string containing python dictionary)"), + // s.field( "trigger_candidate_plugin", self.string, default='TriggerCandidateMakerPrescalePlugin', doc="Trigger candidate algorithm plugin"), + // s.field( "trigger_candidate_config", self.trigger_algo_config, default=self.trigger_algo_config, doc="Trigger candidate algorithm config (string containing python dictionary)"), + // s.field( "hsi_trigger_type_passthrough", self.flag, default=false, doc="Option to override trigger type in the MLT"), + // // s.field( "enable_tpset_writing", self.flag, default=false, doc="Enable the writing of TPs to disk (only works with enable_tpg or enable_firmware_tpg)"), + // // s.field( "tpset_output_path", self.path,default='.', doc="Output directory for TPSet stream files"), + // // s.field( "tpset_output_file_size",self.count, default=4*1024*1024*1024, doc="The size threshold when TPSet stream files are closed (in bytes)"), + // // s.field( "tpg_channel_map", self.tpg_channel_map, default="ProtoDUNESP1ChannelMap", doc="Channel map for TPG"), + // s.field( "mlt_merge_overlapping_tcs", self.flag, default=true, doc="Option to turn off merging of overlapping TCs when forming TDs in MLT"), + // s.field( "mlt_buffer_timeout", self.count, default=100, doc="Timeout (buffer) to wait for new overlapping TCs before sending TD"), + // s.field( "mlt_send_timed_out_tds", self.flag, default=true, doc="Option to drop TD if TC comes out of timeout window"), + // s.field( "mlt_max_td_length_ms", self.count, default=1000, doc="Maximum allowed time length [ms] for a readout window of a single TD"), + // s.field( "mlt_ignore_tc", self.tc_types, default=[], doc="Optional list of TC types to be ignored in MLT"), + // s.field( "mlt_use_readout_map", self.flag, default=false, doc="Option to use custom readout map in MLT"), + // s.field( "mlt_td_readout_map", self.tc_readout_map, default=self.tc_readout_map, doc="The readout windows assigned to TDs in MLT, based on TC type."), + // s.field( "use_custom_maker", self.flag, default=false, doc="Option to use a Custom Trigger Candidate Maker (plugin)"), + // s.field( "ctcm_trigger_types", self.tc_types, default=[4], doc="Optional list of TC types to be used by the Custom Trigger Candidate Maker (plugin)"), + // s.field( "ctcm_trigger_intervals", self.tc_intervals, default=[10000000], doc="Optional list of intervals (clock ticks) for the TC types to be used by the Custom Trigger Candidate Maker (plugin)"), + // ]), - dataflowapp: s.record("dataflowapp",[ - s.field( "app_name", self.string, default="dataflow0"), - s.field( "output_paths",self.paths, default=['.'], doc="Location(s) for the dataflow app to write data. Former -o"), - s.field( "host_df", self.host, default='localhost'), - s.field( "max_file_size",self.count, default=4*1024*1024*1024, doc="The size threshold when raw data files are closed (in bytes)"), - s.field( "data_store_mode", self.string, default="all-per-file", doc="all-per-file or one-event-per-file"), - s.field( "max_trigger_record_window",self.count, default=0, doc="The maximum size for the window of data that will included in a single TriggerRecord (in ticks). Readout windows that are longer than this size will result in TriggerRecords being split into a sequence of TRs. A zero value for this parameter means no splitting."), - - ], doc="Element of the dataflow.apps array"), - - dataflowapps: s.sequence("dataflowapps", self.dataflowapp, doc="List of dataflowapp instances"), - - dataflow: s.record("dataflow", [ - s.field( "host_dfo", self.host, default='localhost', doc="Sets the host for the DFO app"), - s.field( "apps", self.dataflowapps, default=[], doc="Configuration for the dataflow apps (see dataflowapp for options)"), - s.field( "token_count",self.count, default=10, doc="Number of tokens the dataflow apps give to the DFO. Former -c"), - // Trigger - s.field( "host_tpw", self.host, default='localhost', doc='Host to run the TPWriter app on'), - s.field( "enable_tpset_writing", self.flag, default=false, doc="Enable the writing of TPs to disk (only works with enable_tpg or enable_firmware_tpg)"), - s.field( "tpset_output_path", self.path,default='.', doc="Output directory for TPSet stream files"), - s.field( "tpset_output_file_size",self.count, default=4*1024*1024*1024, doc="The size threshold when TPSet stream files are closed (in bytes)"), - ]), + // dataflowapp: s.record("dataflowapp",[ + // s.field( "app_name", self.string, default="dataflow0"), + // s.field( "output_paths",self.paths, default=['.'], doc="Location(s) for the dataflow app to write data. Former -o"), + // s.field( "host_df", self.host, default='localhost'), + // s.field( "max_file_size",self.count, default=4*1024*1024*1024, doc="The size threshold when raw data files are closed (in bytes)"), + // s.field( "data_store_mode", self.string, default="all-per-file", doc="all-per-file or one-event-per-file"), + // s.field( "max_trigger_record_window",self.count, default=0, doc="The maximum size for the window of data that will included in a single TriggerRecord (in ticks). Readout windows that are longer than this size will result in TriggerRecords being split into a sequence of TRs. A zero value for this parameter means no splitting."), - dqm: s.record("dqm", [ - s.field('enable_dqm', self.flag, default=false, doc="Enable Data Quality Monitoring"), - s.field('impl', self.monitoring_dest, default='local', doc="DQM destination (Kafka used for cern and pocket)"), - s.field('cmap', self.dqm_channel_map, default='HD', doc="Which channel map to use for DQM"), - s.field('host_dqm', self.hosts, default=['localhost'], doc='Host(s) to run the DQM app on'), - s.field('raw_params', self.dqm_params, default=[60, 50], doc="Parameters that control the data sent for the raw display plot"), - s.field('std_params', self.dqm_params, default=[10, 1000], doc="Parameters that control the data sent for the mean/rms plot"), - s.field('rms_params', self.dqm_params, default=[0, 1000], doc="Parameters that control the data sent for the mean/rms plot"), - s.field('fourier_channel_params', self.dqm_params, default=[0, 0], doc="Parameters that control the data sent for the fourier transform plot"), - s.field('fourier_plane_params', self.dqm_params, default=[600, 1000], doc="Parameters that control the data sent for the summed fourier transform plot"), - s.field('df_rate', self.count, default=10, doc='How many seconds between requests to DF for Trigger Records'), - s.field('df_algs', self.string, default='raw std fourier_plane', doc='Algorithms to be run on Trigger Records from DF (use quotes)'), - s.field('max_num_frames', self.count, default=32768, doc='Maximum number of frames to use in the algorithms'), - s.field('kafka_address', self.string, default='', doc='kafka address used to send messages'), - s.field('kafka_topic', self.string, default='DQM', doc='kafka topic used to send messages'), - ]), + // ], doc="Element of the dataflow.apps array"), - dpdk_sender: s.record("dpdk_sender", [ - s.field( "enable_dpdk_sender", self.flag, default=false, doc="Enable sending frames using DPDK"), - s.field( "host_dpdk_sender", self.hosts, default=['np04-srv-021'], doc="Which host to use to send frames"), - s.field( "eal_args", self.string, default='-l 0-1 -n 3 -- -m [0:1].0 -j', doc='Args passed to the EAL in DPDK'), - ]), + // dataflowapps: s.sequence("dataflowapps", self.dataflowapp, doc="List of dataflowapp instances"), + + // dataflow: s.record("dataflow", [ + // s.field( "host_dfo", self.host, default='localhost', doc="Sets the host for the DFO app"), + // s.field( "apps", self.dataflowapps, default=[], doc="Configuration for the dataflow apps (see dataflowapp for options)"), + // s.field( "token_count",self.count, default=10, doc="Number of tokens the dataflow apps give to the DFO. Former -c"), + // // Trigger + // s.field( "host_tpw", self.host, default='localhost', doc='Host to run the TPWriter app on'), + // s.field( "enable_tpset_writing", self.flag, default=false, doc="Enable the writing of TPs to disk (only works with enable_tpg or enable_firmware_tpg)"), + // s.field( "tpset_output_path", self.path,default='.', doc="Output directory for TPSet stream files"), + // s.field( "tpset_output_file_size",self.count, default=4*1024*1024*1024, doc="The size threshold when TPSet stream files are closed (in bytes)"), + // ]), + + // dqm: s.record("dqm", [ + // s.field('enable_dqm', self.flag, default=false, doc="Enable Data Quality Monitoring"), + // s.field('impl', self.monitoring_dest, default='local', doc="DQM destination (Kafka used for cern and pocket)"), + // s.field('cmap', self.dqm_channel_map, default='HD', doc="Which channel map to use for DQM"), + // s.field('host_dqm', self.hosts, default=['localhost'], doc='Host(s) to run the DQM app on'), + // s.field('raw_params', self.dqm_params, default=[60, 50], doc="Parameters that control the data sent for the raw display plot"), + // s.field('std_params', self.dqm_params, default=[10, 1000], doc="Parameters that control the data sent for the mean/rms plot"), + // s.field('rms_params', self.dqm_params, default=[0, 1000], doc="Parameters that control the data sent for the mean/rms plot"), + // s.field('fourier_channel_params', self.dqm_params, default=[0, 0], doc="Parameters that control the data sent for the fourier transform plot"), + // s.field('fourier_plane_params', self.dqm_params, default=[600, 1000], doc="Parameters that control the data sent for the summed fourier transform plot"), + // s.field('df_rate', self.count, default=10, doc='How many seconds between requests to DF for Trigger Records'), + // s.field('df_algs', self.string, default='raw std fourier_plane', doc='Algorithms to be run on Trigger Records from DF (use quotes)'), + // s.field('max_num_frames', self.count, default=32768, doc='Maximum number of frames to use in the algorithms'), + // s.field('kafka_address', self.string, default='', doc='kafka address used to send messages'), + // s.field('kafka_topic', self.string, default='DQM', doc='kafka topic used to send messages'), + // ]), + + // dpdk_sender: s.record("dpdk_sender", [ + // s.field( "enable_dpdk_sender", self.flag, default=false, doc="Enable sending frames using DPDK"), + // s.field( "host_dpdk_sender", self.hosts, default=['np04-srv-021'], doc="Which host to use to send frames"), + // s.field( "eal_args", self.string, default='-l 0-1 -n 3 -- -m [0:1].0 -j', doc='Args passed to the EAL in DPDK'), + // ]), daqconf_multiru_gen: s.record('daqconf_multiru_gen', [ - s.field('detector', self.detector, default=self.detector, doc='Boot parameters'), - s.field('daq_common', self.daq_common, default=self.daq_common, doc='DAQ common parameters'), - s.field('boot', self.boot, default=self.boot, doc='Boot parameters'), - s.field('dataflow', self.dataflow, default=self.dataflow, doc='Dataflow paramaters'), - s.field('dqm', self.dqm, default=self.dqm, doc='DQM parameters'), - s.field('hsi', self.hsi, default=self.hsi, doc='HSI parameters'), + s.field('detector', detectorgen.detector, default=detectorgen.detector, doc='Boot parameters'), + s.field('daq_common', daqcommongen.daq_common, default=daqcommongen.daq_common, doc='DAQ common parameters'), + s.field('boot', bootgen.boot, default=bootgen.boot, doc='Boot parameters'), + s.field('dataflow', dataflowgen.dataflow, default=dataflowgen.dataflow, doc='Dataflow paramaters'), + s.field('dqm', dqmgen.dqm, default=dqmgen.dqm, doc='DQM parameters'), + s.field('hsi', hsigen.hsi, default=hsigen.hsi, doc='HSI parameters'), s.field('ctb_hsi', self.ctb_hsi, default=self.ctb_hsi, doc='CTB parameters'), - s.field('readout', self.readout, default=self.readout, doc='Readout parameters'), - s.field('timing', self.timing, default=self.timing, doc='Timing parameters'), - s.field('trigger', self.trigger, default=self.trigger, doc='Trigger parameters'), - s.field('dpdk_sender', self.dpdk_sender, default=self.dpdk_sender, doc='DPDK sender parameters'), + s.field('readout', readoutgen.readout, default=readoutgen.readout, doc='Readout parameters'), + s.field('timing', timinggen.timing, default=timinggen.timing, doc='Timing parameters'), + s.field('trigger', triggergen.trigger, default=triggergen.trigger, doc='Trigger parameters') + // s.field('dpdk_sender', self.dpdk_sender, default=self.dpdk_sender, doc='DPDK sender parameters'), ]), }; // Output a topologically sorted array. -sctb + moo.oschema.sort_select(cs) +sboot + sdetector + sdaqcommon + stiming + shsi + sreadout + strigger + sdataflow + sdqm + sctb + moo.oschema.sort_select(cs) diff --git a/schema/daqconf/daqcommongen.jsonnet b/schema/daqconf/daqcommongen.jsonnet new file mode 100644 index 00000000..d67e6e5a --- /dev/null +++ b/schema/daqconf/daqcommongen.jsonnet @@ -0,0 +1,44 @@ +// This is the configuration schema for daqconf_multiru_gen +// + +local moo = import "moo.jsonnet"; + +local s = moo.oschema.schema("dunedaq.daqconf.daqcommongen"); +local nc = moo.oschema.numeric_constraints; +// A temporary schema construction context. +local cs = { + port: s.number( "Port", "i4", doc="A TCP/IP port number"), + freq: s.number( "Frequency", "u4", doc="A frequency"), + rate: s.number( "Rate", "f8", doc="A rate as a double"), + count: s.number( "count", "i8", doc="A count of things"), + three_choice: s.number( "threechoice", "i8", nc(minimum=0, exclusiveMaximum=3), doc="A choice between 0, 1, or 2"), + flag: s.boolean( "Flag", doc="Parameter that can be used to enable or disable functionality"), + monitoring_dest: s.enum( "MonitoringDest", ["local", "cern", "pocket"]), + path: s.string( "Path", doc="Location on a filesystem"), + paths: s.sequence( "Paths", self.path, doc="Multiple paths"), + host: s.string( "Host", moo.re.dnshost, doc="A hostname"), + hosts: s.sequence( "Hosts", self.host, "Multiple hosts"), + string: s.string( "Str", doc="Generic string"), + strings: s.sequence( "Strings", self.string, doc="List of strings"), + tpg_channel_map: s.enum( "TPGChannelMap", ["VDColdboxChannelMap", "ProtoDUNESP1ChannelMap", "PD2HDChannelMap", "HDColdboxChannelMap"]), + dqm_channel_map: s.enum( "DQMChannelMap", ['HD', 'VD', 'PD2HD', 'HDCB']), + dqm_params: s.sequence( "DQMParams", self.count, doc="Parameters for DQM (fixme)"), + tc_types: s.sequence( "TCTypes", self.count, doc="List of TC types"), + tc_type: s.number( "TCType", "i4", nc(minimum=0, maximum=9), doc="Number representing TC type. Currently ranging from 0 to 9"), + tc_interval: s.number( "TCInterval", "i8", nc(minimum=1, maximum=30000000000), doc="The intervals between TCs that are inserted into MLT by CTCM, in clock ticks"), + tc_intervals: s.sequence( "TCIntervals", self.tc_interval, doc="List of TC intervals used by CTCM"), + readout_time: s.number( "ROTime", "i8", doc="A readout time in ticks"), + channel_list: s.sequence( "ChannelList", self.count, doc="List of offline channels to be masked out from the TPHandler"), + tpg_algo_choice: s.enum( "TPGAlgoChoice", ["SimpleThreshold", "AbsRS"], doc="Trigger algorithm choice"), + pm_choice: s.enum( "PMChoice", ["k8s", "ssh"], doc="Process Manager choice: ssh or Kubernetes"), + rte_choice: s.enum( "RTEChoice", ["auto", "release", "devarea"], doc="Kubernetes DAQ application RTE choice"), + + daq_common : s.record("daq_common", [ + s.field( "data_request_timeout_ms", self.count, default=1000, doc="The baseline data request timeout that will be used by modules in the Readout and Trigger subsystems (i.e. any module that produces data fragments). Downstream timeouts, such as the trigger-record-building timeout, are derived from this."), + s.field( "use_data_network", self.flag, default = false, doc="Whether to use the data network (Won't work with k8s)"), + s.field( "data_rate_slowdown_factor",self.count, default=1, doc="Factor by which to suppress data generation. Former -s"), + ], doc="Common daq_common settings"), + +}; + +moo.oschema.sort_select(cs) diff --git a/schema/daqconf/dataflowgen.jsonnet b/schema/daqconf/dataflowgen.jsonnet new file mode 100644 index 00000000..b7a18c9a --- /dev/null +++ b/schema/daqconf/dataflowgen.jsonnet @@ -0,0 +1,63 @@ +// This is the configuration schema for daqconf_multiru_gen +// + +local moo = import "moo.jsonnet"; + +local s = moo.oschema.schema("dunedaq.daqconf.dataflowgen"); +local nc = moo.oschema.numeric_constraints; + +local cs = { + port: s.number( "Port", "i4", doc="A TCP/IP port number"), + freq: s.number( "Frequency", "u4", doc="A frequency"), + rate: s.number( "Rate", "f8", doc="A rate as a double"), + count: s.number( "count", "i8", doc="A count of things"), + three_choice: s.number( "threechoice", "i8", nc(minimum=0, exclusiveMaximum=3), doc="A choice between 0, 1, or 2"), + flag: s.boolean( "Flag", doc="Parameter that can be used to enable or disable functionality"), + monitoring_dest: s.enum( "MonitoringDest", ["local", "cern", "pocket"]), + path: s.string( "Path", doc="Location on a filesystem"), + paths: s.sequence( "Paths", self.path, doc="Multiple paths"), + host: s.string( "Host", moo.re.dnshost, doc="A hostname"), + hosts: s.sequence( "Hosts", self.host, "Multiple hosts"), + string: s.string( "Str", doc="Generic string"), + strings: s.sequence( "Strings", self.string, doc="List of strings"), + tpg_channel_map: s.enum( "TPGChannelMap", ["VDColdboxChannelMap", "ProtoDUNESP1ChannelMap", "PD2HDChannelMap", "HDColdboxChannelMap"]), + dqm_channel_map: s.enum( "DQMChannelMap", ['HD', 'VD', 'PD2HD', 'HDCB']), + dqm_params: s.sequence( "DQMParams", self.count, doc="Parameters for DQM (fixme)"), + tc_types: s.sequence( "TCTypes", self.count, doc="List of TC types"), + tc_type: s.number( "TCType", "i4", nc(minimum=0, maximum=9), doc="Number representing TC type. Currently ranging from 0 to 9"), + tc_interval: s.number( "TCInterval", "i8", nc(minimum=1, maximum=30000000000), doc="The intervals between TCs that are inserted into MLT by CTCM, in clock ticks"), + tc_intervals: s.sequence( "TCIntervals", self.tc_interval, doc="List of TC intervals used by CTCM"), + readout_time: s.number( "ROTime", "i8", doc="A readout time in ticks"), + channel_list: s.sequence( "ChannelList", self.count, doc="List of offline channels to be masked out from the TPHandler"), + tpg_algo_choice: s.enum( "TPGAlgoChoice", ["SimpleThreshold", "AbsRS"], doc="Trigger algorithm choice"), + pm_choice: s.enum( "PMChoice", ["k8s", "ssh"], doc="Process Manager choice: ssh or Kubernetes"), + rte_choice: s.enum( "RTEChoice", ["auto", "release", "devarea"], doc="Kubernetes DAQ application RTE choice"), + + + dataflowapp: s.record("dataflowapp",[ + s.field( "app_name", self.string, default="dataflow0"), + s.field( "output_paths",self.paths, default=['.'], doc="Location(s) for the dataflow app to write data. Former -o"), + s.field( "host_df", self.host, default='localhost'), + s.field( "max_file_size",self.count, default=4*1024*1024*1024, doc="The size threshold when raw data files are closed (in bytes)"), + s.field( "data_store_mode", self.string, default="all-per-file", doc="all-per-file or one-event-per-file"), + s.field( "max_trigger_record_window",self.count, default=0, doc="The maximum size for the window of data that will included in a single TriggerRecord (in ticks). Readout windows that are longer than this size will result in TriggerRecords being split into a sequence of TRs. A zero value for this parameter means no splitting."), + + ], doc="Element of the dataflow.apps array"), + + dataflowapps: s.sequence("dataflowapps", self.dataflowapp, doc="List of dataflowapp instances"), + + dataflow: s.record("dataflow", [ + s.field( "host_dfo", self.host, default='localhost', doc="Sets the host for the DFO app"), + s.field( "apps", self.dataflowapps, default=[], doc="Configuration for the dataflow apps (see dataflowapp for options)"), + s.field( "token_count",self.count, default=10, doc="Number of tokens the dataflow apps give to the DFO. Former -c"), + // Trigger + s.field( "host_tpw", self.host, default='localhost', doc='Host to run the TPWriter app on'), + s.field( "enable_tpset_writing", self.flag, default=false, doc="Enable the writing of TPs to disk (only works with enable_tpg or enable_firmware_tpg)"), + s.field( "tpset_output_path", self.path,default='.', doc="Output directory for TPSet stream files"), + s.field( "tpset_output_file_size",self.count, default=4*1024*1024*1024, doc="The size threshold when TPSet stream files are closed (in bytes)"), + ]), + +}; + + +moo.oschema.sort_select(cs) diff --git a/schema/daqconf/detectorgen.jsonnet b/schema/daqconf/detectorgen.jsonnet new file mode 100644 index 00000000..a7a0ab5d --- /dev/null +++ b/schema/daqconf/detectorgen.jsonnet @@ -0,0 +1,46 @@ +// This is the configuration schema for daqconf_multiru_gen +// + +local moo = import "moo.jsonnet"; + +local s = moo.oschema.schema("dunedaq.daqconf.detectorgen"); +local nc = moo.oschema.numeric_constraints; +// A temporary schema construction context. +local cs = { + port: s.number( "Port", "i4", doc="A TCP/IP port number"), + freq: s.number( "Frequency", "u4", doc="A frequency"), + rate: s.number( "Rate", "f8", doc="A rate as a double"), + count: s.number( "count", "i8", doc="A count of things"), + three_choice: s.number( "threechoice", "i8", nc(minimum=0, exclusiveMaximum=3), doc="A choice between 0, 1, or 2"), + flag: s.boolean( "Flag", doc="Parameter that can be used to enable or disable functionality"), + monitoring_dest: s.enum( "MonitoringDest", ["local", "cern", "pocket"]), + path: s.string( "Path", doc="Location on a filesystem"), + paths: s.sequence( "Paths", self.path, doc="Multiple paths"), + host: s.string( "Host", moo.re.dnshost, doc="A hostname"), + hosts: s.sequence( "Hosts", self.host, "Multiple hosts"), + string: s.string( "Str", doc="Generic string"), + strings: s.sequence( "Strings", self.string, doc="List of strings"), + tpg_channel_map: s.enum( "TPGChannelMap", ["VDColdboxChannelMap", "ProtoDUNESP1ChannelMap", "PD2HDChannelMap", "HDColdboxChannelMap"]), + dqm_channel_map: s.enum( "DQMChannelMap", ['HD', 'VD', 'PD2HD', 'HDCB']), + dqm_params: s.sequence( "DQMParams", self.count, doc="Parameters for DQM (fixme)"), + tc_types: s.sequence( "TCTypes", self.count, doc="List of TC types"), + tc_type: s.number( "TCType", "i4", nc(minimum=0, maximum=9), doc="Number representing TC type. Currently ranging from 0 to 9"), + tc_interval: s.number( "TCInterval", "i8", nc(minimum=1, maximum=30000000000), doc="The intervals between TCs that are inserted into MLT by CTCM, in clock ticks"), + tc_intervals: s.sequence( "TCIntervals", self.tc_interval, doc="List of TC intervals used by CTCM"), + readout_time: s.number( "ROTime", "i8", doc="A readout time in ticks"), + channel_list: s.sequence( "ChannelList", self.count, doc="List of offline channels to be masked out from the TPHandler"), + tpg_algo_choice: s.enum( "TPGAlgoChoice", ["SimpleThreshold", "AbsRS"], doc="Trigger algorithm choice"), + pm_choice: s.enum( "PMChoice", ["k8s", "ssh"], doc="Process Manager choice: ssh or Kubernetes"), + rte_choice: s.enum( "RTEChoice", ["auto", "release", "devarea"], doc="Kubernetes DAQ application RTE choice"), + + + detector : s.record("detector", [ + s.field( "op_env", self.string, default='swtest', doc="Operational environment - used for HDF5 Attribute inside the files"), + s.field( "clock_speed_hz", self.freq, default=62500000), + s.field( "tpg_channel_map", self.tpg_channel_map, default="PD2HDChannelMap", doc="Channel map for TPG"), + ], doc="Global common settings"), + + +}; + +moo.oschema.sort_select(cs) diff --git a/schema/daqconf/dqmgen.jsonnet b/schema/daqconf/dqmgen.jsonnet new file mode 100644 index 00000000..5778c3f6 --- /dev/null +++ b/schema/daqconf/dqmgen.jsonnet @@ -0,0 +1,57 @@ +// This is the configuration schema for daqconf_multiru_gen +// + +local moo = import "moo.jsonnet"; + +local s = moo.oschema.schema("dunedaq.daqconf.dqmgen"); +local nc = moo.oschema.numeric_constraints; + +local cs = { + port: s.number( "Port", "i4", doc="A TCP/IP port number"), + freq: s.number( "Frequency", "u4", doc="A frequency"), + rate: s.number( "Rate", "f8", doc="A rate as a double"), + count: s.number( "count", "i8", doc="A count of things"), + three_choice: s.number( "threechoice", "i8", nc(minimum=0, exclusiveMaximum=3), doc="A choice between 0, 1, or 2"), + flag: s.boolean( "Flag", doc="Parameter that can be used to enable or disable functionality"), + monitoring_dest: s.enum( "MonitoringDest", ["local", "cern", "pocket"]), + path: s.string( "Path", doc="Location on a filesystem"), + paths: s.sequence( "Paths", self.path, doc="Multiple paths"), + host: s.string( "Host", moo.re.dnshost, doc="A hostname"), + hosts: s.sequence( "Hosts", self.host, "Multiple hosts"), + string: s.string( "Str", doc="Generic string"), + strings: s.sequence( "Strings", self.string, doc="List of strings"), + tpg_channel_map: s.enum( "TPGChannelMap", ["VDColdboxChannelMap", "ProtoDUNESP1ChannelMap", "PD2HDChannelMap", "HDColdboxChannelMap"]), + dqm_channel_map: s.enum( "DQMChannelMap", ['HD', 'VD', 'PD2HD', 'HDCB']), + dqm_params: s.sequence( "DQMParams", self.count, doc="Parameters for DQM (fixme)"), + tc_types: s.sequence( "TCTypes", self.count, doc="List of TC types"), + tc_type: s.number( "TCType", "i4", nc(minimum=0, maximum=9), doc="Number representing TC type. Currently ranging from 0 to 9"), + tc_interval: s.number( "TCInterval", "i8", nc(minimum=1, maximum=30000000000), doc="The intervals between TCs that are inserted into MLT by CTCM, in clock ticks"), + tc_intervals: s.sequence( "TCIntervals", self.tc_interval, doc="List of TC intervals used by CTCM"), + readout_time: s.number( "ROTime", "i8", doc="A readout time in ticks"), + channel_list: s.sequence( "ChannelList", self.count, doc="List of offline channels to be masked out from the TPHandler"), + tpg_algo_choice: s.enum( "TPGAlgoChoice", ["SimpleThreshold", "AbsRS"], doc="Trigger algorithm choice"), + pm_choice: s.enum( "PMChoice", ["k8s", "ssh"], doc="Process Manager choice: ssh or Kubernetes"), + rte_choice: s.enum( "RTEChoice", ["auto", "release", "devarea"], doc="Kubernetes DAQ application RTE choice"), + + + + dqm: s.record("dqm", [ + s.field('enable_dqm', self.flag, default=false, doc="Enable Data Quality Monitoring"), + s.field('impl', self.monitoring_dest, default='local', doc="DQM destination (Kafka used for cern and pocket)"), + s.field('cmap', self.dqm_channel_map, default='HD', doc="Which channel map to use for DQM"), + s.field('host_dqm', self.hosts, default=['localhost'], doc='Host(s) to run the DQM app on'), + s.field('raw_params', self.dqm_params, default=[60, 50], doc="Parameters that control the data sent for the raw display plot"), + s.field('std_params', self.dqm_params, default=[10, 1000], doc="Parameters that control the data sent for the mean/rms plot"), + s.field('rms_params', self.dqm_params, default=[0, 1000], doc="Parameters that control the data sent for the mean/rms plot"), + s.field('fourier_channel_params', self.dqm_params, default=[0, 0], doc="Parameters that control the data sent for the fourier transform plot"), + s.field('fourier_plane_params', self.dqm_params, default=[600, 1000], doc="Parameters that control the data sent for the summed fourier transform plot"), + s.field('df_rate', self.count, default=10, doc='How many seconds between requests to DF for Trigger Records'), + s.field('df_algs', self.string, default='raw std fourier_plane', doc='Algorithms to be run on Trigger Records from DF (use quotes)'), + s.field('max_num_frames', self.count, default=32768, doc='Maximum number of frames to use in the algorithms'), + s.field('kafka_address', self.string, default='', doc='kafka address used to send messages'), + s.field('kafka_topic', self.string, default='DQM', doc='kafka topic used to send messages'), + ]), +}; + + +moo.oschema.sort_select(cs) diff --git a/schema/daqconf/hsigen.jsonnet b/schema/daqconf/hsigen.jsonnet new file mode 100644 index 00000000..9c3640ea --- /dev/null +++ b/schema/daqconf/hsigen.jsonnet @@ -0,0 +1,64 @@ +// This is the configuration schema for daqconf_multiru_gen +// + +local moo = import "moo.jsonnet"; + +local s = moo.oschema.schema("dunedaq.daqconf.hsigen"); +local nc = moo.oschema.numeric_constraints; + +local cs = { + port: s.number( "Port", "i4", doc="A TCP/IP port number"), + freq: s.number( "Frequency", "u4", doc="A frequency"), + rate: s.number( "Rate", "f8", doc="A rate as a double"), + count: s.number( "count", "i8", doc="A count of things"), + three_choice: s.number( "threechoice", "i8", nc(minimum=0, exclusiveMaximum=3), doc="A choice between 0, 1, or 2"), + flag: s.boolean( "Flag", doc="Parameter that can be used to enable or disable functionality"), + monitoring_dest: s.enum( "MonitoringDest", ["local", "cern", "pocket"]), + path: s.string( "Path", doc="Location on a filesystem"), + paths: s.sequence( "Paths", self.path, doc="Multiple paths"), + host: s.string( "Host", moo.re.dnshost, doc="A hostname"), + hosts: s.sequence( "Hosts", self.host, "Multiple hosts"), + string: s.string( "Str", doc="Generic string"), + strings: s.sequence( "Strings", self.string, doc="List of strings"), + tpg_channel_map: s.enum( "TPGChannelMap", ["VDColdboxChannelMap", "ProtoDUNESP1ChannelMap", "PD2HDChannelMap", "HDColdboxChannelMap"]), + dqm_channel_map: s.enum( "DQMChannelMap", ['HD', 'VD', 'PD2HD', 'HDCB']), + dqm_params: s.sequence( "DQMParams", self.count, doc="Parameters for DQM (fixme)"), + tc_types: s.sequence( "TCTypes", self.count, doc="List of TC types"), + tc_type: s.number( "TCType", "i4", nc(minimum=0, maximum=9), doc="Number representing TC type. Currently ranging from 0 to 9"), + tc_interval: s.number( "TCInterval", "i8", nc(minimum=1, maximum=30000000000), doc="The intervals between TCs that are inserted into MLT by CTCM, in clock ticks"), + tc_intervals: s.sequence( "TCIntervals", self.tc_interval, doc="List of TC intervals used by CTCM"), + readout_time: s.number( "ROTime", "i8", doc="A readout time in ticks"), + channel_list: s.sequence( "ChannelList", self.count, doc="List of offline channels to be masked out from the TPHandler"), + tpg_algo_choice: s.enum( "TPGAlgoChoice", ["SimpleThreshold", "AbsRS"], doc="Trigger algorithm choice"), + pm_choice: s.enum( "PMChoice", ["k8s", "ssh"], doc="Process Manager choice: ssh or Kubernetes"), + rte_choice: s.enum( "RTEChoice", ["auto", "release", "devarea"], doc="Kubernetes DAQ application RTE choice"), + + + hsi: s.record("hsi", [ + # timing hsi options + s.field( "use_timing_hsi", self.flag, default=false, doc='Flag to control whether real hardware timing HSI config is generated. Default is false'), + s.field( "host_timing_hsi", self.host, default='localhost', doc='Host to run the HSI app on'), + s.field( "hsi_hw_connections_file", self.path, default="${TIMING_SHARE}/config/etc/connections.xml", doc='Real timing hardware only: path to hardware connections file'), + s.field( "enable_hardware_state_recovery", self.flag, default=true, doc="Enable (or not) hardware state recovery"), + s.field( "hsi_device_name", self.string, default="", doc='Real HSI hardware only: device name of HSI hw'), + s.field( "hsi_readout_period", self.count, default=1e3, doc='Real HSI hardware only: Period between HSI hardware polling [us]'), + s.field( "control_hsi_hw", self.flag, default=false, doc='Flag to control whether we are controlling hsi hardware'), + s.field( "hsi_endpoint_address", self.count, default=1, doc='Timing address of HSI endpoint'), + s.field( "hsi_endpoint_partition", self.count, default=0, doc='Timing partition of HSI endpoint'), + s.field( "hsi_re_mask",self.count, default=0, doc='Rising-edge trigger mask'), + s.field( "hsi_fe_mask", self.count, default=0, doc='Falling-edge trigger mask'), + s.field( "hsi_inv_mask",self.count, default=0, doc='Invert-edge mask'), + s.field( "hsi_source",self.count, default=1, doc='HSI signal source; 0 - hardware, 1 - emulation (trigger timestamp bits)'), + # fake hsi options + s.field( "use_fake_hsi", self.flag, default=true, doc='Flag to control whether fake or real hardware HSI config is generated. Default is true'), + s.field( "host_fake_hsi", self.host, default='localhost', doc='Host to run the HSI app on'), + s.field( "hsi_device_id", self.count, default=0, doc='Fake HSI only: device ID of fake HSIEvents'), + s.field( "mean_hsi_signal_multiplicity", self.count, default=1, doc='Fake HSI only: rate of individual HSI signals in emulation mode 1'), + s.field( "hsi_signal_emulation_mode", self.count, default=0, doc='Fake HSI only: HSI signal emulation mode'), + s.field( "enabled_hsi_signals", self.count, default=1, doc='Fake HSI only: bit mask of enabled fake HSI signals') + ]), + +}; + + +moo.oschema.sort_select(cs) diff --git a/schema/daqconf/readoutgen.jsonnet b/schema/daqconf/readoutgen.jsonnet new file mode 100644 index 00000000..68687168 --- /dev/null +++ b/schema/daqconf/readoutgen.jsonnet @@ -0,0 +1,91 @@ +// This is the configuration schema for daqconf_multiru_gen +// + +local moo = import "moo.jsonnet"; + +local s = moo.oschema.schema("dunedaq.daqconf.readoutgen"); +local nc = moo.oschema.numeric_constraints; +// A temporary schema construction context. +local cs = { + port: s.number( "Port", "i4", doc="A TCP/IP port number"), + freq: s.number( "Frequency", "u4", doc="A frequency"), + rate: s.number( "Rate", "f8", doc="A rate as a double"), + count: s.number( "count", "i8", doc="A count of things"), + three_choice: s.number( "threechoice", "i8", nc(minimum=0, exclusiveMaximum=3), doc="A choice between 0, 1, or 2"), + flag: s.boolean( "Flag", doc="Parameter that can be used to enable or disable functionality"), + monitoring_dest: s.enum( "MonitoringDest", ["local", "cern", "pocket"]), + path: s.string( "Path", doc="Location on a filesystem"), + paths: s.sequence( "Paths", self.path, doc="Multiple paths"), + host: s.string( "Host", moo.re.dnshost, doc="A hostname"), + hosts: s.sequence( "Hosts", self.host, "Multiple hosts"), + string: s.string( "Str", doc="Generic string"), + strings: s.sequence( "Strings", self.string, doc="List of strings"), + tpg_channel_map: s.enum( "TPGChannelMap", ["VDColdboxChannelMap", "ProtoDUNESP1ChannelMap", "PD2HDChannelMap", "HDColdboxChannelMap"]), + dqm_channel_map: s.enum( "DQMChannelMap", ['HD', 'VD', 'PD2HD', 'HDCB']), + dqm_params: s.sequence( "DQMParams", self.count, doc="Parameters for DQM (fixme)"), + tc_types: s.sequence( "TCTypes", self.count, doc="List of TC types"), + tc_type: s.number( "TCType", "i4", nc(minimum=0, maximum=9), doc="Number representing TC type. Currently ranging from 0 to 9"), + tc_interval: s.number( "TCInterval", "i8", nc(minimum=1, maximum=30000000000), doc="The intervals between TCs that are inserted into MLT by CTCM, in clock ticks"), + tc_intervals: s.sequence( "TCIntervals", self.tc_interval, doc="List of TC intervals used by CTCM"), + readout_time: s.number( "ROTime", "i8", doc="A readout time in ticks"), + channel_list: s.sequence( "ChannelList", self.count, doc="List of offline channels to be masked out from the TPHandler"), + tpg_algo_choice: s.enum( "TPGAlgoChoice", ["SimpleThreshold", "AbsRS"], doc="Trigger algorithm choice"), + pm_choice: s.enum( "PMChoice", ["k8s", "ssh"], doc="Process Manager choice: ssh or Kubernetes"), + rte_choice: s.enum( "RTEChoice", ["auto", "release", "devarea"], doc="Kubernetes DAQ application RTE choice"), + + + data_file_entry: s.record("data_file_entry", [ + s.field( "data_file", self.path, default='./frames.bin', doc="File containing data frames to be replayed by the fake cards. Former -d. Uses the asset manager, can also be 'asset://checksum/somelonghash', or 'file://somewhere/frames.bin' or 'frames.bin'"), + s.field( "detector_id", self.count, default=3, doc="Detector ID that this file applies to"), + ]), + data_files: s.sequence("data_files", self.data_file_entry), + + numa_exception: s.record( "NUMAException", [ + s.field( "host", self.host, default='localhost', doc="Host of exception"), + s.field( "card", self.count, default=0, doc="Card ID of exception"), + s.field( "numa_id", self.count, default=0, doc="NUMA ID of exception"), + s.field( "felix_card_id", self.count, default=-1, doc="CARD ID override, -1 indicates no override"), + s.field( "latency_buffer_numa_aware", self.flag, default=false, doc="Enable NUMA-aware mode for the Latency Buffer"), + s.field( "latency_buffer_preallocation", self.flag, default=false, doc="Enable Latency Buffer preallocation"), + ], doc="Exception to the default NUMA ID for FELIX cards"), + + numa_exceptions: s.sequence( "NUMAExceptions", self.numa_exception, doc="Exceptions to the default NUMA ID"), + + numa_config: s.record("numa_config", [ + s.field( "default_id", self.count, default=0, doc="Default NUMA ID for FELIX cards"), + s.field( "default_latency_numa_aware", self.flag, default=false, doc="Default for Latency Buffer NUMA awareness"), + s.field( "default_latency_preallocation", self.flag, default=false, doc="Default for Latency Buffer Preallocation"), + s.field( "exceptions", self.numa_exceptions, default=[], doc="Exceptions to the default NUMA ID"), + ]), + + readout: s.record("readout", [ + s.field( "detector_readout_map_file", self.path, default='./DetectorReadoutMap.json', doc="File containing detector hardware map for configuration to run"), + s.field( "use_fake_data_producers", self.flag, default=false, doc="Use fake data producers that respond with empty fragments immediately instead of (fake) cards and DLHs"), + // s.field( "memory_limit_gb", self.count, default=64, doc="Application memory limit in GB") + // Fake cards + s.field( "use_fake_cards", self.flag, default=false, doc="Use fake cards"), + s.field( "emulated_data_times_start_with_now", self.flag, default=false, doc="If active, the timestamp of the first emulated data frame is set to the current wallclock time"), + s.field( "default_data_file", self.path, default='asset://?label=ProtoWIB&subsystem=readout', doc="File containing data frames to be replayed by the fake cards. Former -d. Uses the asset manager, can also be 'asset://?checksum=somelonghash', or 'file://somewhere/frames.bin' or 'frames.bin'"), + s.field( "data_files", self.data_files, default=[], doc="Files to use by detector type"), + // DPDK + s.field( "dpdk_eal_args", self.string, default='-l 0-1 -n 3 -- -m [0:1].0 -j', doc='Args passed to the EAL in DPDK'), + s.field( "dpdk_rxqueues_per_lcore", self.count, default=1, doc='Number of rx queues per core'), + // FLX + s.field( "numa_config", self.numa_config, default=self.numa_config, doc='Configuration of FELIX NUMA IDs'), + // DLH + s.field( "emulator_mode", self.flag, default=false, doc="If active, timestamps of data frames are overwritten when processed by the readout. This is necessary if the felix card does not set correct timestamps. Former -e"), + s.field( "thread_pinning_file", self.path, default="", doc="A thread pinning configuration file that gets executed after conf."), + // s.field( "data_rate_slowdown_factor",self.count, default=1, doc="Factor by which to suppress data generation. Former -s"), + s.field( "latency_buffer_size", self.count, default=499968, doc="Size of the latency buffers (in number of elements)"), + s.field( "fragment_send_timeout_ms", self.count, default=10, doc="The send timeout that will be used in the readout modules when sending fragments downstream (i.e. to the TRB)."), + s.field( "enable_tpg", self.flag, default=false, doc="Enable TPG"), + s.field( "tpg_threshold", self.count, default=120, doc="Select TPG threshold"), + s.field( "tpg_algorithm", self.string, default="SimpleThreshold", doc="Select TPG algorithm (SimpleThreshold, AbsRS)"), + s.field( "tpg_channel_mask", self.channel_list, default=[], doc="List of offline channels to be masked out from the TPHandler"), + s.field( "enable_raw_recording", self.flag, default=false, doc="Add queues and modules necessary for the record command"), + s.field( "raw_recording_output_dir", self.path, default='.', doc="Output directory where recorded data is written to. Data for each link is written to a separate file") + ]), + +}; + +moo.oschema.sort_select(cs) diff --git a/schema/daqconf/timinggen.jsonnet b/schema/daqconf/timinggen.jsonnet new file mode 100644 index 00000000..69249a59 --- /dev/null +++ b/schema/daqconf/timinggen.jsonnet @@ -0,0 +1,50 @@ +// This is the configuration schema for daqconf_multiru_gen +// + +local moo = import "moo.jsonnet"; + +local s = moo.oschema.schema("dunedaq.daqconf.timinggen"); +local nc = moo.oschema.numeric_constraints; + +local cs = { + port: s.number( "Port", "i4", doc="A TCP/IP port number"), + freq: s.number( "Frequency", "u4", doc="A frequency"), + rate: s.number( "Rate", "f8", doc="A rate as a double"), + count: s.number( "count", "i8", doc="A count of things"), + three_choice: s.number( "threechoice", "i8", nc(minimum=0, exclusiveMaximum=3), doc="A choice between 0, 1, or 2"), + flag: s.boolean( "Flag", doc="Parameter that can be used to enable or disable functionality"), + monitoring_dest: s.enum( "MonitoringDest", ["local", "cern", "pocket"]), + path: s.string( "Path", doc="Location on a filesystem"), + paths: s.sequence( "Paths", self.path, doc="Multiple paths"), + host: s.string( "Host", moo.re.dnshost, doc="A hostname"), + hosts: s.sequence( "Hosts", self.host, "Multiple hosts"), + string: s.string( "Str", doc="Generic string"), + strings: s.sequence( "Strings", self.string, doc="List of strings"), + tpg_channel_map: s.enum( "TPGChannelMap", ["VDColdboxChannelMap", "ProtoDUNESP1ChannelMap", "PD2HDChannelMap", "HDColdboxChannelMap"]), + dqm_channel_map: s.enum( "DQMChannelMap", ['HD', 'VD', 'PD2HD', 'HDCB']), + dqm_params: s.sequence( "DQMParams", self.count, doc="Parameters for DQM (fixme)"), + tc_types: s.sequence( "TCTypes", self.count, doc="List of TC types"), + tc_type: s.number( "TCType", "i4", nc(minimum=0, maximum=9), doc="Number representing TC type. Currently ranging from 0 to 9"), + tc_interval: s.number( "TCInterval", "i8", nc(minimum=1, maximum=30000000000), doc="The intervals between TCs that are inserted into MLT by CTCM, in clock ticks"), + tc_intervals: s.sequence( "TCIntervals", self.tc_interval, doc="List of TC intervals used by CTCM"), + readout_time: s.number( "ROTime", "i8", doc="A readout time in ticks"), + channel_list: s.sequence( "ChannelList", self.count, doc="List of offline channels to be masked out from the TPHandler"), + tpg_algo_choice: s.enum( "TPGAlgoChoice", ["SimpleThreshold", "AbsRS"], doc="Trigger algorithm choice"), + pm_choice: s.enum( "PMChoice", ["k8s", "ssh"], doc="Process Manager choice: ssh or Kubernetes"), + rte_choice: s.enum( "RTEChoice", ["auto", "release", "devarea"], doc="Kubernetes DAQ application RTE choice"), + + timing: s.record("timing", [ + s.field( "timing_session_name", self.string, default="", doc="Name of the global timing session to use, for timing commands"), + s.field( "host_tprtc", self.host, default='localhost', doc='Host to run the timing partition controller app on'), + # timing hw partition options + s.field( "control_timing_partition", self.flag, default=false, doc='Flag to control whether we are controlling timing partition in master hardware'), + s.field( "timing_partition_master_device_name", self.string, default="", doc='Timing partition master hardware device name'), + s.field( "timing_partition_id", self.count, default=0, doc='Timing partition id'), + s.field( "timing_partition_trigger_mask", self.count, default=255, doc='Timing partition trigger mask'), + s.field( "timing_partition_rate_control_enabled", self.flag, default=false, doc='Timing partition rate control enabled'), + s.field( "timing_partition_spill_gate_enabled", self.flag, default=false, doc='Timing partition spill gate enabled'), + ]), +}; + + +moo.oschema.sort_select(cs) diff --git a/schema/daqconf/triggergen.jsonnet b/schema/daqconf/triggergen.jsonnet new file mode 100644 index 00000000..256c6932 --- /dev/null +++ b/schema/daqconf/triggergen.jsonnet @@ -0,0 +1,147 @@ +// This is the configuration schema for daqconf_multiru_gen +// + +local moo = import "moo.jsonnet"; + +local s = moo.oschema.schema("dunedaq.daqconf.triggergen"); +local nc = moo.oschema.numeric_constraints; +// A temporary schema construction context. +local cs = { + port: s.number( "Port", "i4", doc="A TCP/IP port number"), + freq: s.number( "Frequency", "u4", doc="A frequency"), + rate: s.number( "Rate", "f8", doc="A rate as a double"), + count: s.number( "count", "i8", doc="A count of things"), + three_choice: s.number( "threechoice", "i8", nc(minimum=0, exclusiveMaximum=3), doc="A choice between 0, 1, or 2"), + flag: s.boolean( "Flag", doc="Parameter that can be used to enable or disable functionality"), + monitoring_dest: s.enum( "MonitoringDest", ["local", "cern", "pocket"]), + path: s.string( "Path", doc="Location on a filesystem"), + paths: s.sequence( "Paths", self.path, doc="Multiple paths"), + host: s.string( "Host", moo.re.dnshost, doc="A hostname"), + hosts: s.sequence( "Hosts", self.host, "Multiple hosts"), + string: s.string( "Str", doc="Generic string"), + strings: s.sequence( "Strings", self.string, doc="List of strings"), + tpg_channel_map: s.enum( "TPGChannelMap", ["VDColdboxChannelMap", "ProtoDUNESP1ChannelMap", "PD2HDChannelMap", "HDColdboxChannelMap"]), + dqm_channel_map: s.enum( "DQMChannelMap", ['HD', 'VD', 'PD2HD', 'HDCB']), + dqm_params: s.sequence( "DQMParams", self.count, doc="Parameters for DQM (fixme)"), + tc_types: s.sequence( "TCTypes", self.count, doc="List of TC types"), + tc_type: s.number( "TCType", "i4", nc(minimum=0, maximum=9), doc="Number representing TC type. Currently ranging from 0 to 9"), + tc_interval: s.number( "TCInterval", "i8", nc(minimum=1, maximum=30000000000), doc="The intervals between TCs that are inserted into MLT by CTCM, in clock ticks"), + tc_intervals: s.sequence( "TCIntervals", self.tc_interval, doc="List of TC intervals used by CTCM"), + readout_time: s.number( "ROTime", "i8", doc="A readout time in ticks"), + channel_list: s.sequence( "ChannelList", self.count, doc="List of offline channels to be masked out from the TPHandler"), + tpg_algo_choice: s.enum( "TPGAlgoChoice", ["SimpleThreshold", "AbsRS"], doc="Trigger algorithm choice"), + pm_choice: s.enum( "PMChoice", ["k8s", "ssh"], doc="Process Manager choice: ssh or Kubernetes"), + rte_choice: s.enum( "RTEChoice", ["auto", "release", "devarea"], doc="Kubernetes DAQ application RTE choice"), + + trigger_algo_config: s.record("trigger_algo_config", [ + s.field("prescale", self.count, default=100), + s.field("window_length", self.count, default=10000), + s.field("adjacency_threshold", self.count, default=6), + s.field("adj_tolerance", self.count, default=4), + s.field("trigger_on_adc", self.flag, default=false), + s.field("trigger_on_n_channels", self.flag, default=false), + s.field("trigger_on_adjacency", self.flag, default=true), + s.field("adc_threshold", self.count, default=10000), + s.field("n_channels_threshold", self.count, default=8), + s.field("print_tp_info", self.flag, default=false), + ]), + + c0_readout: s.record("c0_readout", [ + s.field("candidate_type", self.tc_type, default=0, doc="The TC type, 0=Unknown"), + s.field("time_before", self.readout_time, default=1000, doc="Time to readout before TC time [ticks]"), + s.field("time_after", self.readout_time, default=1001, doc="Time to readout after TC time [ticks]"), + ]), + c1_readout: s.record("c1_readout", [ + s.field("candidate_type", self.tc_type, default=1, doc="The TC type, 1=Timing"), + s.field("time_before", self.readout_time, default=1000, doc="Time to readout before TC time [ticks]"), + s.field("time_after", self.readout_time, default=1001, doc="Time to readout after TC time [ticks]"), + ]), + c2_readout: s.record("c2_readout", [ + s.field("candidate_type", self.tc_type, default=2, doc="The TC type, 2=TPCLowE"), + s.field("time_before", self.readout_time, default=1000, doc="Time to readout before TC time [ticks]"), + s.field("time_after", self.readout_time, default=1001, doc="Time to readout after TC time [ticks]"), + ]), + c3_readout: s.record("c3_readout", [ + s.field("candidate_type", self.tc_type, default=3, doc="The TC type, 3=Supernova"), + s.field("time_before", self.readout_time, default=1000, doc="Time to readout before TC time [ticks]"), + s.field("time_after", self.readout_time, default=1001, doc="Time to readout after TC time [ticks]"), + ]), + c4_readout: s.record("c4_readout", [ + s.field("candidate_type", self.tc_type, default=4, doc="The TC type, 4=Random"), + s.field("time_before", self.readout_time, default=1000, doc="Time to readout before TC time [ticks]"), + s.field("time_after", self.readout_time, default=1001, doc="Time to readout after TC time [ticks]"), + ]), + c5_readout: s.record("c5_readout", [ + s.field("candidate_type", self.tc_type, default=5, doc="The TC type, 5=Prescale"), + s.field("time_before", self.readout_time, default=1000, doc="Time to readout before TC time [ticks]"), + s.field("time_after", self.readout_time, default=1001, doc="Time to readout after TC time [ticks]"), + ]), + c6_readout: s.record("c6_readout", [ + s.field("candidate_type", self.tc_type, default=6, doc="The TC type, 6=ADCSimpleWindow"), + s.field("time_before", self.readout_time, default=1000, doc="Time to readout before TC time [ticks]"), + s.field("time_after", self.readout_time, default=1001, doc="Time to readout after TC time [ticks]"), + ]), + c7_readout: s.record("c7_readout", [ + s.field("candidate_type", self.tc_type, default=7, doc="The TC type, 7=HorizontalMuon"), + s.field("time_before", self.readout_time, default=1000, doc="Time to readout before TC time [ticks]"), + s.field("time_after", self.readout_time, default=1001, doc="Time to readout after TC time [ticks]"), + ]), + c8_readout: s.record("c8_readout", [ + s.field("candidate_type", self.tc_type, default=8, doc="The TC type, 8=MichelElectron"), + s.field("time_before", self.readout_time, default=1000, doc="Time to readout before TC time [ticks]"), + s.field("time_after", self.readout_time, default=1001, doc="Time to readout after TC time [ticks]"), + ]), + c9_readout: s.record("c9_readout", [ + s.field("candidate_type", self.tc_type, default=9, doc="The TC type, 9=LowEnergyEvent"), + s.field("time_before", self.readout_time, default=1000, doc="Time to readout before TC time [ticks]"), + s.field("time_after", self.readout_time, default=1001, doc="Time to readout after TC time [ticks]"), + ]), + + tc_readout_map: s.record("tc_readout_map", [ + s.field("c0", self.c0_readout, default=self.c0_readout, doc="TC readout for TC type 0"), + s.field("c1", self.c1_readout, default=self.c1_readout, doc="TC readout for TC type 1"), + s.field("c2", self.c2_readout, default=self.c2_readout, doc="TC readout for TC type 2"), + s.field("c3", self.c3_readout, default=self.c3_readout, doc="TC readout for TC type 3"), + s.field("c4", self.c4_readout, default=self.c4_readout, doc="TC readout for TC type 4"), + s.field("c5", self.c5_readout, default=self.c5_readout, doc="TC readout for TC type 5"), + s.field("c6", self.c6_readout, default=self.c6_readout, doc="TC readout for TC type 6"), + s.field("c7", self.c7_readout, default=self.c7_readout, doc="TC readout for TC type 7"), + s.field("c8", self.c8_readout, default=self.c8_readout, doc="TC readout for TC type 8"), + s.field("c9", self.c9_readout, default=self.c9_readout, doc="TC readout for TC type 9"), + ]), + + trigger: s.record("trigger",[ + s.field( "trigger_rate_hz", self.rate, default=1.0, doc='Fake HSI only: rate at which fake HSIEvents are sent. 0 - disable HSIEvent generation. Former -t'), + s.field( "trigger_window_before_ticks",self.count, default=1000, doc="Trigger window before marker. Former -b"), + s.field( "trigger_window_after_ticks", self.count, default=1000, doc="Trigger window after marker. Former -a"), + s.field( "host_trigger", self.host, default='localhost', doc='Host to run the trigger app on'), + // s.field( "host_tpw", self.host, default='localhost', doc='Host to run the TPWriter app on'), + # trigger options + s.field( "completeness_tolerance", self.count, default=1, doc="Maximum number of inactive queues we will tolerate."), + s.field( "tolerate_incompleteness", self.flag, default=false, doc="Flag to tell trigger to tolerate inactive queues."), + s.field( "ttcm_s1", self.count,default=1, doc="Timing trigger candidate maker accepted HSI signal ID 1"), + s.field( "ttcm_s2", self.count, default=2, doc="Timing trigger candidate maker accepted HSI signal ID 2"), + s.field( "trigger_activity_plugin", self.string, default='TriggerActivityMakerPrescalePlugin', doc="Trigger activity algorithm plugin"), + s.field( "trigger_activity_config", self.trigger_algo_config, default=self.trigger_algo_config,doc="Trigger activity algorithm config (string containing python dictionary)"), + s.field( "trigger_candidate_plugin", self.string, default='TriggerCandidateMakerPrescalePlugin', doc="Trigger candidate algorithm plugin"), + s.field( "trigger_candidate_config", self.trigger_algo_config, default=self.trigger_algo_config, doc="Trigger candidate algorithm config (string containing python dictionary)"), + s.field( "hsi_trigger_type_passthrough", self.flag, default=false, doc="Option to override trigger type in the MLT"), + // s.field( "enable_tpset_writing", self.flag, default=false, doc="Enable the writing of TPs to disk (only works with enable_tpg or enable_firmware_tpg)"), + // s.field( "tpset_output_path", self.path,default='.', doc="Output directory for TPSet stream files"), + // s.field( "tpset_output_file_size",self.count, default=4*1024*1024*1024, doc="The size threshold when TPSet stream files are closed (in bytes)"), + // s.field( "tpg_channel_map", self.tpg_channel_map, default="ProtoDUNESP1ChannelMap", doc="Channel map for TPG"), + s.field( "mlt_merge_overlapping_tcs", self.flag, default=true, doc="Option to turn off merging of overlapping TCs when forming TDs in MLT"), + s.field( "mlt_buffer_timeout", self.count, default=100, doc="Timeout (buffer) to wait for new overlapping TCs before sending TD"), + s.field( "mlt_send_timed_out_tds", self.flag, default=true, doc="Option to drop TD if TC comes out of timeout window"), + s.field( "mlt_max_td_length_ms", self.count, default=1000, doc="Maximum allowed time length [ms] for a readout window of a single TD"), + s.field( "mlt_ignore_tc", self.tc_types, default=[], doc="Optional list of TC types to be ignored in MLT"), + s.field( "mlt_use_readout_map", self.flag, default=false, doc="Option to use custom readout map in MLT"), + s.field( "mlt_td_readout_map", self.tc_readout_map, default=self.tc_readout_map, doc="The readout windows assigned to TDs in MLT, based on TC type."), + s.field( "use_custom_maker", self.flag, default=false, doc="Option to use a Custom Trigger Candidate Maker (plugin)"), + s.field( "ctcm_trigger_types", self.tc_types, default=[4], doc="Optional list of TC types to be used by the Custom Trigger Candidate Maker (plugin)"), + s.field( "ctcm_trigger_intervals", self.tc_intervals, default=[10000000], doc="Optional list of intervals (clock ticks) for the TC types to be used by the Custom Trigger Candidate Maker (plugin)"), + ]), + +}; + +moo.oschema.sort_select(cs) diff --git a/scripts/daqconf_multiru_gen b/scripts/daqconf_multiru_gen index a7e2ebfd..11a38634 100755 --- a/scripts/daqconf_multiru_gen +++ b/scripts/daqconf_multiru_gen @@ -47,40 +47,49 @@ def expand_conf(config_data, debug=False): """ import dunedaq.daqconf.confgen as confgen + import dunedaq.daqconf.bootgen as bootgen + import dunedaq.daqconf.detectorgen as detectorgen + import dunedaq.daqconf.daqcommongen as daqcommongen + import dunedaq.daqconf.timinggen as timinggen + import dunedaq.daqconf.hsigen as hsigen + import dunedaq.daqconf.readoutgen as readoutgen + import dunedaq.daqconf.triggergen as triggergen + import dunedaq.daqconf.dataflowgen as dataflowgen + import dunedaq.daqconf.dqmgen as dqmgen ## Hack, we shouldn't need to do that, in the future it should be, boot = config_data.boot - boot = confgen.boot(**config_data.boot) + boot = bootgen.boot(**config_data.boot) if debug: console.log(f"boot configuration object: {boot.pod()}") - detector = confgen.detector(**config_data.detector) + detector = detectorgen.detector(**config_data.detector) if debug: console.log(f"detector configuration object: {detector.pod()}") - daq_common = confgen.daq_common(**config_data.daq_common) + daq_common = daqcommongen.daq_common(**config_data.daq_common) if debug: console.log(f"daq_common configuration object: {daq_common.pod()}") - timing = confgen.timing(**config_data.timing) + timing = timinggen.timing(**config_data.timing) if debug: console.log(f"timing configuration object: {timing.pod()}") - hsi = confgen.hsi(**config_data.hsi) + hsi = hsigen.hsi(**config_data.hsi) if debug: console.log(f"hsi configuration object: {hsi.pod()}") ctb_hsi = confgen.ctb_hsi(**config_data.ctb_hsi) if debug: console.log(f"ctb_hsi configuration object: {ctb_hsi.pod()}") - readout = confgen.readout(**config_data.readout) + readout = readoutgen.readout(**config_data.readout) if debug: console.log(f"readout configuration object: {readout.pod()}") - trigger = confgen.trigger(**config_data.trigger) + trigger = triggergen.trigger(**config_data.trigger) if debug: console.log(f"trigger configuration object: {trigger.pod()}") - dataflow = confgen.dataflow(**config_data.dataflow) + dataflow = dataflowgen.dataflow(**config_data.dataflow) if debug: console.log(f"dataflow configuration object: {dataflow.pod()}") - dqm = confgen.dqm(**config_data.dqm) + dqm = dqmgen.dqm(**config_data.dqm) if debug: console.log(f"dqm configuration object: {dqm.pod()}") - dpdk_sender = confgen.dpdk_sender(**config_data.dpdk_sender) - if debug: console.log(f"dpdk_sender configuration object: {dpdk_sender.pod()}") + # dpdk_sender = confgen.dpdk_sender(**config_data.dpdk_sender) + # if debug: console.log(f"dpdk_sender configuration object: {dpdk_sender.pod()}") return ( boot, @@ -93,7 +102,7 @@ def expand_conf(config_data, debug=False): trigger, dataflow, dqm, - dpdk_sender + # dpdk_sender ) def validate_conf(boot, readout, dataflow, timing, hsi, dqm): @@ -143,11 +152,11 @@ def create_df_apps( sourceid_broker ): - import dunedaq.daqconf.confgen as confgen + import dunedaq.daqconf.dataflowgen as dataflowgen if len(dataflow.apps) == 0: console.log(f"No Dataflow apps defined, adding default dataflow0") - dataflow.apps = [confgen.dataflowapp()] + dataflow.apps = [dataflowgen.dataflowapp()] host_df = [] appconfig_df = {} @@ -156,7 +165,7 @@ def create_df_apps( console.log(f"Parsing dataflow app config {d}") ## Hack, we shouldn't need to do that, in the future, it should be appconfig = d - appconfig = confgen.dataflowapp(**d) + appconfig = dataflowgen.dataflowapp(**d) dfapp = appconfig.app_name if dfapp in df_app_names: @@ -173,16 +182,16 @@ def create_df_apps( # Add -h as default help option CONTEXT_SETTINGS = dict(help_option_names=['-h', '--help']) @click.command(context_settings=CONTEXT_SETTINGS) -@generate_cli_from_schema('daqconf/confgen.jsonnet', 'daqconf_multiru_gen', 'dataflowapp') +@generate_cli_from_schema('daqconf/confgen.jsonnet', 'daqconf_multiru_gen', 'daqconf.dataflowgen.dataflowapp') @click.option('--force-pm', default=None, type=click.Choice(['ssh', 'k8s']), help="Force process manager") @click.option('--base-command-port', type=int, default=None, help="Base port of application command endpoints") @click.option('-m', '--detector-readout-map-file', default=None, help="File containing detector detector-readout map for configuration to run") @click.option('-s', '--data-rate-slowdown-factor', default=0, help="Scale factor for readout internal clock to generate less data") @click.option('--file-label', default=None, help="File - used for raw data filename prefix") @click.option('--enable-dqm', default=False, is_flag=True, help="Enable generation of DQM apps") -@click.option('-a', '--only-check-args', default=False, is_flag=True, help="Check input arguments and quit") +@click.option('-a', '--check-args-and-exit', default=False, is_flag=True, help="Check input arguments and quit") @click.option('-n', '--dry-run', default=False, is_flag=True, help="Dry run, do not generate output files") -@click.option('-f', '--force', default=False, is_flag=True, help="Force configuration generation - delete ") +@click.option('-f', '--force', default=False, is_flag=True, help="Force configuration generation - delete existing target directory if exists") @click.option('--debug', default=False, is_flag=True, help="Switch to get a lot of printout and dot files") @click.argument('json_dir', type=click.Path()) def cli( @@ -193,15 +202,15 @@ def cli( data_rate_slowdown_factor, enable_dqm, file_label, - only_check_args, + check_args_and_exit, dry_run, force, debug, json_dir ): - console.log("Commandline parsing completed") - if only_check_args: + # console.log("Commandline parsing completed") + if check_args_and_exit: return output_dir = Path(json_dir) @@ -237,7 +246,7 @@ def cli( trigger, dataflow, dqm, - dpdk_sender + # dpdk_sender ) = expand_conf(config_data, debug) # @@ -269,7 +278,9 @@ def cli( file_label = file_label if file_label is not None else detector.op_env - + #-------------------------------------------------------------------------- + # Validate configuration + #-------------------------------------------------------------------------- validate_conf(boot, readout, dataflow, timing, hsi, dqm) @@ -294,7 +305,8 @@ def cli( console.log("Loading timing partition controller config generator") from daqconf.apps.tprtc_gen import get_tprtc_app console.log("Loading DPDK sender config generator") - from daqconf.apps.dpdk_sender_gen import get_dpdk_sender_app + # from daqconf.apps.dpdk_sender_gen import get_dpdk_sender_app + if dataflow.enable_tpset_writing: console.log("Loading TPWriter config generator") from daqconf.apps.tpwriter_gen import get_tpwriter_app @@ -615,10 +627,10 @@ def cli( if debug: console.log(f"{tpw_name} app: {the_system.apps[tpw_name]}") - if dpdk_sender.enable_dpdk_sender: - the_system.apps["dpdk_sender"] = get_dpdk_sender_app( - HOST=dpdk_sender.host_dpdk_sender[0], - ) + # if dpdk_sender.enable_dpdk_sender: + # the_system.apps["dpdk_sender"] = get_dpdk_sender_app( + # HOST=dpdk_sender.host_dpdk_sender[0], + # ) #-------------------------------------------------------------------------- # App generation completed @@ -761,7 +773,7 @@ def cli( write_config_file( output_dir, config_file.name if config_file else "default.json", - confgen.daqconf_multiru_gen( # + confgen.daqconf_multiru_gen( # :facepalm: boot = boot, dataflow = dataflow, dqm = dqm, @@ -770,7 +782,7 @@ def cli( readout = readout, timing = timing, trigger = trigger, - dpdk_sender = dpdk_sender, + # dpdk_sender = dpdk_sender, ) # ) From 9d1f1421ae7ebdb97db2616504b88f30c83701c2 Mon Sep 17 00:00:00 2001 From: Jonathan Hancock Date: Mon, 26 Jun 2023 16:35:53 +0200 Subject: [PATCH 37/90] Added titles to the widgets, and cleaned up the imports. --- scripts/daqconf_viewer | 36 ++++++++++++++++++++++++++---------- scripts/daqconf_viewer.css | 15 +++++++++------ 2 files changed, 35 insertions(+), 16 deletions(-) diff --git a/scripts/daqconf_viewer b/scripts/daqconf_viewer index 9b7d2254..e02d48c8 100755 --- a/scripts/daqconf_viewer +++ b/scripts/daqconf_viewer @@ -6,20 +6,24 @@ import json import sys from rich.text import Text +from rich.markdown import Markdown from difflib import context_diff, ndiff, unified_diff from textual import log, events from textual.app import App, ComposeResult from textual.containers import Horizontal, Content, Container, Vertical from textual.widget import Widget -from textual.widgets import Button, Header, Footer, Static, Input, Label, ListView, ListItem, Tree +from textual.widgets import Button, Header, Footer, Static, Label, ListView, ListItem, Tree from textual.reactive import reactive, Reactive -from textual.message import Message, MessageTarget from textual.screen import Screen auth = ("fooUsr", "barPass") oldconf = None +class TitleBox(Static): + def __init__(self, title, **kwargs): + super().__init__(Markdown(f'# {title}')) + class LabelItem(ListItem): def __init__(self, label: str) -> None: super().__init__() @@ -39,6 +43,7 @@ class Configs(Static): self.set_interval(0.1, self.update_configs) def compose(self) -> ComposeResult: + yield TitleBox('Configuration List') yield ListView(LabelItem("asfas")) async def update_configs(self) -> None: @@ -56,10 +61,12 @@ class Configs(Static): def on_list_view_selected(self, event: ListView.Selected): confname = event.item.label - versions = self.screen.query_one(Horizontal) - versions.new_conf(confname) + for v in self.screen.query(Vertical): + if isinstance(v, Versions): + v.new_conf(confname) + break -class Versions(Horizontal): +class Versions(Vertical): vlist = reactive([]) def __init__(self, hostname, **kwargs): @@ -67,6 +74,10 @@ class Versions(Horizontal): self.hostname = hostname self.current_conf = None + def compose(self) -> ComposeResult: + yield TitleBox('Available Versions') + yield Horizontal(id='buttonbox') + def on_mount(self) -> None: self.set_interval(0.1, self.update_versions) @@ -81,12 +92,13 @@ class Versions(Horizontal): self.vlist = r.json()['versions'] #This is a list of ints def watch_vlist(self, vlist:list[int]) -> None: - old_buttons = self.query(Button) + bb = self.query_one(Horizontal) + old_buttons = bb.query(Button) for b in old_buttons: b.remove() for v in vlist: b_id = 'v' + str(v) #An id can't be just a number for some reason - self.mount(Button(str(v), id=b_id, classes='vbuttons', variant='primary')) + bb.mount(Button(str(v), id=b_id, classes='vbuttons', variant='primary')) async def on_button_pressed (self, event: Button.Pressed) -> None: button_id = event.button.id @@ -106,6 +118,7 @@ class Display(Vertical): self.version = None def compose(self) -> ComposeResult: + yield TitleBox('Configuration Data') yield Tree("", id='conftree') async def get_json(self, conf, ver) -> None: @@ -173,7 +186,8 @@ class DiffDisplay(Vertical): self.version = None def compose(self) -> ComposeResult: - yield Static(id='diffbox') + yield TitleBox('Diff') + yield Vertical(Static(id='diffbox')) async def get_json(self, conf, ver) -> None: self.confname = conf @@ -208,8 +222,10 @@ class DiffDisplay(Vertical): t = Text(d) diff += t - box = self.query_one(Static) - box.update(diff) + for b in self.query(Static): + if b.id == 'diffbox': + b.update(diff) + break def on_button_pressed(self) -> None: self.remove() diff --git a/scripts/daqconf_viewer.css b/scripts/daqconf_viewer.css index 9889bcb9..3ad1c36f 100644 --- a/scripts/daqconf_viewer.css +++ b/scripts/daqconf_viewer.css @@ -2,11 +2,15 @@ Screen { layout: grid; layers: below above; - grid-size: 4 10; - grid-gutter: 1; + grid-size: 4 4; + grid-gutter: 0; height: 100%; } +#buttonbox { + overflow-x: auto; +} + #conftree { height:90%; } @@ -24,21 +28,20 @@ Screen { } .configs { - row-span: 10; + row-span: 4; column-span: 1; height: 100%; } .versions { - row-span: 2; + row-span: 1; column-span: 3; height: 100%; - overflow-x: auto; align-vertical: middle; } .display { - row-span: 8; + row-span: 4; column-span: 3; height: 100%; align-horizontal: center; From ae6219648f02f9911d865125fd7874aab91e6c96 Mon Sep 17 00:00:00 2001 From: Jonathan Hancock Date: Mon, 26 Jun 2023 19:02:04 +0200 Subject: [PATCH 38/90] Added error handling and the ability to choose host and port from the command line. --- scripts/daqconf_viewer | 153 ++++++++++++++++++++++++++++------------- 1 file changed, 106 insertions(+), 47 deletions(-) diff --git a/scripts/daqconf_viewer b/scripts/daqconf_viewer index e02d48c8..2349ecdd 100755 --- a/scripts/daqconf_viewer +++ b/scripts/daqconf_viewer @@ -1,6 +1,7 @@ #!/usr/bin/env python3 import asyncio import copy +import click import httpx import json import sys @@ -44,13 +45,20 @@ class Configs(Static): def compose(self) -> ComposeResult: yield TitleBox('Configuration List') - yield ListView(LabelItem("asfas")) + yield ListView(LabelItem("This shouldn't be visible!")) async def update_configs(self) -> None: - async with httpx.AsyncClient() as client: - r = await client.get(f'{self.hostname}/listConfigs', auth=auth, timeout=60) - unsorted = r.json()['configs'] - self.conflist = sorted(unsorted, key=str.lower) + try: + async with httpx.AsyncClient() as client: + r = await client.get(f'{self.hostname}/listConfigs', auth=auth, timeout=5) + unsorted = r.json()['configs'] + self.conflist = sorted(unsorted, key=str.lower) + except Exception as e: + #Exiting the program mid-request causes a CancelledError: we don't want to call our function + #in this case, as it will not be able to find the relevent widgets. + if isinstance(e, asyncio.CancelledError): + return + self.display_error(f"Couldn't retrieve configs from {self.hostname}/listConfigs") def watch_conflist(self, conflist:list[str]): label_list = [LabelItem(c) for c in conflist] @@ -66,6 +74,19 @@ class Configs(Static): v.new_conf(confname) break + def display_error(self, text): + '''If something goes wrong with getting the configs, we hijack the display to tell the user.''' + for v in self.screen.query(Vertical): + if isinstance(v, Display): + e_json = {'error': text} + v.confdata = e_json + break + if isinstance(v, DiffDisplay): + for s in v.query(Static): + if s.id == 'diffbox': + s.update(text) + break + class Versions(Vertical): vlist = reactive([]) @@ -86,10 +107,14 @@ class Versions(Vertical): async def update_versions(self) -> None: if self.current_conf: - async with httpx.AsyncClient() as client: - payload = {'name': self.current_conf} - r = await client.get(f'{self.hostname}/listVersions', auth=auth, params=payload, timeout=60) - self.vlist = r.json()['versions'] #This is a list of ints + try: + async with httpx.AsyncClient() as client: + payload = {'name': self.current_conf} + r = await client.get(f'{self.hostname}/listVersions', auth=auth, params=payload, timeout=5) + self.vlist = r.json()['versions'] #This is a list of ints + except Exception as e: + if isinstance(e, asyncio.CancelledError): + self.display_error(f"Couldn't retrieve versions from {self.hostname}/listVersions") def watch_vlist(self, vlist:list[int]) -> None: bb = self.query_one(Horizontal) @@ -108,6 +133,19 @@ class Versions(Vertical): await v.get_json(self.current_conf, version) break + def display_error(self, text): + '''If something goes wrong with getting the configs, we hijack the display to tell the user.''' + for v in self.screen.query(Vertical): + if isinstance(v, Display): + e_json = {'error': text} + v.confdata = e_json + break + if isinstance(v, DiffDisplay): + for s in v.query(Static): + if s.id == 'diffbox': + s.update(text) + break + class Display(Vertical): confdata = reactive(None) @@ -115,7 +153,7 @@ class Display(Vertical): super().__init__(**kwargs) self.hostname = hostname self.confname = None - self.version = None + self.version = None def compose(self) -> ComposeResult: yield TitleBox('Configuration Data') @@ -125,10 +163,13 @@ class Display(Vertical): self.confname = conf self.version = ver if self.confname != None and self.version != None: - async with httpx.AsyncClient() as client: - payload = {'name': self.confname, 'version': self.version} - r = await client.get(f'{self.hostname}/retrieveVersion', auth=auth, params=payload, timeout=60) - self.confdata = r.json() + try: + async with httpx.AsyncClient() as client: + payload = {'name': self.confname, 'version': self.version} + r = await client.get(f'{self.hostname}/retrieveVersion', auth=auth, params=payload, timeout=5) + self.confdata = r.json() + except: + self.confdata = {"error": f"Couldn't retrieve the configuration at {self.hostname}/retrieveVersion (payload: {payload}"} def json_into_tree(cls, node, json_data): """Takes a JSON, and puts it into the tree.""" @@ -193,39 +234,48 @@ class DiffDisplay(Vertical): self.confname = conf self.version = ver if self.confname != None and self.version != None: - async with httpx.AsyncClient() as client: - payload = {'name': self.confname, 'version': self.version} - r = await client.get(f'{self.hostname}/retrieveVersion', auth=auth, params=payload, timeout=60) - self.confdata = r.json() + try: + async with httpx.AsyncClient() as client: + payload = {'name': self.confname, 'version': self.version} + r = await client.get(f'{self.hostname}/retrieveVersion', auth=auth, params=payload, timeout=5) + self.confdata = r.json() + except: + self.confdata = {"error": f"Couldn't retrieve the configuration at {self.hostname}/retrieveVersion (payload: {payload})"} async def watch_confdata(self, confdata:dict) -> None: '''Turns the jsons into a string format with newlines, then generates a diff of the two.''' if confdata: - j1 = copy.deepcopy(oldconf) - j2 = copy.deepcopy(confdata) - if "_id" in j1: del j1["_id"] #We don't want to include the ID in the diff since it's always different. - if "_id" in j2: del j2["_id"] - a = json.dumps(j1, sort_keys=True, indent=4).splitlines(keepends=True) - b = json.dumps(j2, sort_keys=True, indent=4).splitlines(keepends=True) - delta = unified_diff(a,b) - diff = Text() - for d in delta: - sym = d[0] - match sym: - case '+': - t = Text(d, style='green') - case '-': - t = Text(d, style='red') - case '@': - t = Text(d, style='gold1') - case _: - t = Text(d) - diff += t - - for b in self.query(Static): - if b.id == 'diffbox': - b.update(diff) - break + if "error" in confdata: + for s in self.query(Static): + if s.id == 'diffbox': + s.update(confdata['error']) + break + else: + j1 = copy.deepcopy(oldconf) + j2 = copy.deepcopy(confdata) + if "_id" in j1: del j1["_id"] #We don't want to include the ID in the diff since it's always different. + if "_id" in j2: del j2["_id"] + a = json.dumps(j1, sort_keys=True, indent=4).splitlines(keepends=True) + b = json.dumps(j2, sort_keys=True, indent=4).splitlines(keepends=True) + delta = unified_diff(a,b) + diff = Text() + for d in delta: + sym = d[0] + match sym: + case '+': + t = Text(d, style='green') + case '-': + t = Text(d, style='red') + case '@': + t = Text(d, style='gold1') + case _: + t = Text(d) + diff += t + + for s in self.query(Static): + if s.id == 'diffbox': + s.update(diff) + break def on_button_pressed(self) -> None: self.remove() @@ -249,9 +299,9 @@ class ConfViewer(App): CSS_PATH = "daqconf_viewer.css" BINDINGS = [("d", "make_diff()", "Diff")] - def __init__(self, **kwargs): + def __init__(self, host, port, **kwargs): super().__init__(**kwargs) - self.hostname = "http://np04-srv-023:31011" + self.hostname = f"{host}:{port}" def on_mount(self) -> None: self.install_screen(DiffScreen(hostname=self.hostname), name="diff") @@ -272,6 +322,15 @@ class ConfViewer(App): oldconf = dis.confdata self.push_screen('diff') -if __name__ == "__main__": - app = ConfViewer() +CONTEXT_SETTINGS = dict(help_option_names=['-h', '--help']) +@click.command(context_settings=CONTEXT_SETTINGS) +@click.option('--host', default="http://np04-srv-023", help='Machine hosting the config service') +@click.option('--port', default="31011", help='Port that the config service listens on') +def start(host:str, port:str): + app = ConfViewer(host, port) app.run() + +if __name__ == "__main__": + start() +"http://np04-srv-023:31011" + From 67a82173ea14745c08f3c5608e08bc41a8384987 Mon Sep 17 00:00:00 2001 From: Jonathan Hancock Date: Mon, 26 Jun 2023 19:11:16 +0200 Subject: [PATCH 39/90] Removed a string that shouldn't have been in the code. --- scripts/daqconf_viewer | 2 -- 1 file changed, 2 deletions(-) diff --git a/scripts/daqconf_viewer b/scripts/daqconf_viewer index 2349ecdd..8e0a2f1a 100755 --- a/scripts/daqconf_viewer +++ b/scripts/daqconf_viewer @@ -332,5 +332,3 @@ def start(host:str, port:str): if __name__ == "__main__": start() -"http://np04-srv-023:31011" - From 9193d83251a696805ba4717a47487d8ca7207cc6 Mon Sep 17 00:00:00 2001 From: Alessandro Thea Date: Tue, 27 Jun 2023 08:47:59 +0200 Subject: [PATCH 40/90] rationalising basic types definition in daqconf moo schemas --- schema/daqconf/bootgen.jsonnet | 38 +-- schema/daqconf/confgen.jsonnet | 282 +----------------- schema/daqconf/daqcommongen.jsonnet | 36 +-- schema/daqconf/dataflowgen.jsonnet | 55 +--- schema/daqconf/detectorgen.jsonnet | 35 +-- schema/daqconf/dqmgen.jsonnet | 46 +-- schema/daqconf/hsigen.jsonnet | 69 ++--- schema/daqconf/readoutgen.jsonnet | 89 +++--- schema/daqconf/timinggen.jsonnet | 46 +-- schema/daqconf/triggergen.jsonnet | 89 +++--- schema/daqconf/types.jsonnet | 27 ++ ...nfigs.py => daqconf_check_np04_configs.py} | 0 test/scripts/daqconf_check_schema.py | 17 ++ 13 files changed, 207 insertions(+), 622 deletions(-) create mode 100644 schema/daqconf/types.jsonnet rename test/scripts/{check_np04_configs.py => daqconf_check_np04_configs.py} (100%) create mode 100755 test/scripts/daqconf_check_schema.py diff --git a/schema/daqconf/bootgen.jsonnet b/schema/daqconf/bootgen.jsonnet index 69a6b397..ec548826 100644 --- a/schema/daqconf/bootgen.jsonnet +++ b/schema/daqconf/bootgen.jsonnet @@ -3,22 +3,14 @@ local moo = import "moo.jsonnet"; +local stypes = import "daqconf/types.jsonnet"; +local types = moo.oschema.hier(stypes).dunedaq.daqconf.types; + local s = moo.oschema.schema("dunedaq.daqconf.bootgen"); local nc = moo.oschema.numeric_constraints; local cs = { - port: s.number( "Port", "i4", doc="A TCP/IP port number"), - count: s.number( "count", "i8", doc="A count of things"), - flag: s.boolean( "Flag", doc="Parameter that can be used to enable or disable functionality"), monitoring_dest: s.enum( "MonitoringDest", ["local", "cern", "pocket"]), - path: s.string( "Path", doc="Location on a filesystem"), - paths: s.sequence( "Paths", self.path, doc="Multiple paths"), - host: s.string( "Host", moo.re.dnshost, doc="A hostname"), - hosts: s.sequence( "Hosts", self.host, "Multiple hosts"), - string: s.string( "Str", doc="Generic string"), - strings: s.sequence( "Strings", self.string, doc="List of strings"), - tpg_channel_map: s.enum( "TPGChannelMap", ["VDColdboxChannelMap", "ProtoDUNESP1ChannelMap", "PD2HDChannelMap", "HDColdboxChannelMap"]), - readout_time: s.number( "ROTime", "i8", doc="A readout time in ticks"), pm_choice: s.enum( "PMChoice", ["k8s", "ssh"], doc="Process Manager choice: ssh or Kubernetes"), rte_choice: s.enum( "RTEChoice", ["auto", "release", "devarea"], doc="Kubernetes DAQ application RTE choice"), @@ -26,29 +18,29 @@ local cs = { boot: s.record("boot", [ // s.field( "op_env", self.string, default='swtest', doc="Operational environment - used for raw data filename prefix and HDF5 Attribute inside the files"), - s.field( "base_command_port", self.port, default=3333, doc="Base port of application command endpoints"), + s.field( "base_command_port", types.port, default=3333, doc="Base port of application command endpoints"), # Obscure - s.field( "capture_env_vars", self.strings, default=['TIMING_SHARE', 'DETCHANNELMAPS_SHARE'], doc="List of variables to capture from the environment"), - s.field( "disable_trace", self.flag, false, doc="Do not enable TRACE (default TRACE_FILE is /tmp/trace_buffer_${HOSTNAME}_${USER})"), + s.field( "capture_env_vars", types.strings, default=['TIMING_SHARE', 'DETCHANNELMAPS_SHARE'], doc="List of variables to capture from the environment"), + s.field( "disable_trace", types.flag, false, doc="Do not enable TRACE (default TRACE_FILE is /tmp/trace_buffer_${HOSTNAME}_${USER})"), s.field( "opmon_impl", self.monitoring_dest, default='local', doc="Info collector service implementation to use"), s.field( "ers_impl", self.monitoring_dest, default='local', doc="ERS destination (Kafka used for cern and pocket)"), - s.field( "pocket_url", self.host, default='127.0.0.1', doc="URL for connecting to Pocket services"), + s.field( "pocket_url", types.host, default='127.0.0.1', doc="URL for connecting to Pocket services"), s.field( "process_manager", self.pm_choice, default="ssh", doc="Choice of process manager"), # K8S - s.field( "k8s_image", self.string, default="dunedaq/c8-minimal", doc="Which docker image to use"), + s.field( "k8s_image", types.string, default="dunedaq/c8-minimal", doc="Which docker image to use"), s.field( "k8s_rte", self.rte_choice, default="auto", doc="0 - Use an RTE script if not in a dev environment, 1 - Always use RTE, 2 - never use RTE"), # Connectivity Service - s.field( "use_connectivity_service", self.flag, default=true, doc="Whether to use the ConnectivityService to manage connections"), - s.field( "start_connectivity_service", self.flag, default=true, doc="Whether to use the ConnectivityService to manage connections"), - s.field( "connectivity_service_threads", self.count, default=2, doc="Number of threads for the gunicorn server that serves connection info"), - s.field( "connectivity_service_host", self.host, default='localhost', doc="Hostname for the ConnectivityService"), - s.field( "connectivity_service_port", self.port, default=15000, doc="Port for the ConnectivityService"), - s.field( "connectivity_service_interval", self.count, default=1000, doc="Publish interval for the ConnectivityService") + s.field( "use_connectivity_service", types.flag, default=true, doc="Whether to use the ConnectivityService to manage connections"), + s.field( "start_connectivity_service", types.flag, default=true, doc="Whether to use the ConnectivityService to manage connections"), + s.field( "connectivity_service_threads", types.count, default=2, doc="Number of threads for the gunicorn server that serves connection info"), + s.field( "connectivity_service_host", types.host, default='localhost', doc="Hostname for the ConnectivityService"), + s.field( "connectivity_service_port", types.port, default=15000, doc="Port for the ConnectivityService"), + s.field( "connectivity_service_interval", types.count, default=1000, doc="Publish interval for the ConnectivityService") ]), }; -moo.oschema.sort_select(cs) +stypes + moo.oschema.sort_select(cs) diff --git a/schema/daqconf/confgen.jsonnet b/schema/daqconf/confgen.jsonnet index f9af8200..6e572372 100755 --- a/schema/daqconf/confgen.jsonnet +++ b/schema/daqconf/confgen.jsonnet @@ -51,6 +51,7 @@ local cs = { hosts: s.sequence( "Hosts", self.host, "Multiple hosts"), string: s.string( "Str", doc="Generic string"), strings: s.sequence( "Strings", self.string, doc="List of strings"), + tpg_channel_map: s.enum( "TPGChannelMap", ["VDColdboxChannelMap", "ProtoDUNESP1ChannelMap", "PD2HDChannelMap", "HDColdboxChannelMap"]), dqm_channel_map: s.enum( "DQMChannelMap", ['HD', 'VD', 'PD2HD', 'HDCB']), dqm_params: s.sequence( "DQMParams", self.count, doc="Parameters for DQM (fixme)"), @@ -65,81 +66,6 @@ local cs = { rte_choice: s.enum( "RTEChoice", ["auto", "release", "devarea"], doc="Kubernetes DAQ application RTE choice"), - - // boot: s.record("boot", [ - // // s.field( "op_env", self.string, default='swtest', doc="Operational environment - used for raw data filename prefix and HDF5 Attribute inside the files"), - // s.field( "base_command_port", self.port, default=3333, doc="Base port of application command endpoints"), - - // # Obscure - // s.field( "capture_env_vars", self.strings, default=['TIMING_SHARE', 'DETCHANNELMAPS_SHARE'], doc="List of variables to capture from the environment"), - // s.field( "disable_trace", self.flag, false, doc="Do not enable TRACE (default TRACE_FILE is /tmp/trace_buffer_${HOSTNAME}_${USER})"), - // s.field( "opmon_impl", self.monitoring_dest, default='local', doc="Info collector service implementation to use"), - // s.field( "ers_impl", self.monitoring_dest, default='local', doc="ERS destination (Kafka used for cern and pocket)"), - // s.field( "pocket_url", self.host, default='127.0.0.1', doc="URL for connecting to Pocket services"), - // s.field( "process_manager", self.pm_choice, default="ssh", doc="Choice of process manager"), - - // # K8S - // s.field( "k8s_image", self.string, default="dunedaq/c8-minimal", doc="Which docker image to use"), - // s.field( "k8s_rte", self.rte_choice, default="auto", doc="0 - Use an RTE script if not in a dev environment, 1 - Always use RTE, 2 - never use RTE"), - - // # Connectivity Service - // s.field( "use_connectivity_service", self.flag, default=true, doc="Whether to use the ConnectivityService to manage connections"), - // s.field( "start_connectivity_service", self.flag, default=true, doc="Whether to use the ConnectivityService to manage connections"), - // s.field( "connectivity_service_threads", self.count, default=2, doc="Number of threads for the gunicorn server that serves connection info"), - // s.field( "connectivity_service_host", self.host, default='localhost', doc="Hostname for the ConnectivityService"), - // s.field( "connectivity_service_port", self.port, default=15000, doc="Port for the ConnectivityService"), - // s.field( "connectivity_service_interval", self.count, default=1000, doc="Publish interval for the ConnectivityService") - // ]), - - - // daq_common : s.record("daq_common", [ - // s.field( "data_request_timeout_ms", self.count, default=1000, doc="The baseline data request timeout that will be used by modules in the Readout and Trigger subsystems (i.e. any module that produces data fragments). Downstream timeouts, such as the trigger-record-building timeout, are derived from this."), - // s.field( "use_data_network", self.flag, default = false, doc="Whether to use the data network (Won't work with k8s)"), - // s.field( "data_rate_slowdown_factor",self.count, default=1, doc="Factor by which to suppress data generation. Former -s"), - // ], doc="Cmmon daq_common settings"), - - // detector : s.record("detector", [ - // s.field( "op_env", self.string, default='swtest', doc="Operational environment - used for HDF5 Attribute inside the files"), - // s.field( "clock_speed_hz", self.freq, default=62500000), - // s.field( "tpg_channel_map", self.tpg_channel_map, default="PD2HDChannelMap", doc="Channel map for TPG"), - // ], doc="Global common settings"), - - // timing: s.record("timing", [ - // s.field( "timing_session_name", self.string, default="", doc="Name of the global timing session to use, for timing commands"), - // s.field( "host_tprtc", self.host, default='localhost', doc='Host to run the timing partition controller app on'), - // # timing hw partition options - // s.field( "control_timing_partition", self.flag, default=false, doc='Flag to control whether we are controlling timing partition in master hardware'), - // s.field( "timing_partition_master_device_name", self.string, default="", doc='Timing partition master hardware device name'), - // s.field( "timing_partition_id", self.count, default=0, doc='Timing partition id'), - // s.field( "timing_partition_trigger_mask", self.count, default=255, doc='Timing partition trigger mask'), - // s.field( "timing_partition_rate_control_enabled", self.flag, default=false, doc='Timing partition rate control enabled'), - // s.field( "timing_partition_spill_gate_enabled", self.flag, default=false, doc='Timing partition spill gate enabled'), - // ]), - - // hsi: s.record("hsi", [ - // # timing hsi options - // s.field( "use_timing_hsi", self.flag, default=false, doc='Flag to control whether real hardware timing HSI config is generated. Default is false'), - // s.field( "host_timing_hsi", self.host, default='localhost', doc='Host to run the HSI app on'), - // s.field( "hsi_hw_connections_file", self.path, default="${TIMING_SHARE}/config/etc/connections.xml", doc='Real timing hardware only: path to hardware connections file'), - // s.field( "enable_hardware_state_recovery", self.flag, default=true, doc="Enable (or not) hardware state recovery"), - // s.field( "hsi_device_name", self.string, default="", doc='Real HSI hardware only: device name of HSI hw'), - // s.field( "hsi_readout_period", self.count, default=1e3, doc='Real HSI hardware only: Period between HSI hardware polling [us]'), - // s.field( "control_hsi_hw", self.flag, default=false, doc='Flag to control whether we are controlling hsi hardware'), - // s.field( "hsi_endpoint_address", self.count, default=1, doc='Timing address of HSI endpoint'), - // s.field( "hsi_endpoint_partition", self.count, default=0, doc='Timing partition of HSI endpoint'), - // s.field( "hsi_re_mask",self.count, default=0, doc='Rising-edge trigger mask'), - // s.field( "hsi_fe_mask", self.count, default=0, doc='Falling-edge trigger mask'), - // s.field( "hsi_inv_mask",self.count, default=0, doc='Invert-edge mask'), - // s.field( "hsi_source",self.count, default=1, doc='HSI signal source; 0 - hardware, 1 - emulation (trigger timestamp bits)'), - // # fake hsi options - // s.field( "use_fake_hsi", self.flag, default=true, doc='Flag to control whether fake or real hardware HSI config is generated. Default is true'), - // s.field( "host_fake_hsi", self.host, default='localhost', doc='Host to run the HSI app on'), - // s.field( "hsi_device_id", self.count, default=0, doc='Fake HSI only: device ID of fake HSIEvents'), - // s.field( "mean_hsi_signal_multiplicity", self.count, default=1, doc='Fake HSI only: rate of individual HSI signals in emulation mode 1'), - // s.field( "hsi_signal_emulation_mode", self.count, default=0, doc='Fake HSI only: HSI signal emulation mode'), - // s.field( "enabled_hsi_signals", self.count, default=1, doc='Fake HSI only: bit mask of enabled fake HSI signals') - // ]), - ctb_hsi: s.record("ctb_hsi", [ # ctb options s.field( "use_ctb_hsi", self.flag, default=false, doc='Flag to control whether CTB HSI config is generated. Default is false'), @@ -152,212 +78,6 @@ local cs = { s.field( "fake_trig_2", ctbmodule.Randomtrigger, ctbmodule.Randomtrigger) ]), - // data_file_entry: s.record("data_file_entry", [ - // s.field( "data_file", self.path, default='./frames.bin', doc="File containing data frames to be replayed by the fake cards. Former -d. Uses the asset manager, can also be 'asset://checksum/somelonghash', or 'file://somewhere/frames.bin' or 'frames.bin'"), - // s.field( "detector_id", self.count, default=3, doc="Detector ID that this file applies to"), - // ]), - // data_files: s.sequence("data_files", self.data_file_entry), - - // numa_exception: s.record( "NUMAException", [ - // s.field( "host", self.host, default='localhost', doc="Host of exception"), - // s.field( "card", self.count, default=0, doc="Card ID of exception"), - // s.field( "numa_id", self.count, default=0, doc="NUMA ID of exception"), - // s.field( "felix_card_id", self.count, default=-1, doc="CARD ID override, -1 indicates no override"), - // s.field( "latency_buffer_numa_aware", self.flag, default=false, doc="Enable NUMA-aware mode for the Latency Buffer"), - // s.field( "latency_buffer_preallocation", self.flag, default=false, doc="Enable Latency Buffer preallocation"), - // ], doc="Exception to the default NUMA ID for FELIX cards"), - - // numa_exceptions: s.sequence( "NUMAExceptions", self.numa_exception, doc="Exceptions to the default NUMA ID"), - - // numa_config: s.record("numa_config", [ - // s.field( "default_id", self.count, default=0, doc="Default NUMA ID for FELIX cards"), - // s.field( "default_latency_numa_aware", self.flag, default=false, doc="Default for Latency Buffer NUMA awareness"), - // s.field( "default_latency_preallocation", self.flag, default=false, doc="Default for Latency Buffer Preallocation"), - // s.field( "exceptions", self.numa_exceptions, default=[], doc="Exceptions to the default NUMA ID"), - // ]), - - // readout: s.record("readout", [ - // s.field( "detector_readout_map_file", self.path, default='./DetectorReadoutMap.json', doc="File containing detector hardware map for configuration to run"), - // s.field( "use_fake_data_producers", self.flag, default=false, doc="Use fake data producers that respond with empty fragments immediately instead of (fake) cards and DLHs"), - // // s.field( "memory_limit_gb", self.count, default=64, doc="Application memory limit in GB") - // // Fake cards - // s.field( "use_fake_cards", self.flag, default=false, doc="Use fake cards"), - // s.field( "emulated_data_times_start_with_now", self.flag, default=false, doc="If active, the timestamp of the first emulated data frame is set to the current wallclock time"), - // s.field( "default_data_file", self.path, default='asset://?label=ProtoWIB&subsystem=readout', doc="File containing data frames to be replayed by the fake cards. Former -d. Uses the asset manager, can also be 'asset://?checksum=somelonghash', or 'file://somewhere/frames.bin' or 'frames.bin'"), - // s.field( "data_files", self.data_files, default=[], doc="Files to use by detector type"), - // // DPDK - // s.field( "dpdk_eal_args", self.string, default='-l 0-1 -n 3 -- -m [0:1].0 -j', doc='Args passed to the EAL in DPDK'), - // s.field( "dpdk_rxqueues_per_lcore", self.count, default=1, doc='Number of rx queues per core'), - // // FLX - // s.field( "numa_config", self.numa_config, default=self.numa_config, doc='Configuration of FELIX NUMA IDs'), - // // DLH - // s.field( "emulator_mode", self.flag, default=false, doc="If active, timestamps of data frames are overwritten when processed by the readout. This is necessary if the felix card does not set correct timestamps. Former -e"), - // s.field( "thread_pinning_file", self.path, default="", doc="A thread pinning configuration file that gets executed after conf."), - // // s.field( "data_rate_slowdown_factor",self.count, default=1, doc="Factor by which to suppress data generation. Former -s"), - // s.field( "latency_buffer_size", self.count, default=499968, doc="Size of the latency buffers (in number of elements)"), - // s.field( "fragment_send_timeout_ms", self.count, default=10, doc="The send timeout that will be used in the readout modules when sending fragments downstream (i.e. to the TRB)."), - // s.field( "enable_tpg", self.flag, default=false, doc="Enable TPG"), - // s.field( "tpg_threshold", self.count, default=120, doc="Select TPG threshold"), - // s.field( "tpg_algorithm", self.string, default="SimpleThreshold", doc="Select TPG algorithm (SimpleThreshold, AbsRS)"), - // s.field( "tpg_channel_mask", self.channel_list, default=[], doc="List of offline channels to be masked out from the TPHandler"), - // s.field( "enable_raw_recording", self.flag, default=false, doc="Add queues and modules necessary for the record command"), - // s.field( "raw_recording_output_dir", self.path, default='.', doc="Output directory where recorded data is written to. Data for each link is written to a separate file") - // ]), - - // trigger_algo_config: s.record("trigger_algo_config", [ - // s.field("prescale", self.count, default=100), - // s.field("window_length", self.count, default=10000), - // s.field("adjacency_threshold", self.count, default=6), - // s.field("adj_tolerance", self.count, default=4), - // s.field("trigger_on_adc", self.flag, default=false), - // s.field("trigger_on_n_channels", self.flag, default=false), - // s.field("trigger_on_adjacency", self.flag, default=true), - // s.field("adc_threshold", self.count, default=10000), - // s.field("n_channels_threshold", self.count, default=8), - // s.field("print_tp_info", self.flag, default=false), - // ]), - - // c0_readout: s.record("c0_readout", [ - // s.field("candidate_type", self.tc_type, default=0, doc="The TC type, 0=Unknown"), - // s.field("time_before", self.readout_time, default=1000, doc="Time to readout before TC time [ticks]"), - // s.field("time_after", self.readout_time, default=1001, doc="Time to readout after TC time [ticks]"), - // ]), - // c1_readout: s.record("c1_readout", [ - // s.field("candidate_type", self.tc_type, default=1, doc="The TC type, 1=Timing"), - // s.field("time_before", self.readout_time, default=1000, doc="Time to readout before TC time [ticks]"), - // s.field("time_after", self.readout_time, default=1001, doc="Time to readout after TC time [ticks]"), - // ]), - // c2_readout: s.record("c2_readout", [ - // s.field("candidate_type", self.tc_type, default=2, doc="The TC type, 2=TPCLowE"), - // s.field("time_before", self.readout_time, default=1000, doc="Time to readout before TC time [ticks]"), - // s.field("time_after", self.readout_time, default=1001, doc="Time to readout after TC time [ticks]"), - // ]), - // c3_readout: s.record("c3_readout", [ - // s.field("candidate_type", self.tc_type, default=3, doc="The TC type, 3=Supernova"), - // s.field("time_before", self.readout_time, default=1000, doc="Time to readout before TC time [ticks]"), - // s.field("time_after", self.readout_time, default=1001, doc="Time to readout after TC time [ticks]"), - // ]), - // c4_readout: s.record("c4_readout", [ - // s.field("candidate_type", self.tc_type, default=4, doc="The TC type, 4=Random"), - // s.field("time_before", self.readout_time, default=1000, doc="Time to readout before TC time [ticks]"), - // s.field("time_after", self.readout_time, default=1001, doc="Time to readout after TC time [ticks]"), - // ]), - // c5_readout: s.record("c5_readout", [ - // s.field("candidate_type", self.tc_type, default=5, doc="The TC type, 5=Prescale"), - // s.field("time_before", self.readout_time, default=1000, doc="Time to readout before TC time [ticks]"), - // s.field("time_after", self.readout_time, default=1001, doc="Time to readout after TC time [ticks]"), - // ]), - // c6_readout: s.record("c6_readout", [ - // s.field("candidate_type", self.tc_type, default=6, doc="The TC type, 6=ADCSimpleWindow"), - // s.field("time_before", self.readout_time, default=1000, doc="Time to readout before TC time [ticks]"), - // s.field("time_after", self.readout_time, default=1001, doc="Time to readout after TC time [ticks]"), - // ]), - // c7_readout: s.record("c7_readout", [ - // s.field("candidate_type", self.tc_type, default=7, doc="The TC type, 7=HorizontalMuon"), - // s.field("time_before", self.readout_time, default=1000, doc="Time to readout before TC time [ticks]"), - // s.field("time_after", self.readout_time, default=1001, doc="Time to readout after TC time [ticks]"), - // ]), - // c8_readout: s.record("c8_readout", [ - // s.field("candidate_type", self.tc_type, default=8, doc="The TC type, 8=MichelElectron"), - // s.field("time_before", self.readout_time, default=1000, doc="Time to readout before TC time [ticks]"), - // s.field("time_after", self.readout_time, default=1001, doc="Time to readout after TC time [ticks]"), - // ]), - // c9_readout: s.record("c9_readout", [ - // s.field("candidate_type", self.tc_type, default=9, doc="The TC type, 9=LowEnergyEvent"), - // s.field("time_before", self.readout_time, default=1000, doc="Time to readout before TC time [ticks]"), - // s.field("time_after", self.readout_time, default=1001, doc="Time to readout after TC time [ticks]"), - // ]), - - // tc_readout_map: s.record("tc_readout_map", [ - // s.field("c0", self.c0_readout, default=self.c0_readout, doc="TC readout for TC type 0"), - // s.field("c1", self.c1_readout, default=self.c1_readout, doc="TC readout for TC type 1"), - // s.field("c2", self.c2_readout, default=self.c2_readout, doc="TC readout for TC type 2"), - // s.field("c3", self.c3_readout, default=self.c3_readout, doc="TC readout for TC type 3"), - // s.field("c4", self.c4_readout, default=self.c4_readout, doc="TC readout for TC type 4"), - // s.field("c5", self.c5_readout, default=self.c5_readout, doc="TC readout for TC type 5"), - // s.field("c6", self.c6_readout, default=self.c6_readout, doc="TC readout for TC type 6"), - // s.field("c7", self.c7_readout, default=self.c7_readout, doc="TC readout for TC type 7"), - // s.field("c8", self.c8_readout, default=self.c8_readout, doc="TC readout for TC type 8"), - // s.field("c9", self.c9_readout, default=self.c9_readout, doc="TC readout for TC type 9"), - // ]), - - // trigger: s.record("trigger",[ - // s.field( "trigger_rate_hz", self.rate, default=1.0, doc='Fake HSI only: rate at which fake HSIEvents are sent. 0 - disable HSIEvent generation. Former -t'), - // s.field( "trigger_window_before_ticks",self.count, default=1000, doc="Trigger window before marker. Former -b"), - // s.field( "trigger_window_after_ticks", self.count, default=1000, doc="Trigger window after marker. Former -a"), - // s.field( "host_trigger", self.host, default='localhost', doc='Host to run the trigger app on'), - // // s.field( "host_tpw", self.host, default='localhost', doc='Host to run the TPWriter app on'), - // # trigger options - // s.field( "completeness_tolerance", self.count, default=1, doc="Maximum number of inactive queues we will tolerate."), - // s.field( "tolerate_incompleteness", self.flag, default=false, doc="Flag to tell trigger to tolerate inactive queues."), - // s.field( "ttcm_s1", self.count,default=1, doc="Timing trigger candidate maker accepted HSI signal ID 1"), - // s.field( "ttcm_s2", self.count, default=2, doc="Timing trigger candidate maker accepted HSI signal ID 2"), - // s.field( "trigger_activity_plugin", self.string, default='TriggerActivityMakerPrescalePlugin', doc="Trigger activity algorithm plugin"), - // s.field( "trigger_activity_config", self.trigger_algo_config, default=self.trigger_algo_config,doc="Trigger activity algorithm config (string containing python dictionary)"), - // s.field( "trigger_candidate_plugin", self.string, default='TriggerCandidateMakerPrescalePlugin', doc="Trigger candidate algorithm plugin"), - // s.field( "trigger_candidate_config", self.trigger_algo_config, default=self.trigger_algo_config, doc="Trigger candidate algorithm config (string containing python dictionary)"), - // s.field( "hsi_trigger_type_passthrough", self.flag, default=false, doc="Option to override trigger type in the MLT"), - // // s.field( "enable_tpset_writing", self.flag, default=false, doc="Enable the writing of TPs to disk (only works with enable_tpg or enable_firmware_tpg)"), - // // s.field( "tpset_output_path", self.path,default='.', doc="Output directory for TPSet stream files"), - // // s.field( "tpset_output_file_size",self.count, default=4*1024*1024*1024, doc="The size threshold when TPSet stream files are closed (in bytes)"), - // // s.field( "tpg_channel_map", self.tpg_channel_map, default="ProtoDUNESP1ChannelMap", doc="Channel map for TPG"), - // s.field( "mlt_merge_overlapping_tcs", self.flag, default=true, doc="Option to turn off merging of overlapping TCs when forming TDs in MLT"), - // s.field( "mlt_buffer_timeout", self.count, default=100, doc="Timeout (buffer) to wait for new overlapping TCs before sending TD"), - // s.field( "mlt_send_timed_out_tds", self.flag, default=true, doc="Option to drop TD if TC comes out of timeout window"), - // s.field( "mlt_max_td_length_ms", self.count, default=1000, doc="Maximum allowed time length [ms] for a readout window of a single TD"), - // s.field( "mlt_ignore_tc", self.tc_types, default=[], doc="Optional list of TC types to be ignored in MLT"), - // s.field( "mlt_use_readout_map", self.flag, default=false, doc="Option to use custom readout map in MLT"), - // s.field( "mlt_td_readout_map", self.tc_readout_map, default=self.tc_readout_map, doc="The readout windows assigned to TDs in MLT, based on TC type."), - // s.field( "use_custom_maker", self.flag, default=false, doc="Option to use a Custom Trigger Candidate Maker (plugin)"), - // s.field( "ctcm_trigger_types", self.tc_types, default=[4], doc="Optional list of TC types to be used by the Custom Trigger Candidate Maker (plugin)"), - // s.field( "ctcm_trigger_intervals", self.tc_intervals, default=[10000000], doc="Optional list of intervals (clock ticks) for the TC types to be used by the Custom Trigger Candidate Maker (plugin)"), - // ]), - - // dataflowapp: s.record("dataflowapp",[ - // s.field( "app_name", self.string, default="dataflow0"), - // s.field( "output_paths",self.paths, default=['.'], doc="Location(s) for the dataflow app to write data. Former -o"), - // s.field( "host_df", self.host, default='localhost'), - // s.field( "max_file_size",self.count, default=4*1024*1024*1024, doc="The size threshold when raw data files are closed (in bytes)"), - // s.field( "data_store_mode", self.string, default="all-per-file", doc="all-per-file or one-event-per-file"), - // s.field( "max_trigger_record_window",self.count, default=0, doc="The maximum size for the window of data that will included in a single TriggerRecord (in ticks). Readout windows that are longer than this size will result in TriggerRecords being split into a sequence of TRs. A zero value for this parameter means no splitting."), - - // ], doc="Element of the dataflow.apps array"), - - // dataflowapps: s.sequence("dataflowapps", self.dataflowapp, doc="List of dataflowapp instances"), - - // dataflow: s.record("dataflow", [ - // s.field( "host_dfo", self.host, default='localhost', doc="Sets the host for the DFO app"), - // s.field( "apps", self.dataflowapps, default=[], doc="Configuration for the dataflow apps (see dataflowapp for options)"), - // s.field( "token_count",self.count, default=10, doc="Number of tokens the dataflow apps give to the DFO. Former -c"), - // // Trigger - // s.field( "host_tpw", self.host, default='localhost', doc='Host to run the TPWriter app on'), - // s.field( "enable_tpset_writing", self.flag, default=false, doc="Enable the writing of TPs to disk (only works with enable_tpg or enable_firmware_tpg)"), - // s.field( "tpset_output_path", self.path,default='.', doc="Output directory for TPSet stream files"), - // s.field( "tpset_output_file_size",self.count, default=4*1024*1024*1024, doc="The size threshold when TPSet stream files are closed (in bytes)"), - // ]), - - // dqm: s.record("dqm", [ - // s.field('enable_dqm', self.flag, default=false, doc="Enable Data Quality Monitoring"), - // s.field('impl', self.monitoring_dest, default='local', doc="DQM destination (Kafka used for cern and pocket)"), - // s.field('cmap', self.dqm_channel_map, default='HD', doc="Which channel map to use for DQM"), - // s.field('host_dqm', self.hosts, default=['localhost'], doc='Host(s) to run the DQM app on'), - // s.field('raw_params', self.dqm_params, default=[60, 50], doc="Parameters that control the data sent for the raw display plot"), - // s.field('std_params', self.dqm_params, default=[10, 1000], doc="Parameters that control the data sent for the mean/rms plot"), - // s.field('rms_params', self.dqm_params, default=[0, 1000], doc="Parameters that control the data sent for the mean/rms plot"), - // s.field('fourier_channel_params', self.dqm_params, default=[0, 0], doc="Parameters that control the data sent for the fourier transform plot"), - // s.field('fourier_plane_params', self.dqm_params, default=[600, 1000], doc="Parameters that control the data sent for the summed fourier transform plot"), - // s.field('df_rate', self.count, default=10, doc='How many seconds between requests to DF for Trigger Records'), - // s.field('df_algs', self.string, default='raw std fourier_plane', doc='Algorithms to be run on Trigger Records from DF (use quotes)'), - // s.field('max_num_frames', self.count, default=32768, doc='Maximum number of frames to use in the algorithms'), - // s.field('kafka_address', self.string, default='', doc='kafka address used to send messages'), - // s.field('kafka_topic', self.string, default='DQM', doc='kafka topic used to send messages'), - // ]), - - // dpdk_sender: s.record("dpdk_sender", [ - // s.field( "enable_dpdk_sender", self.flag, default=false, doc="Enable sending frames using DPDK"), - // s.field( "host_dpdk_sender", self.hosts, default=['np04-srv-021'], doc="Which host to use to send frames"), - // s.field( "eal_args", self.string, default='-l 0-1 -n 3 -- -m [0:1].0 -j', doc='Args passed to the EAL in DPDK'), - // ]), daqconf_multiru_gen: s.record('daqconf_multiru_gen', [ s.field('detector', detectorgen.detector, default=detectorgen.detector, doc='Boot parameters'), diff --git a/schema/daqconf/daqcommongen.jsonnet b/schema/daqconf/daqcommongen.jsonnet index d67e6e5a..8eec3200 100644 --- a/schema/daqconf/daqcommongen.jsonnet +++ b/schema/daqconf/daqcommongen.jsonnet @@ -3,42 +3,20 @@ local moo = import "moo.jsonnet"; +local stypes = import "daqconf/types.jsonnet"; +local types = moo.oschema.hier(stypes).dunedaq.daqconf.types; + local s = moo.oschema.schema("dunedaq.daqconf.daqcommongen"); local nc = moo.oschema.numeric_constraints; // A temporary schema construction context. local cs = { - port: s.number( "Port", "i4", doc="A TCP/IP port number"), - freq: s.number( "Frequency", "u4", doc="A frequency"), - rate: s.number( "Rate", "f8", doc="A rate as a double"), - count: s.number( "count", "i8", doc="A count of things"), - three_choice: s.number( "threechoice", "i8", nc(minimum=0, exclusiveMaximum=3), doc="A choice between 0, 1, or 2"), - flag: s.boolean( "Flag", doc="Parameter that can be used to enable or disable functionality"), - monitoring_dest: s.enum( "MonitoringDest", ["local", "cern", "pocket"]), - path: s.string( "Path", doc="Location on a filesystem"), - paths: s.sequence( "Paths", self.path, doc="Multiple paths"), - host: s.string( "Host", moo.re.dnshost, doc="A hostname"), - hosts: s.sequence( "Hosts", self.host, "Multiple hosts"), - string: s.string( "Str", doc="Generic string"), - strings: s.sequence( "Strings", self.string, doc="List of strings"), - tpg_channel_map: s.enum( "TPGChannelMap", ["VDColdboxChannelMap", "ProtoDUNESP1ChannelMap", "PD2HDChannelMap", "HDColdboxChannelMap"]), - dqm_channel_map: s.enum( "DQMChannelMap", ['HD', 'VD', 'PD2HD', 'HDCB']), - dqm_params: s.sequence( "DQMParams", self.count, doc="Parameters for DQM (fixme)"), - tc_types: s.sequence( "TCTypes", self.count, doc="List of TC types"), - tc_type: s.number( "TCType", "i4", nc(minimum=0, maximum=9), doc="Number representing TC type. Currently ranging from 0 to 9"), - tc_interval: s.number( "TCInterval", "i8", nc(minimum=1, maximum=30000000000), doc="The intervals between TCs that are inserted into MLT by CTCM, in clock ticks"), - tc_intervals: s.sequence( "TCIntervals", self.tc_interval, doc="List of TC intervals used by CTCM"), - readout_time: s.number( "ROTime", "i8", doc="A readout time in ticks"), - channel_list: s.sequence( "ChannelList", self.count, doc="List of offline channels to be masked out from the TPHandler"), - tpg_algo_choice: s.enum( "TPGAlgoChoice", ["SimpleThreshold", "AbsRS"], doc="Trigger algorithm choice"), - pm_choice: s.enum( "PMChoice", ["k8s", "ssh"], doc="Process Manager choice: ssh or Kubernetes"), - rte_choice: s.enum( "RTEChoice", ["auto", "release", "devarea"], doc="Kubernetes DAQ application RTE choice"), daq_common : s.record("daq_common", [ - s.field( "data_request_timeout_ms", self.count, default=1000, doc="The baseline data request timeout that will be used by modules in the Readout and Trigger subsystems (i.e. any module that produces data fragments). Downstream timeouts, such as the trigger-record-building timeout, are derived from this."), - s.field( "use_data_network", self.flag, default = false, doc="Whether to use the data network (Won't work with k8s)"), - s.field( "data_rate_slowdown_factor",self.count, default=1, doc="Factor by which to suppress data generation. Former -s"), + s.field( "data_request_timeout_ms", types.count, default=1000, doc="The baseline data request timeout that will be used by modules in the Readout and Trigger subsystems (i.e. any module that produces data fragments). Downstream timeouts, such as the trigger-record-building timeout, are derived from this."), + s.field( "use_data_network", types.flag, default = false, doc="Whether to use the data network (Won't work with k8s)"), + s.field( "data_rate_slowdown_factor",types.count, default=1, doc="Factor by which to suppress data generation. Former -s"), ], doc="Common daq_common settings"), }; -moo.oschema.sort_select(cs) +stypes + moo.oschema.sort_select(cs) diff --git a/schema/daqconf/dataflowgen.jsonnet b/schema/daqconf/dataflowgen.jsonnet index b7a18c9a..95a9123b 100644 --- a/schema/daqconf/dataflowgen.jsonnet +++ b/schema/daqconf/dataflowgen.jsonnet @@ -3,61 +3,38 @@ local moo = import "moo.jsonnet"; +local stypes = import "daqconf/types.jsonnet"; +local types = moo.oschema.hier(stypes).dunedaq.daqconf.types; + local s = moo.oschema.schema("dunedaq.daqconf.dataflowgen"); local nc = moo.oschema.numeric_constraints; local cs = { - port: s.number( "Port", "i4", doc="A TCP/IP port number"), - freq: s.number( "Frequency", "u4", doc="A frequency"), - rate: s.number( "Rate", "f8", doc="A rate as a double"), - count: s.number( "count", "i8", doc="A count of things"), - three_choice: s.number( "threechoice", "i8", nc(minimum=0, exclusiveMaximum=3), doc="A choice between 0, 1, or 2"), - flag: s.boolean( "Flag", doc="Parameter that can be used to enable or disable functionality"), - monitoring_dest: s.enum( "MonitoringDest", ["local", "cern", "pocket"]), - path: s.string( "Path", doc="Location on a filesystem"), - paths: s.sequence( "Paths", self.path, doc="Multiple paths"), - host: s.string( "Host", moo.re.dnshost, doc="A hostname"), - hosts: s.sequence( "Hosts", self.host, "Multiple hosts"), - string: s.string( "Str", doc="Generic string"), - strings: s.sequence( "Strings", self.string, doc="List of strings"), - tpg_channel_map: s.enum( "TPGChannelMap", ["VDColdboxChannelMap", "ProtoDUNESP1ChannelMap", "PD2HDChannelMap", "HDColdboxChannelMap"]), - dqm_channel_map: s.enum( "DQMChannelMap", ['HD', 'VD', 'PD2HD', 'HDCB']), - dqm_params: s.sequence( "DQMParams", self.count, doc="Parameters for DQM (fixme)"), - tc_types: s.sequence( "TCTypes", self.count, doc="List of TC types"), - tc_type: s.number( "TCType", "i4", nc(minimum=0, maximum=9), doc="Number representing TC type. Currently ranging from 0 to 9"), - tc_interval: s.number( "TCInterval", "i8", nc(minimum=1, maximum=30000000000), doc="The intervals between TCs that are inserted into MLT by CTCM, in clock ticks"), - tc_intervals: s.sequence( "TCIntervals", self.tc_interval, doc="List of TC intervals used by CTCM"), - readout_time: s.number( "ROTime", "i8", doc="A readout time in ticks"), - channel_list: s.sequence( "ChannelList", self.count, doc="List of offline channels to be masked out from the TPHandler"), - tpg_algo_choice: s.enum( "TPGAlgoChoice", ["SimpleThreshold", "AbsRS"], doc="Trigger algorithm choice"), - pm_choice: s.enum( "PMChoice", ["k8s", "ssh"], doc="Process Manager choice: ssh or Kubernetes"), - rte_choice: s.enum( "RTEChoice", ["auto", "release", "devarea"], doc="Kubernetes DAQ application RTE choice"), - dataflowapp: s.record("dataflowapp",[ - s.field( "app_name", self.string, default="dataflow0"), - s.field( "output_paths",self.paths, default=['.'], doc="Location(s) for the dataflow app to write data. Former -o"), - s.field( "host_df", self.host, default='localhost'), - s.field( "max_file_size",self.count, default=4*1024*1024*1024, doc="The size threshold when raw data files are closed (in bytes)"), - s.field( "data_store_mode", self.string, default="all-per-file", doc="all-per-file or one-event-per-file"), - s.field( "max_trigger_record_window",self.count, default=0, doc="The maximum size for the window of data that will included in a single TriggerRecord (in ticks). Readout windows that are longer than this size will result in TriggerRecords being split into a sequence of TRs. A zero value for this parameter means no splitting."), + s.field( "app_name", types.string, default="dataflow0"), + s.field( "output_paths",types.paths, default=['.'], doc="Location(s) for the dataflow app to write data. Former -o"), + s.field( "host_df", types.host, default='localhost'), + s.field( "max_file_size",types.count, default=4*1024*1024*1024, doc="The size threshold when raw data files are closed (in bytes)"), + s.field( "data_store_mode", types.string, default="all-per-file", doc="all-per-file or one-event-per-file"), + s.field( "max_trigger_record_window",types.count, default=0, doc="The maximum size for the window of data that will included in a single TriggerRecord (in ticks). Readout windows that are longer than this size will result in TriggerRecords being split into a sequence of TRs. A zero value for this parameter means no splitting."), ], doc="Element of the dataflow.apps array"), dataflowapps: s.sequence("dataflowapps", self.dataflowapp, doc="List of dataflowapp instances"), dataflow: s.record("dataflow", [ - s.field( "host_dfo", self.host, default='localhost', doc="Sets the host for the DFO app"), + s.field( "host_dfo", types.host, default='localhost', doc="Sets the host for the DFO app"), s.field( "apps", self.dataflowapps, default=[], doc="Configuration for the dataflow apps (see dataflowapp for options)"), - s.field( "token_count",self.count, default=10, doc="Number of tokens the dataflow apps give to the DFO. Former -c"), + s.field( "token_count",types.count, default=10, doc="Number of tokens the dataflow apps give to the DFO. Former -c"), // Trigger - s.field( "host_tpw", self.host, default='localhost', doc='Host to run the TPWriter app on'), - s.field( "enable_tpset_writing", self.flag, default=false, doc="Enable the writing of TPs to disk (only works with enable_tpg or enable_firmware_tpg)"), - s.field( "tpset_output_path", self.path,default='.', doc="Output directory for TPSet stream files"), - s.field( "tpset_output_file_size",self.count, default=4*1024*1024*1024, doc="The size threshold when TPSet stream files are closed (in bytes)"), + s.field( "host_tpw", types.host, default='localhost', doc='Host to run the TPWriter app on'), + s.field( "enable_tpset_writing", types.flag, default=false, doc="Enable the writing of TPs to disk (only works with enable_tpg or enable_firmware_tpg)"), + s.field( "tpset_output_path", types.path,default='.', doc="Output directory for TPSet stream files"), + s.field( "tpset_output_file_size",types.count, default=4*1024*1024*1024, doc="The size threshold when TPSet stream files are closed (in bytes)"), ]), }; -moo.oschema.sort_select(cs) +stypes + moo.oschema.sort_select(cs) diff --git a/schema/daqconf/detectorgen.jsonnet b/schema/daqconf/detectorgen.jsonnet index a7a0ab5d..8476047a 100644 --- a/schema/daqconf/detectorgen.jsonnet +++ b/schema/daqconf/detectorgen.jsonnet @@ -3,44 +3,23 @@ local moo = import "moo.jsonnet"; +local stypes = import "daqconf/types.jsonnet"; +local types = moo.oschema.hier(stypes).dunedaq.daqconf.types; + local s = moo.oschema.schema("dunedaq.daqconf.detectorgen"); local nc = moo.oschema.numeric_constraints; // A temporary schema construction context. local cs = { - port: s.number( "Port", "i4", doc="A TCP/IP port number"), - freq: s.number( "Frequency", "u4", doc="A frequency"), - rate: s.number( "Rate", "f8", doc="A rate as a double"), - count: s.number( "count", "i8", doc="A count of things"), - three_choice: s.number( "threechoice", "i8", nc(minimum=0, exclusiveMaximum=3), doc="A choice between 0, 1, or 2"), - flag: s.boolean( "Flag", doc="Parameter that can be used to enable or disable functionality"), - monitoring_dest: s.enum( "MonitoringDest", ["local", "cern", "pocket"]), - path: s.string( "Path", doc="Location on a filesystem"), - paths: s.sequence( "Paths", self.path, doc="Multiple paths"), - host: s.string( "Host", moo.re.dnshost, doc="A hostname"), - hosts: s.sequence( "Hosts", self.host, "Multiple hosts"), - string: s.string( "Str", doc="Generic string"), - strings: s.sequence( "Strings", self.string, doc="List of strings"), - tpg_channel_map: s.enum( "TPGChannelMap", ["VDColdboxChannelMap", "ProtoDUNESP1ChannelMap", "PD2HDChannelMap", "HDColdboxChannelMap"]), - dqm_channel_map: s.enum( "DQMChannelMap", ['HD', 'VD', 'PD2HD', 'HDCB']), - dqm_params: s.sequence( "DQMParams", self.count, doc="Parameters for DQM (fixme)"), - tc_types: s.sequence( "TCTypes", self.count, doc="List of TC types"), - tc_type: s.number( "TCType", "i4", nc(minimum=0, maximum=9), doc="Number representing TC type. Currently ranging from 0 to 9"), - tc_interval: s.number( "TCInterval", "i8", nc(minimum=1, maximum=30000000000), doc="The intervals between TCs that are inserted into MLT by CTCM, in clock ticks"), - tc_intervals: s.sequence( "TCIntervals", self.tc_interval, doc="List of TC intervals used by CTCM"), - readout_time: s.number( "ROTime", "i8", doc="A readout time in ticks"), - channel_list: s.sequence( "ChannelList", self.count, doc="List of offline channels to be masked out from the TPHandler"), - tpg_algo_choice: s.enum( "TPGAlgoChoice", ["SimpleThreshold", "AbsRS"], doc="Trigger algorithm choice"), - pm_choice: s.enum( "PMChoice", ["k8s", "ssh"], doc="Process Manager choice: ssh or Kubernetes"), - rte_choice: s.enum( "RTEChoice", ["auto", "release", "devarea"], doc="Kubernetes DAQ application RTE choice"), + tpg_channel_map: s.enum( "TPGChannelMap", ["VDColdboxChannelMap", "ProtoDUNESP1ChannelMap", "PD2HDChannelMap", "HDColdboxChannelMap"]), detector : s.record("detector", [ - s.field( "op_env", self.string, default='swtest', doc="Operational environment - used for HDF5 Attribute inside the files"), - s.field( "clock_speed_hz", self.freq, default=62500000), + s.field( "op_env", types.string, default='swtest', doc="Operational environment - used for HDF5 Attribute inside the files"), + s.field( "clock_speed_hz", types.freq, default=62500000), s.field( "tpg_channel_map", self.tpg_channel_map, default="PD2HDChannelMap", doc="Channel map for TPG"), ], doc="Global common settings"), }; -moo.oschema.sort_select(cs) +stypes + moo.oschema.sort_select(cs) diff --git a/schema/daqconf/dqmgen.jsonnet b/schema/daqconf/dqmgen.jsonnet index 5778c3f6..808b7879 100644 --- a/schema/daqconf/dqmgen.jsonnet +++ b/schema/daqconf/dqmgen.jsonnet @@ -3,55 +3,33 @@ local moo = import "moo.jsonnet"; +local stypes = import "daqconf/types.jsonnet"; +local types = moo.oschema.hier(stypes).dunedaq.daqconf.types; + local s = moo.oschema.schema("dunedaq.daqconf.dqmgen"); local nc = moo.oschema.numeric_constraints; local cs = { - port: s.number( "Port", "i4", doc="A TCP/IP port number"), - freq: s.number( "Frequency", "u4", doc="A frequency"), - rate: s.number( "Rate", "f8", doc="A rate as a double"), - count: s.number( "count", "i8", doc="A count of things"), - three_choice: s.number( "threechoice", "i8", nc(minimum=0, exclusiveMaximum=3), doc="A choice between 0, 1, or 2"), - flag: s.boolean( "Flag", doc="Parameter that can be used to enable or disable functionality"), monitoring_dest: s.enum( "MonitoringDest", ["local", "cern", "pocket"]), - path: s.string( "Path", doc="Location on a filesystem"), - paths: s.sequence( "Paths", self.path, doc="Multiple paths"), - host: s.string( "Host", moo.re.dnshost, doc="A hostname"), - hosts: s.sequence( "Hosts", self.host, "Multiple hosts"), - string: s.string( "Str", doc="Generic string"), - strings: s.sequence( "Strings", self.string, doc="List of strings"), - tpg_channel_map: s.enum( "TPGChannelMap", ["VDColdboxChannelMap", "ProtoDUNESP1ChannelMap", "PD2HDChannelMap", "HDColdboxChannelMap"]), dqm_channel_map: s.enum( "DQMChannelMap", ['HD', 'VD', 'PD2HD', 'HDCB']), - dqm_params: s.sequence( "DQMParams", self.count, doc="Parameters for DQM (fixme)"), - tc_types: s.sequence( "TCTypes", self.count, doc="List of TC types"), - tc_type: s.number( "TCType", "i4", nc(minimum=0, maximum=9), doc="Number representing TC type. Currently ranging from 0 to 9"), - tc_interval: s.number( "TCInterval", "i8", nc(minimum=1, maximum=30000000000), doc="The intervals between TCs that are inserted into MLT by CTCM, in clock ticks"), - tc_intervals: s.sequence( "TCIntervals", self.tc_interval, doc="List of TC intervals used by CTCM"), - readout_time: s.number( "ROTime", "i8", doc="A readout time in ticks"), - channel_list: s.sequence( "ChannelList", self.count, doc="List of offline channels to be masked out from the TPHandler"), - tpg_algo_choice: s.enum( "TPGAlgoChoice", ["SimpleThreshold", "AbsRS"], doc="Trigger algorithm choice"), - pm_choice: s.enum( "PMChoice", ["k8s", "ssh"], doc="Process Manager choice: ssh or Kubernetes"), - rte_choice: s.enum( "RTEChoice", ["auto", "release", "devarea"], doc="Kubernetes DAQ application RTE choice"), - - + dqm_params: s.sequence( "DQMParams", types.count, doc="Parameters for DQM (fixme)"), dqm: s.record("dqm", [ - s.field('enable_dqm', self.flag, default=false, doc="Enable Data Quality Monitoring"), + s.field('enable_dqm', types.flag, default=false, doc="Enable Data Quality Monitoring"), s.field('impl', self.monitoring_dest, default='local', doc="DQM destination (Kafka used for cern and pocket)"), s.field('cmap', self.dqm_channel_map, default='HD', doc="Which channel map to use for DQM"), - s.field('host_dqm', self.hosts, default=['localhost'], doc='Host(s) to run the DQM app on'), + s.field('host_dqm', types.hosts, default=['localhost'], doc='Host(s) to run the DQM app on'), s.field('raw_params', self.dqm_params, default=[60, 50], doc="Parameters that control the data sent for the raw display plot"), s.field('std_params', self.dqm_params, default=[10, 1000], doc="Parameters that control the data sent for the mean/rms plot"), s.field('rms_params', self.dqm_params, default=[0, 1000], doc="Parameters that control the data sent for the mean/rms plot"), s.field('fourier_channel_params', self.dqm_params, default=[0, 0], doc="Parameters that control the data sent for the fourier transform plot"), s.field('fourier_plane_params', self.dqm_params, default=[600, 1000], doc="Parameters that control the data sent for the summed fourier transform plot"), - s.field('df_rate', self.count, default=10, doc='How many seconds between requests to DF for Trigger Records'), - s.field('df_algs', self.string, default='raw std fourier_plane', doc='Algorithms to be run on Trigger Records from DF (use quotes)'), - s.field('max_num_frames', self.count, default=32768, doc='Maximum number of frames to use in the algorithms'), - s.field('kafka_address', self.string, default='', doc='kafka address used to send messages'), - s.field('kafka_topic', self.string, default='DQM', doc='kafka topic used to send messages'), + s.field('df_rate', types.count, default=10, doc='How many seconds between requests to DF for Trigger Records'), + s.field('df_algs', types.string, default='raw std fourier_plane', doc='Algorithms to be run on Trigger Records from DF (use quotes)'), + s.field('max_num_frames', types.count, default=32768, doc='Maximum number of frames to use in the algorithms'), + s.field('kafka_address', types.string, default='', doc='kafka address used to send messages'), + s.field('kafka_topic', types.string, default='DQM', doc='kafka topic used to send messages'), ]), }; - -moo.oschema.sort_select(cs) +stypes + moo.oschema.sort_select(cs) diff --git a/schema/daqconf/hsigen.jsonnet b/schema/daqconf/hsigen.jsonnet index 9c3640ea..a9ffccbe 100644 --- a/schema/daqconf/hsigen.jsonnet +++ b/schema/daqconf/hsigen.jsonnet @@ -3,62 +3,39 @@ local moo = import "moo.jsonnet"; +local stypes = import "daqconf/types.jsonnet"; +local types = moo.oschema.hier(stypes).dunedaq.daqconf.types; + local s = moo.oschema.schema("dunedaq.daqconf.hsigen"); local nc = moo.oschema.numeric_constraints; local cs = { - port: s.number( "Port", "i4", doc="A TCP/IP port number"), - freq: s.number( "Frequency", "u4", doc="A frequency"), - rate: s.number( "Rate", "f8", doc="A rate as a double"), - count: s.number( "count", "i8", doc="A count of things"), - three_choice: s.number( "threechoice", "i8", nc(minimum=0, exclusiveMaximum=3), doc="A choice between 0, 1, or 2"), - flag: s.boolean( "Flag", doc="Parameter that can be used to enable or disable functionality"), - monitoring_dest: s.enum( "MonitoringDest", ["local", "cern", "pocket"]), - path: s.string( "Path", doc="Location on a filesystem"), - paths: s.sequence( "Paths", self.path, doc="Multiple paths"), - host: s.string( "Host", moo.re.dnshost, doc="A hostname"), - hosts: s.sequence( "Hosts", self.host, "Multiple hosts"), - string: s.string( "Str", doc="Generic string"), - strings: s.sequence( "Strings", self.string, doc="List of strings"), - tpg_channel_map: s.enum( "TPGChannelMap", ["VDColdboxChannelMap", "ProtoDUNESP1ChannelMap", "PD2HDChannelMap", "HDColdboxChannelMap"]), - dqm_channel_map: s.enum( "DQMChannelMap", ['HD', 'VD', 'PD2HD', 'HDCB']), - dqm_params: s.sequence( "DQMParams", self.count, doc="Parameters for DQM (fixme)"), - tc_types: s.sequence( "TCTypes", self.count, doc="List of TC types"), - tc_type: s.number( "TCType", "i4", nc(minimum=0, maximum=9), doc="Number representing TC type. Currently ranging from 0 to 9"), - tc_interval: s.number( "TCInterval", "i8", nc(minimum=1, maximum=30000000000), doc="The intervals between TCs that are inserted into MLT by CTCM, in clock ticks"), - tc_intervals: s.sequence( "TCIntervals", self.tc_interval, doc="List of TC intervals used by CTCM"), - readout_time: s.number( "ROTime", "i8", doc="A readout time in ticks"), - channel_list: s.sequence( "ChannelList", self.count, doc="List of offline channels to be masked out from the TPHandler"), - tpg_algo_choice: s.enum( "TPGAlgoChoice", ["SimpleThreshold", "AbsRS"], doc="Trigger algorithm choice"), - pm_choice: s.enum( "PMChoice", ["k8s", "ssh"], doc="Process Manager choice: ssh or Kubernetes"), - rte_choice: s.enum( "RTEChoice", ["auto", "release", "devarea"], doc="Kubernetes DAQ application RTE choice"), - hsi: s.record("hsi", [ # timing hsi options - s.field( "use_timing_hsi", self.flag, default=false, doc='Flag to control whether real hardware timing HSI config is generated. Default is false'), - s.field( "host_timing_hsi", self.host, default='localhost', doc='Host to run the HSI app on'), - s.field( "hsi_hw_connections_file", self.path, default="${TIMING_SHARE}/config/etc/connections.xml", doc='Real timing hardware only: path to hardware connections file'), - s.field( "enable_hardware_state_recovery", self.flag, default=true, doc="Enable (or not) hardware state recovery"), - s.field( "hsi_device_name", self.string, default="", doc='Real HSI hardware only: device name of HSI hw'), - s.field( "hsi_readout_period", self.count, default=1e3, doc='Real HSI hardware only: Period between HSI hardware polling [us]'), - s.field( "control_hsi_hw", self.flag, default=false, doc='Flag to control whether we are controlling hsi hardware'), - s.field( "hsi_endpoint_address", self.count, default=1, doc='Timing address of HSI endpoint'), - s.field( "hsi_endpoint_partition", self.count, default=0, doc='Timing partition of HSI endpoint'), - s.field( "hsi_re_mask",self.count, default=0, doc='Rising-edge trigger mask'), - s.field( "hsi_fe_mask", self.count, default=0, doc='Falling-edge trigger mask'), - s.field( "hsi_inv_mask",self.count, default=0, doc='Invert-edge mask'), - s.field( "hsi_source",self.count, default=1, doc='HSI signal source; 0 - hardware, 1 - emulation (trigger timestamp bits)'), + s.field( "use_timing_hsi", types.flag, default=false, doc='Flag to control whether real hardware timing HSI config is generated. Default is false'), + s.field( "host_timing_hsi", types.host, default='localhost', doc='Host to run the HSI app on'), + s.field( "hsi_hw_connections_file", types.path, default="${TIMING_SHARE}/config/etc/connections.xml", doc='Real timing hardware only: path to hardware connections file'), + s.field( "enable_hardware_state_recovery", types.flag, default=true, doc="Enable (or not) hardware state recovery"), + s.field( "hsi_device_name", types.string, default="", doc='Real HSI hardware only: device name of HSI hw'), + s.field( "hsi_readout_period", types.count, default=1e3, doc='Real HSI hardware only: Period between HSI hardware polling [us]'), + s.field( "control_hsi_hw", types.flag, default=false, doc='Flag to control whether we are controlling hsi hardware'), + s.field( "hsi_endpoint_address", types.count, default=1, doc='Timing address of HSI endpoint'), + s.field( "hsi_endpoint_partition", types.count, default=0, doc='Timing partition of HSI endpoint'), + s.field( "hsi_re_mask",types.count, default=0, doc='Rising-edge trigger mask'), + s.field( "hsi_fe_mask", types.count, default=0, doc='Falling-edge trigger mask'), + s.field( "hsi_inv_mask",types.count, default=0, doc='Invert-edge mask'), + s.field( "hsi_source",types.count, default=1, doc='HSI signal source; 0 - hardware, 1 - emulation (trigger timestamp bits)'), # fake hsi options - s.field( "use_fake_hsi", self.flag, default=true, doc='Flag to control whether fake or real hardware HSI config is generated. Default is true'), - s.field( "host_fake_hsi", self.host, default='localhost', doc='Host to run the HSI app on'), - s.field( "hsi_device_id", self.count, default=0, doc='Fake HSI only: device ID of fake HSIEvents'), - s.field( "mean_hsi_signal_multiplicity", self.count, default=1, doc='Fake HSI only: rate of individual HSI signals in emulation mode 1'), - s.field( "hsi_signal_emulation_mode", self.count, default=0, doc='Fake HSI only: HSI signal emulation mode'), - s.field( "enabled_hsi_signals", self.count, default=1, doc='Fake HSI only: bit mask of enabled fake HSI signals') + s.field( "use_fake_hsi", types.flag, default=true, doc='Flag to control whether fake or real hardware HSI config is generated. Default is true'), + s.field( "host_fake_hsi", types.host, default='localhost', doc='Host to run the HSI app on'), + s.field( "hsi_device_id", types.count, default=0, doc='Fake HSI only: device ID of fake HSIEvents'), + s.field( "mean_hsi_signal_multiplicity", types.count, default=1, doc='Fake HSI only: rate of individual HSI signals in emulation mode 1'), + s.field( "hsi_signal_emulation_mode", types.count, default=0, doc='Fake HSI only: HSI signal emulation mode'), + s.field( "enabled_hsi_signals", types.count, default=1, doc='Fake HSI only: bit mask of enabled fake HSI signals') ]), }; -moo.oschema.sort_select(cs) +stypes + moo.oschema.sort_select(cs) diff --git a/schema/daqconf/readoutgen.jsonnet b/schema/daqconf/readoutgen.jsonnet index 68687168..11731b35 100644 --- a/schema/daqconf/readoutgen.jsonnet +++ b/schema/daqconf/readoutgen.jsonnet @@ -3,89 +3,68 @@ local moo = import "moo.jsonnet"; +local stypes = import "daqconf/types.jsonnet"; +local types = moo.oschema.hier(stypes).dunedaq.daqconf.types; + local s = moo.oschema.schema("dunedaq.daqconf.readoutgen"); local nc = moo.oschema.numeric_constraints; // A temporary schema construction context. local cs = { - port: s.number( "Port", "i4", doc="A TCP/IP port number"), - freq: s.number( "Frequency", "u4", doc="A frequency"), - rate: s.number( "Rate", "f8", doc="A rate as a double"), - count: s.number( "count", "i8", doc="A count of things"), - three_choice: s.number( "threechoice", "i8", nc(minimum=0, exclusiveMaximum=3), doc="A choice between 0, 1, or 2"), - flag: s.boolean( "Flag", doc="Parameter that can be used to enable or disable functionality"), - monitoring_dest: s.enum( "MonitoringDest", ["local", "cern", "pocket"]), - path: s.string( "Path", doc="Location on a filesystem"), - paths: s.sequence( "Paths", self.path, doc="Multiple paths"), - host: s.string( "Host", moo.re.dnshost, doc="A hostname"), - hosts: s.sequence( "Hosts", self.host, "Multiple hosts"), - string: s.string( "Str", doc="Generic string"), - strings: s.sequence( "Strings", self.string, doc="List of strings"), - tpg_channel_map: s.enum( "TPGChannelMap", ["VDColdboxChannelMap", "ProtoDUNESP1ChannelMap", "PD2HDChannelMap", "HDColdboxChannelMap"]), - dqm_channel_map: s.enum( "DQMChannelMap", ['HD', 'VD', 'PD2HD', 'HDCB']), - dqm_params: s.sequence( "DQMParams", self.count, doc="Parameters for DQM (fixme)"), - tc_types: s.sequence( "TCTypes", self.count, doc="List of TC types"), - tc_type: s.number( "TCType", "i4", nc(minimum=0, maximum=9), doc="Number representing TC type. Currently ranging from 0 to 9"), - tc_interval: s.number( "TCInterval", "i8", nc(minimum=1, maximum=30000000000), doc="The intervals between TCs that are inserted into MLT by CTCM, in clock ticks"), - tc_intervals: s.sequence( "TCIntervals", self.tc_interval, doc="List of TC intervals used by CTCM"), - readout_time: s.number( "ROTime", "i8", doc="A readout time in ticks"), - channel_list: s.sequence( "ChannelList", self.count, doc="List of offline channels to be masked out from the TPHandler"), - tpg_algo_choice: s.enum( "TPGAlgoChoice", ["SimpleThreshold", "AbsRS"], doc="Trigger algorithm choice"), - pm_choice: s.enum( "PMChoice", ["k8s", "ssh"], doc="Process Manager choice: ssh or Kubernetes"), - rte_choice: s.enum( "RTEChoice", ["auto", "release", "devarea"], doc="Kubernetes DAQ application RTE choice"), + channel_list: s.sequence( "ChannelList", types.count, doc="List of offline channels to be masked out from the TPHandler"), data_file_entry: s.record("data_file_entry", [ - s.field( "data_file", self.path, default='./frames.bin', doc="File containing data frames to be replayed by the fake cards. Former -d. Uses the asset manager, can also be 'asset://checksum/somelonghash', or 'file://somewhere/frames.bin' or 'frames.bin'"), - s.field( "detector_id", self.count, default=3, doc="Detector ID that this file applies to"), + s.field( "data_file", types.path, default='./frames.bin', doc="File containing data frames to be replayed by the fake cards. Former -d. Uses the asset manager, can also be 'asset://checksum/somelonghash', or 'file://somewhere/frames.bin' or 'frames.bin'"), + s.field( "detector_id", types.count, default=3, doc="Detector ID that this file applies to"), ]), data_files: s.sequence("data_files", self.data_file_entry), numa_exception: s.record( "NUMAException", [ - s.field( "host", self.host, default='localhost', doc="Host of exception"), - s.field( "card", self.count, default=0, doc="Card ID of exception"), - s.field( "numa_id", self.count, default=0, doc="NUMA ID of exception"), - s.field( "felix_card_id", self.count, default=-1, doc="CARD ID override, -1 indicates no override"), - s.field( "latency_buffer_numa_aware", self.flag, default=false, doc="Enable NUMA-aware mode for the Latency Buffer"), - s.field( "latency_buffer_preallocation", self.flag, default=false, doc="Enable Latency Buffer preallocation"), + s.field( "host", types.host, default='localhost', doc="Host of exception"), + s.field( "card", types.count, default=0, doc="Card ID of exception"), + s.field( "numa_id", types.count, default=0, doc="NUMA ID of exception"), + s.field( "felix_card_id", types.count, default=-1, doc="CARD ID override, -1 indicates no override"), + s.field( "latency_buffer_numa_aware", types.flag, default=false, doc="Enable NUMA-aware mode for the Latency Buffer"), + s.field( "latency_buffer_preallocation", types.flag, default=false, doc="Enable Latency Buffer preallocation"), ], doc="Exception to the default NUMA ID for FELIX cards"), numa_exceptions: s.sequence( "NUMAExceptions", self.numa_exception, doc="Exceptions to the default NUMA ID"), numa_config: s.record("numa_config", [ - s.field( "default_id", self.count, default=0, doc="Default NUMA ID for FELIX cards"), - s.field( "default_latency_numa_aware", self.flag, default=false, doc="Default for Latency Buffer NUMA awareness"), - s.field( "default_latency_preallocation", self.flag, default=false, doc="Default for Latency Buffer Preallocation"), + s.field( "default_id", types.count, default=0, doc="Default NUMA ID for FELIX cards"), + s.field( "default_latency_numa_aware", types.flag, default=false, doc="Default for Latency Buffer NUMA awareness"), + s.field( "default_latency_preallocation", types.flag, default=false, doc="Default for Latency Buffer Preallocation"), s.field( "exceptions", self.numa_exceptions, default=[], doc="Exceptions to the default NUMA ID"), ]), readout: s.record("readout", [ - s.field( "detector_readout_map_file", self.path, default='./DetectorReadoutMap.json', doc="File containing detector hardware map for configuration to run"), - s.field( "use_fake_data_producers", self.flag, default=false, doc="Use fake data producers that respond with empty fragments immediately instead of (fake) cards and DLHs"), - // s.field( "memory_limit_gb", self.count, default=64, doc="Application memory limit in GB") + s.field( "detector_readout_map_file", types.path, default='./DetectorReadoutMap.json', doc="File containing detector hardware map for configuration to run"), + s.field( "use_fake_data_producers", types.flag, default=false, doc="Use fake data producers that respond with empty fragments immediately instead of (fake) cards and DLHs"), + // s.field( "memory_limit_gb", types.count, default=64, doc="Application memory limit in GB") // Fake cards - s.field( "use_fake_cards", self.flag, default=false, doc="Use fake cards"), - s.field( "emulated_data_times_start_with_now", self.flag, default=false, doc="If active, the timestamp of the first emulated data frame is set to the current wallclock time"), - s.field( "default_data_file", self.path, default='asset://?label=ProtoWIB&subsystem=readout', doc="File containing data frames to be replayed by the fake cards. Former -d. Uses the asset manager, can also be 'asset://?checksum=somelonghash', or 'file://somewhere/frames.bin' or 'frames.bin'"), + s.field( "use_fake_cards", types.flag, default=false, doc="Use fake cards"), + s.field( "emulated_data_times_start_with_now", types.flag, default=false, doc="If active, the timestamp of the first emulated data frame is set to the current wallclock time"), + s.field( "default_data_file", types.path, default='asset://?label=ProtoWIB&subsystem=readout', doc="File containing data frames to be replayed by the fake cards. Former -d. Uses the asset manager, can also be 'asset://?checksum=somelonghash', or 'file://somewhere/frames.bin' or 'frames.bin'"), s.field( "data_files", self.data_files, default=[], doc="Files to use by detector type"), // DPDK - s.field( "dpdk_eal_args", self.string, default='-l 0-1 -n 3 -- -m [0:1].0 -j', doc='Args passed to the EAL in DPDK'), - s.field( "dpdk_rxqueues_per_lcore", self.count, default=1, doc='Number of rx queues per core'), + s.field( "dpdk_eal_args", types.string, default='-l 0-1 -n 3 -- -m [0:1].0 -j', doc='Args passed to the EAL in DPDK'), + s.field( "dpdk_rxqueues_per_lcore", types.count, default=1, doc='Number of rx queues per core'), // FLX s.field( "numa_config", self.numa_config, default=self.numa_config, doc='Configuration of FELIX NUMA IDs'), // DLH - s.field( "emulator_mode", self.flag, default=false, doc="If active, timestamps of data frames are overwritten when processed by the readout. This is necessary if the felix card does not set correct timestamps. Former -e"), - s.field( "thread_pinning_file", self.path, default="", doc="A thread pinning configuration file that gets executed after conf."), - // s.field( "data_rate_slowdown_factor",self.count, default=1, doc="Factor by which to suppress data generation. Former -s"), - s.field( "latency_buffer_size", self.count, default=499968, doc="Size of the latency buffers (in number of elements)"), - s.field( "fragment_send_timeout_ms", self.count, default=10, doc="The send timeout that will be used in the readout modules when sending fragments downstream (i.e. to the TRB)."), - s.field( "enable_tpg", self.flag, default=false, doc="Enable TPG"), - s.field( "tpg_threshold", self.count, default=120, doc="Select TPG threshold"), - s.field( "tpg_algorithm", self.string, default="SimpleThreshold", doc="Select TPG algorithm (SimpleThreshold, AbsRS)"), + s.field( "emulator_mode", types.flag, default=false, doc="If active, timestamps of data frames are overwritten when processed by the readout. This is necessary if the felix card does not set correct timestamps. Former -e"), + s.field( "thread_pinning_file", types.path, default="", doc="A thread pinning configuration file that gets executed after conf."), + // s.field( "data_rate_slowdown_factor",types.count, default=1, doc="Factor by which to suppress data generation. Former -s"), + s.field( "latency_buffer_size", types.count, default=499968, doc="Size of the latency buffers (in number of elements)"), + s.field( "fragment_send_timeout_ms", types.count, default=10, doc="The send timeout that will be used in the readout modules when sending fragments downstream (i.e. to the TRB)."), + s.field( "enable_tpg", types.flag, default=false, doc="Enable TPG"), + s.field( "tpg_threshold", types.count, default=120, doc="Select TPG threshold"), + s.field( "tpg_algorithm", types.string, default="SimpleThreshold", doc="Select TPG algorithm (SimpleThreshold, AbsRS)"), s.field( "tpg_channel_mask", self.channel_list, default=[], doc="List of offline channels to be masked out from the TPHandler"), - s.field( "enable_raw_recording", self.flag, default=false, doc="Add queues and modules necessary for the record command"), - s.field( "raw_recording_output_dir", self.path, default='.', doc="Output directory where recorded data is written to. Data for each link is written to a separate file") + s.field( "enable_raw_recording", types.flag, default=false, doc="Add queues and modules necessary for the record command"), + s.field( "raw_recording_output_dir", types.path, default='.', doc="Output directory where recorded data is written to. Data for each link is written to a separate file") ]), }; -moo.oschema.sort_select(cs) +stypes + moo.oschema.sort_select(cs) diff --git a/schema/daqconf/timinggen.jsonnet b/schema/daqconf/timinggen.jsonnet index 69249a59..547a488d 100644 --- a/schema/daqconf/timinggen.jsonnet +++ b/schema/daqconf/timinggen.jsonnet @@ -3,48 +3,26 @@ local moo = import "moo.jsonnet"; +local stypes = import "daqconf/types.jsonnet"; +local types = moo.oschema.hier(stypes).dunedaq.daqconf.types; + local s = moo.oschema.schema("dunedaq.daqconf.timinggen"); local nc = moo.oschema.numeric_constraints; local cs = { - port: s.number( "Port", "i4", doc="A TCP/IP port number"), - freq: s.number( "Frequency", "u4", doc="A frequency"), - rate: s.number( "Rate", "f8", doc="A rate as a double"), - count: s.number( "count", "i8", doc="A count of things"), - three_choice: s.number( "threechoice", "i8", nc(minimum=0, exclusiveMaximum=3), doc="A choice between 0, 1, or 2"), - flag: s.boolean( "Flag", doc="Parameter that can be used to enable or disable functionality"), - monitoring_dest: s.enum( "MonitoringDest", ["local", "cern", "pocket"]), - path: s.string( "Path", doc="Location on a filesystem"), - paths: s.sequence( "Paths", self.path, doc="Multiple paths"), - host: s.string( "Host", moo.re.dnshost, doc="A hostname"), - hosts: s.sequence( "Hosts", self.host, "Multiple hosts"), - string: s.string( "Str", doc="Generic string"), - strings: s.sequence( "Strings", self.string, doc="List of strings"), - tpg_channel_map: s.enum( "TPGChannelMap", ["VDColdboxChannelMap", "ProtoDUNESP1ChannelMap", "PD2HDChannelMap", "HDColdboxChannelMap"]), - dqm_channel_map: s.enum( "DQMChannelMap", ['HD', 'VD', 'PD2HD', 'HDCB']), - dqm_params: s.sequence( "DQMParams", self.count, doc="Parameters for DQM (fixme)"), - tc_types: s.sequence( "TCTypes", self.count, doc="List of TC types"), - tc_type: s.number( "TCType", "i4", nc(minimum=0, maximum=9), doc="Number representing TC type. Currently ranging from 0 to 9"), - tc_interval: s.number( "TCInterval", "i8", nc(minimum=1, maximum=30000000000), doc="The intervals between TCs that are inserted into MLT by CTCM, in clock ticks"), - tc_intervals: s.sequence( "TCIntervals", self.tc_interval, doc="List of TC intervals used by CTCM"), - readout_time: s.number( "ROTime", "i8", doc="A readout time in ticks"), - channel_list: s.sequence( "ChannelList", self.count, doc="List of offline channels to be masked out from the TPHandler"), - tpg_algo_choice: s.enum( "TPGAlgoChoice", ["SimpleThreshold", "AbsRS"], doc="Trigger algorithm choice"), - pm_choice: s.enum( "PMChoice", ["k8s", "ssh"], doc="Process Manager choice: ssh or Kubernetes"), - rte_choice: s.enum( "RTEChoice", ["auto", "release", "devarea"], doc="Kubernetes DAQ application RTE choice"), timing: s.record("timing", [ - s.field( "timing_session_name", self.string, default="", doc="Name of the global timing session to use, for timing commands"), - s.field( "host_tprtc", self.host, default='localhost', doc='Host to run the timing partition controller app on'), + s.field( "timing_session_name", types.string, default="", doc="Name of the global timing session to use, for timing commands"), + s.field( "host_tprtc", types.host, default='localhost', doc='Host to run the timing partition controller app on'), # timing hw partition options - s.field( "control_timing_partition", self.flag, default=false, doc='Flag to control whether we are controlling timing partition in master hardware'), - s.field( "timing_partition_master_device_name", self.string, default="", doc='Timing partition master hardware device name'), - s.field( "timing_partition_id", self.count, default=0, doc='Timing partition id'), - s.field( "timing_partition_trigger_mask", self.count, default=255, doc='Timing partition trigger mask'), - s.field( "timing_partition_rate_control_enabled", self.flag, default=false, doc='Timing partition rate control enabled'), - s.field( "timing_partition_spill_gate_enabled", self.flag, default=false, doc='Timing partition spill gate enabled'), + s.field( "control_timing_partition", types.flag, default=false, doc='Flag to control whether we are controlling timing partition in master hardware'), + s.field( "timing_partition_master_device_name", types.string, default="", doc='Timing partition master hardware device name'), + s.field( "timing_partition_id", types.count, default=0, doc='Timing partition id'), + s.field( "timing_partition_trigger_mask", types.count, default=255, doc='Timing partition trigger mask'), + s.field( "timing_partition_rate_control_enabled", types.flag, default=false, doc='Timing partition rate control enabled'), + s.field( "timing_partition_spill_gate_enabled", types.flag, default=false, doc='Timing partition spill gate enabled'), ]), }; -moo.oschema.sort_select(cs) +stypes + moo.oschema.sort_select(cs) diff --git a/schema/daqconf/triggergen.jsonnet b/schema/daqconf/triggergen.jsonnet index 256c6932..042d9c1e 100644 --- a/schema/daqconf/triggergen.jsonnet +++ b/schema/daqconf/triggergen.jsonnet @@ -3,47 +3,30 @@ local moo = import "moo.jsonnet"; +local stypes = import "daqconf/types.jsonnet"; +local types = moo.oschema.hier(stypes).dunedaq.daqconf.types; + local s = moo.oschema.schema("dunedaq.daqconf.triggergen"); local nc = moo.oschema.numeric_constraints; // A temporary schema construction context. local cs = { - port: s.number( "Port", "i4", doc="A TCP/IP port number"), - freq: s.number( "Frequency", "u4", doc="A frequency"), - rate: s.number( "Rate", "f8", doc="A rate as a double"), - count: s.number( "count", "i8", doc="A count of things"), - three_choice: s.number( "threechoice", "i8", nc(minimum=0, exclusiveMaximum=3), doc="A choice between 0, 1, or 2"), - flag: s.boolean( "Flag", doc="Parameter that can be used to enable or disable functionality"), - monitoring_dest: s.enum( "MonitoringDest", ["local", "cern", "pocket"]), - path: s.string( "Path", doc="Location on a filesystem"), - paths: s.sequence( "Paths", self.path, doc="Multiple paths"), - host: s.string( "Host", moo.re.dnshost, doc="A hostname"), - hosts: s.sequence( "Hosts", self.host, "Multiple hosts"), - string: s.string( "Str", doc="Generic string"), - strings: s.sequence( "Strings", self.string, doc="List of strings"), - tpg_channel_map: s.enum( "TPGChannelMap", ["VDColdboxChannelMap", "ProtoDUNESP1ChannelMap", "PD2HDChannelMap", "HDColdboxChannelMap"]), - dqm_channel_map: s.enum( "DQMChannelMap", ['HD', 'VD', 'PD2HD', 'HDCB']), - dqm_params: s.sequence( "DQMParams", self.count, doc="Parameters for DQM (fixme)"), - tc_types: s.sequence( "TCTypes", self.count, doc="List of TC types"), tc_type: s.number( "TCType", "i4", nc(minimum=0, maximum=9), doc="Number representing TC type. Currently ranging from 0 to 9"), + tc_types: s.sequence( "TCTypes", self.tc_type, doc="List of TC types"), tc_interval: s.number( "TCInterval", "i8", nc(minimum=1, maximum=30000000000), doc="The intervals between TCs that are inserted into MLT by CTCM, in clock ticks"), tc_intervals: s.sequence( "TCIntervals", self.tc_interval, doc="List of TC intervals used by CTCM"), readout_time: s.number( "ROTime", "i8", doc="A readout time in ticks"), - channel_list: s.sequence( "ChannelList", self.count, doc="List of offline channels to be masked out from the TPHandler"), - tpg_algo_choice: s.enum( "TPGAlgoChoice", ["SimpleThreshold", "AbsRS"], doc="Trigger algorithm choice"), - pm_choice: s.enum( "PMChoice", ["k8s", "ssh"], doc="Process Manager choice: ssh or Kubernetes"), - rte_choice: s.enum( "RTEChoice", ["auto", "release", "devarea"], doc="Kubernetes DAQ application RTE choice"), trigger_algo_config: s.record("trigger_algo_config", [ - s.field("prescale", self.count, default=100), - s.field("window_length", self.count, default=10000), - s.field("adjacency_threshold", self.count, default=6), - s.field("adj_tolerance", self.count, default=4), - s.field("trigger_on_adc", self.flag, default=false), - s.field("trigger_on_n_channels", self.flag, default=false), - s.field("trigger_on_adjacency", self.flag, default=true), - s.field("adc_threshold", self.count, default=10000), - s.field("n_channels_threshold", self.count, default=8), - s.field("print_tp_info", self.flag, default=false), + s.field("prescale", types.count, default=100), + s.field("window_length", types.count, default=10000), + s.field("adjacency_threshold", types.count, default=6), + s.field("adj_tolerance", types.count, default=4), + s.field("trigger_on_adc", types.flag, default=false), + s.field("trigger_on_n_channels", types.flag, default=false), + s.field("trigger_on_adjacency", types.flag, default=true), + s.field("adc_threshold", types.count, default=10000), + s.field("n_channels_threshold", types.count, default=8), + s.field("print_tp_info", types.flag, default=false), ]), c0_readout: s.record("c0_readout", [ @@ -111,37 +94,37 @@ local cs = { ]), trigger: s.record("trigger",[ - s.field( "trigger_rate_hz", self.rate, default=1.0, doc='Fake HSI only: rate at which fake HSIEvents are sent. 0 - disable HSIEvent generation. Former -t'), - s.field( "trigger_window_before_ticks",self.count, default=1000, doc="Trigger window before marker. Former -b"), - s.field( "trigger_window_after_ticks", self.count, default=1000, doc="Trigger window after marker. Former -a"), - s.field( "host_trigger", self.host, default='localhost', doc='Host to run the trigger app on'), - // s.field( "host_tpw", self.host, default='localhost', doc='Host to run the TPWriter app on'), + s.field( "trigger_rate_hz", types.rate, default=1.0, doc='Fake HSI only: rate at which fake HSIEvents are sent. 0 - disable HSIEvent generation. Former -t'), + s.field( "trigger_window_before_ticks",types.count, default=1000, doc="Trigger window before marker. Former -b"), + s.field( "trigger_window_after_ticks", types.count, default=1000, doc="Trigger window after marker. Former -a"), + s.field( "host_trigger", types.host, default='localhost', doc='Host to run the trigger app on'), + // s.field( "host_tpw", types.host, default='localhost', doc='Host to run the TPWriter app on'), # trigger options - s.field( "completeness_tolerance", self.count, default=1, doc="Maximum number of inactive queues we will tolerate."), - s.field( "tolerate_incompleteness", self.flag, default=false, doc="Flag to tell trigger to tolerate inactive queues."), - s.field( "ttcm_s1", self.count,default=1, doc="Timing trigger candidate maker accepted HSI signal ID 1"), - s.field( "ttcm_s2", self.count, default=2, doc="Timing trigger candidate maker accepted HSI signal ID 2"), - s.field( "trigger_activity_plugin", self.string, default='TriggerActivityMakerPrescalePlugin', doc="Trigger activity algorithm plugin"), + s.field( "completeness_tolerance", types.count, default=1, doc="Maximum number of inactive queues we will tolerate."), + s.field( "tolerate_incompleteness", types.flag, default=false, doc="Flag to tell trigger to tolerate inactive queues."), + s.field( "ttcm_s1", types.count,default=1, doc="Timing trigger candidate maker accepted HSI signal ID 1"), + s.field( "ttcm_s2", types.count, default=2, doc="Timing trigger candidate maker accepted HSI signal ID 2"), + s.field( "trigger_activity_plugin", types.string, default='TriggerActivityMakerPrescalePlugin', doc="Trigger activity algorithm plugin"), s.field( "trigger_activity_config", self.trigger_algo_config, default=self.trigger_algo_config,doc="Trigger activity algorithm config (string containing python dictionary)"), - s.field( "trigger_candidate_plugin", self.string, default='TriggerCandidateMakerPrescalePlugin', doc="Trigger candidate algorithm plugin"), + s.field( "trigger_candidate_plugin", types.string, default='TriggerCandidateMakerPrescalePlugin', doc="Trigger candidate algorithm plugin"), s.field( "trigger_candidate_config", self.trigger_algo_config, default=self.trigger_algo_config, doc="Trigger candidate algorithm config (string containing python dictionary)"), - s.field( "hsi_trigger_type_passthrough", self.flag, default=false, doc="Option to override trigger type in the MLT"), - // s.field( "enable_tpset_writing", self.flag, default=false, doc="Enable the writing of TPs to disk (only works with enable_tpg or enable_firmware_tpg)"), - // s.field( "tpset_output_path", self.path,default='.', doc="Output directory for TPSet stream files"), - // s.field( "tpset_output_file_size",self.count, default=4*1024*1024*1024, doc="The size threshold when TPSet stream files are closed (in bytes)"), + s.field( "hsi_trigger_type_passthrough", types.flag, default=false, doc="Option to override trigger type in the MLT"), + // s.field( "enable_tpset_writing", types.flag, default=false, doc="Enable the writing of TPs to disk (only works with enable_tpg or enable_firmware_tpg)"), + // s.field( "tpset_output_path", types.path,default='.', doc="Output directory for TPSet stream files"), + // s.field( "tpset_output_file_size",types.count, default=4*1024*1024*1024, doc="The size threshold when TPSet stream files are closed (in bytes)"), // s.field( "tpg_channel_map", self.tpg_channel_map, default="ProtoDUNESP1ChannelMap", doc="Channel map for TPG"), - s.field( "mlt_merge_overlapping_tcs", self.flag, default=true, doc="Option to turn off merging of overlapping TCs when forming TDs in MLT"), - s.field( "mlt_buffer_timeout", self.count, default=100, doc="Timeout (buffer) to wait for new overlapping TCs before sending TD"), - s.field( "mlt_send_timed_out_tds", self.flag, default=true, doc="Option to drop TD if TC comes out of timeout window"), - s.field( "mlt_max_td_length_ms", self.count, default=1000, doc="Maximum allowed time length [ms] for a readout window of a single TD"), + s.field( "mlt_merge_overlapping_tcs", types.flag, default=true, doc="Option to turn off merging of overlapping TCs when forming TDs in MLT"), + s.field( "mlt_buffer_timeout", types.count, default=100, doc="Timeout (buffer) to wait for new overlapping TCs before sending TD"), + s.field( "mlt_send_timed_out_tds", types.flag, default=true, doc="Option to drop TD if TC comes out of timeout window"), + s.field( "mlt_max_td_length_ms", types.count, default=1000, doc="Maximum allowed time length [ms] for a readout window of a single TD"), s.field( "mlt_ignore_tc", self.tc_types, default=[], doc="Optional list of TC types to be ignored in MLT"), - s.field( "mlt_use_readout_map", self.flag, default=false, doc="Option to use custom readout map in MLT"), + s.field( "mlt_use_readout_map", types.flag, default=false, doc="Option to use custom readout map in MLT"), s.field( "mlt_td_readout_map", self.tc_readout_map, default=self.tc_readout_map, doc="The readout windows assigned to TDs in MLT, based on TC type."), - s.field( "use_custom_maker", self.flag, default=false, doc="Option to use a Custom Trigger Candidate Maker (plugin)"), + s.field( "use_custom_maker", types.flag, default=false, doc="Option to use a Custom Trigger Candidate Maker (plugin)"), s.field( "ctcm_trigger_types", self.tc_types, default=[4], doc="Optional list of TC types to be used by the Custom Trigger Candidate Maker (plugin)"), s.field( "ctcm_trigger_intervals", self.tc_intervals, default=[10000000], doc="Optional list of intervals (clock ticks) for the TC types to be used by the Custom Trigger Candidate Maker (plugin)"), ]), }; -moo.oschema.sort_select(cs) +stypes + moo.oschema.sort_select(cs) diff --git a/schema/daqconf/types.jsonnet b/schema/daqconf/types.jsonnet new file mode 100644 index 00000000..6c7bb048 --- /dev/null +++ b/schema/daqconf/types.jsonnet @@ -0,0 +1,27 @@ +// This is the configuration schema for daqconf_multiru_gen +// + +local moo = import "moo.jsonnet"; + +local s = moo.oschema.schema("dunedaq.daqconf.types"); +local nc = moo.oschema.numeric_constraints; +// A temporary schema construction context. + +local cs = { + port: s.number( "port", "i4", doc="A TCP/IP port number"), + freq: s.number( "freq", "u4", doc="A frequency"), + rate: s.number( "rate", "f8", doc="A rate as a double"), + count: s.number( "count", "i8", doc="A count of things"), + flag: s.boolean( "flag", doc="Parameter that can be used to enable or disable functionality"), + path: s.string( "path", doc="Location on a filesystem"), + paths: s.sequence( "paths", self.path, doc="Multiple paths"), + string: s.string( "string", doc="Generic string"), + strings:s.sequence( "strings", self.string, doc="List of strings"), + host: s.string( "host", moo.re.dnshost, doc="A hostname"), + hosts: s.sequence( "hosts", self.host, doc="A collection of host names"), + ipv4: s.string( "ipv4", pattern=moo.re.ipv4, doc="ipv4 string"), + mac: s.string( "mac", pattern="^[a-fA-F0-9]{2}(:[a-fA-F0-9]{2}){5}$", doc="mac string"), +}; + +// Output a topologically sorted array. +moo.oschema.sort_select(cs) diff --git a/test/scripts/check_np04_configs.py b/test/scripts/daqconf_check_np04_configs.py similarity index 100% rename from test/scripts/check_np04_configs.py rename to test/scripts/daqconf_check_np04_configs.py diff --git a/test/scripts/daqconf_check_schema.py b/test/scripts/daqconf_check_schema.py new file mode 100755 index 00000000..29432b7f --- /dev/null +++ b/test/scripts/daqconf_check_schema.py @@ -0,0 +1,17 @@ +#!/usr/bin/env python + +import click +from rich import print + +from dunedaq.env import get_moo_model_path +import moo.io + +@click.command() +@click.argument("schema_name") +def cli(schema_name): + moo.io.default_load_path = get_moo_model_path() + x = moo.otypes.load_types(schema_name) + print(x) + +if __name__ == '__main__': + cli() From 7acdaa428862a3c0398948dfa8512725304a779b Mon Sep 17 00:00:00 2001 From: Pierre Lasorak Date: Tue, 27 Jun 2023 10:11:18 +0200 Subject: [PATCH 41/90] add a quit button and change the titles a bit --- scripts/daqconf_viewer | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/scripts/daqconf_viewer b/scripts/daqconf_viewer index 8e0a2f1a..e02e513c 100755 --- a/scripts/daqconf_viewer +++ b/scripts/daqconf_viewer @@ -44,7 +44,7 @@ class Configs(Static): self.set_interval(0.1, self.update_configs) def compose(self) -> ComposeResult: - yield TitleBox('Configuration List') + yield TitleBox('Configurations') yield ListView(LabelItem("This shouldn't be visible!")) async def update_configs(self) -> None: @@ -96,7 +96,7 @@ class Versions(Vertical): self.current_conf = None def compose(self) -> ComposeResult: - yield TitleBox('Available Versions') + yield TitleBox(f'Configuration versions') yield Horizontal(id='buttonbox') def on_mount(self) -> None: @@ -156,7 +156,7 @@ class Display(Vertical): self.version = None def compose(self) -> ComposeResult: - yield TitleBox('Configuration Data') + yield TitleBox('Configuration data') yield Tree("", id='conftree') async def get_json(self, conf, ver) -> None: @@ -297,7 +297,10 @@ class DiffScreen(Screen): class ConfViewer(App): CSS_PATH = "daqconf_viewer.css" - BINDINGS = [("d", "make_diff()", "Diff")] + BINDINGS = [ + ("d", "make_diff", "Diff"), + ("q", "quit", "Quit"), + ] def __init__(self, host, port, **kwargs): super().__init__(**kwargs) From c47e90a44b7046efa725bd4aa317e8383ee7a24e Mon Sep 17 00:00:00 2001 From: Alessandro Thea Date: Tue, 27 Jun 2023 10:50:23 +0200 Subject: [PATCH 42/90] confgen types: adding basic types --- schema/daqconf/types.jsonnet | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/schema/daqconf/types.jsonnet b/schema/daqconf/types.jsonnet index 6c7bb048..74c8ecf6 100644 --- a/schema/daqconf/types.jsonnet +++ b/schema/daqconf/types.jsonnet @@ -8,6 +8,13 @@ local nc = moo.oschema.numeric_constraints; // A temporary schema construction context. local cs = { + int4 : s.number( "int4", "i4", doc="A signed integer of 4 bytes"), + uint4 : s.number( "uint4", "u4", doc="An unsigned integer of 4 bytes"), + int8 : s.number( "int8", "i8", doc="A signed integer of 8 bytes"), + uint8 : s.number( "uint8", "u8", doc="An unsigned integer of 8 bytes"), + float4 : s.number( "float4", "f4", doc="A float of 4 bytes"), + double8 : s.number( "double8", "f8", doc="A double of 8 bytes"), + port: s.number( "port", "i4", doc="A TCP/IP port number"), freq: s.number( "freq", "u4", doc="A frequency"), rate: s.number( "rate", "f8", doc="A rate as a double"), From fce916cda3865f962b188a1d92038c35c9522d67 Mon Sep 17 00:00:00 2001 From: Alessandro Thea Date: Tue, 27 Jun 2023 16:33:45 +0200 Subject: [PATCH 43/90] Adding hsi random_trigger_Rate parameter --- config/daqconf_full_config.json | 2 +- python/daqconf/apps/fake_hsi_gen.py | 16 +-- python/daqconf/apps/hsi_gen.py | 32 ++---- python/daqconf/apps/readout_gen.py | 9 +- python/daqconf/apps/tprtc_gen.py | 2 - python/daqconf/apps/trigger_gen.py | 2 +- python/daqconf/core/assets.py | 5 +- python/daqconf/core/conf_utils.py | 4 +- python/daqconf/core/config_file.py | 113 ++++++++++++---------- python/daqconf/core/fragment_producers.py | 5 - python/daqconf/core/metadata.py | 3 +- python/daqconf/core/sourceid.py | 3 +- schema/daqconf/detectorgen.jsonnet | 4 +- schema/daqconf/hsigen.jsonnet | 1 + schema/daqconf/triggergen.jsonnet | 2 +- scripts/daqconf_multiru_gen | 5 +- 16 files changed, 92 insertions(+), 116 deletions(-) diff --git a/config/daqconf_full_config.json b/config/daqconf_full_config.json index 9f30f880..678b948b 100644 --- a/config/daqconf_full_config.json +++ b/config/daqconf_full_config.json @@ -69,7 +69,7 @@ "detector": { "clock_speed_hz": 62500000, "op_env": "swtest", - "tpg_channel_map": "PD2HDChannelMap" + "tpc_channel_map": "PD2HDChannelMap" }, "dpdk_sender": { "eal_args": "-l 0-1 -n 3 -- -m [0:1].0 -j", diff --git a/python/daqconf/apps/fake_hsi_gen.py b/python/daqconf/apps/fake_hsi_gen.py index d105da1f..287d359f 100644 --- a/python/daqconf/apps/fake_hsi_gen.py +++ b/python/daqconf/apps/fake_hsi_gen.py @@ -57,32 +57,20 @@ def get_fake_hsi_app( CLOCK_SPEED_HZ = detector.clock_speed_hz DATA_RATE_SLOWDOWN_FACTOR = daq_common.data_rate_slowdown_factor - # TRIGGER_RATE_HZ = trigger.trigger_rate_hz HSI_SOURCE_ID=source_id + RANDOM_TRIGGER_RATE_HZ = hsi.random_trigger_rate_hz MEAN_SIGNAL_MULTIPLICITY = hsi.mean_hsi_signal_multiplicity SIGNAL_EMULATION_MODE = hsi.hsi_signal_emulation_mode ENABLED_SIGNALS = hsi.enabled_hsi_signals HOST=hsi.host_fake_hsi - TRIGGER_RATE_HZ: int=1 - - # region_id=0 - # element_id=0 - - # trigger_interval_ticks = 0 - # if TRIGGER_RATE_HZ > 0: - # trigger_interval_ticks = math.floor((1 / TRIGGER_RATE_HZ) * CLOCK_SPEED_HZ / DATA_RATE_SLOWDOWN_FACTOR) - - # startpars = rccmd.StartParams(run=RUN_NUMBER, trigger_rate = TRIGGER_RATE_HZ) - modules = [DAQModule(name = 'fhsig', plugin = "FakeHSIEventGenerator", conf = fhsig.Conf(clock_frequency=CLOCK_SPEED_HZ/DATA_RATE_SLOWDOWN_FACTOR, - trigger_rate=TRIGGER_RATE_HZ, + trigger_rate=RANDOM_TRIGGER_RATE_HZ, mean_signal_multiplicity=MEAN_SIGNAL_MULTIPLICITY, signal_emulation_mode=SIGNAL_EMULATION_MODE, enabled_signals=ENABLED_SIGNALS), - # extra_commands = {"start": startpars} )] diff --git a/python/daqconf/apps/hsi_gen.py b/python/daqconf/apps/hsi_gen.py index 8a6deb1b..8fb15a2c 100644 --- a/python/daqconf/apps/hsi_gen.py +++ b/python/daqconf/apps/hsi_gen.py @@ -11,9 +11,8 @@ # fragments are provided by the FakeDataProd module from dfmodules import math -from rich.console import Console -console = Console() - +# from rich.console import Console +from ..core.console import console # Set moo schema search path from dunedaq.env import get_moo_model_path import moo.io @@ -21,12 +20,12 @@ # Load configuration types import moo.otypes -moo.otypes.load_types('rcif/cmd.jsonnet') +# moo.otypes.load_types('rcif/cmd.jsonnet') moo.otypes.load_types('hsilibs/hsireadout.jsonnet') moo.otypes.load_types('hsilibs/hsicontroller.jsonnet') moo.otypes.load_types('readoutlibs/readoutconfig.jsonnet') -import dunedaq.rcif.cmd as rccmd # AddressedCmd, +# import dunedaq.rcif.cmd as rccmd # AddressedCmd, import dunedaq.hsilibs.hsireadout as hsir import dunedaq.hsilibs.hsicontroller as hsic import dunedaq.readoutlibs.readoutconfig as rconf @@ -51,10 +50,10 @@ def get_timing_hsi_app( - # Temp vars + # Temp vars - remove CLOCK_SPEED_HZ = detector.clock_speed_hz - # TRIGGER_RATE_HZ = trigger.trigger_rate_hz DATA_RATE_SLOWDOWN_FACTOR = daq_common.data_rate_slowdown_factor + RANDOM_TRIGGER_RATE_HZ = hsi.random_trigger_rate_hz CONTROL_HSI_HARDWARE=hsi.control_hsi_hw CONNECTIONS_FILE=hsi.hsi_hw_connections_file READOUT_PERIOD_US = hsi.hsi_readout_period @@ -70,11 +69,6 @@ def get_timing_hsi_app( TIMING_SESSION=timing_session_name HOST=hsi.host_timing_hsi - - # (Useless) constant - TRIGGER_RATE_HZ: int = 1, - - modules = {} ## TODO all the connections... @@ -85,9 +79,6 @@ def get_timing_hsi_app( hsi_device_name=HSI_DEVICE_NAME, uhal_log_level=UHAL_LOG_LEVEL))] - region_id=0 - element_id=0 - modules += [DAQModule(name = f"hsi_datahandler", plugin = "HSIDataLinkHandler", conf = rconf.Conf(readoutmodelconf = rconf.ReadoutModelConf(source_queue_timeout_ms = QUEUE_POP_WAIT_MS, @@ -107,15 +98,6 @@ def get_timing_hsi_app( enable_raw_recording = False) ))] - # trigger_interval_ticks=0 - # if TRIGGER_RATE_HZ > 0: - # trigger_interval_ticks=math.floor((1/TRIGGER_RATE_HZ) * CLOCK_SPEED_HZ) - # elif CONTROL_HSI_HARDWARE: - # console.log('WARNING! Emulated trigger rate of 0 will not disable signal emulation in real HSI hardware! To disable emulated HSI triggers, use option: "--hsi-source 0" or mask all signal bits', style="bold red") - - # startpars = rccmd.StartParams(run=RUN_NUMBER, trigger_rate = TRIGGER_RATE_HZ) - # resumepars = rccmd.ResumeParams(trigger_interval_ticks = trigger_interval_ticks) - if CONTROL_HSI_HARDWARE: modules.extend( [ DAQModule(name="hsic", @@ -124,7 +106,7 @@ def get_timing_hsi_app( hardware_state_recovery_enabled=HARDWARE_STATE_RECOVERY_ENABLED, timing_session_name=TIMING_SESSION, clock_frequency=CLOCK_SPEED_HZ, - trigger_rate=TRIGGER_RATE_HZ, + trigger_rate=RANDOM_TRIGGER_RATE_HZ, address=HSI_ENDPOINT_ADDRESS, partition=HSI_ENDPOINT_PARTITION, rising_edge_mask=HSI_RE_MASK, diff --git a/python/daqconf/apps/readout_gen.py b/python/daqconf/apps/readout_gen.py index 4c687ab5..0615c146 100644 --- a/python/daqconf/apps/readout_gen.py +++ b/python/daqconf/apps/readout_gen.py @@ -1,9 +1,4 @@ # Set moo schema search path -from rich.console import Console - -console = Console() - - from dunedaq.env import get_moo_model_path import moo.io moo.io.default_load_path = get_moo_model_path() @@ -864,7 +859,7 @@ def generate( if TPG_ENABLED: dlhs_mods = self.add_tp_processing( dlh_list=dlhs_mods, - TPG_CHANNEL_MAP=self.det_cfg.tpg_channel_map, + TPG_CHANNEL_MAP=self.det_cfg.tpc_channel_map, ) modules += dlhs_mods @@ -895,7 +890,7 @@ def generate( ) if TPG_ENABLED: - # Add endpoints and frame producers to TP data handlers + # Add endpoints and frame producers to TP data handlers self.add_tpg_eps_and_fps( mgraph=mgraph, tpg_dlh_list=tpg_mods, diff --git a/python/daqconf/apps/tprtc_gen.py b/python/daqconf/apps/tprtc_gen.py index 233a2a3b..a6639562 100644 --- a/python/daqconf/apps/tprtc_gen.py +++ b/python/daqconf/apps/tprtc_gen.py @@ -12,8 +12,6 @@ from distutils.command.check import check import math -from rich.console import Console -console = Console() # Set moo schema search path from dunedaq.env import get_moo_model_path diff --git a/python/daqconf/apps/trigger_gen.py b/python/daqconf/apps/trigger_gen.py index 90b58698..1d6aa9f2 100644 --- a/python/daqconf/apps/trigger_gen.py +++ b/python/daqconf/apps/trigger_gen.py @@ -108,7 +108,7 @@ def get_trigger_app( USE_CUSTOM_MAKER = trigger.use_custom_maker CTCM_TYPES = trigger.ctcm_trigger_types CTCM_INTERVAL = trigger.ctcm_trigger_intervals - CHANNEL_MAP_NAME = detector.tpg_channel_map + CHANNEL_MAP_NAME = detector.tpc_channel_map DATA_REQUEST_TIMEOUT=trigger_data_request_timeout HOST=trigger.host_trigger diff --git a/python/daqconf/core/assets.py b/python/daqconf/core/assets.py index cac11a2c..add083fe 100755 --- a/python/daqconf/core/assets.py +++ b/python/daqconf/core/assets.py @@ -1,7 +1,8 @@ from os.path import exists,abspath,dirname -from rich.console import Console -console = Console() + +from .console import console + from daq_assettools.asset_file import AssetFile from daq_assettools.asset_database import Database diff --git a/python/daqconf/core/conf_utils.py b/python/daqconf/core/conf_utils.py index 2dd4f989..b141ee3f 100644 --- a/python/daqconf/core/conf_utils.py +++ b/python/daqconf/core/conf_utils.py @@ -5,11 +5,9 @@ import urllib from pathlib import Path -from rich.console import Console from copy import deepcopy from collections import namedtuple, defaultdict import json -import os from enum import Enum from typing import Callable from graphviz import Digraph @@ -29,7 +27,7 @@ from daqconf.core.daqmodule import DAQModule -console = Console() +from .console import console ######################################################################## # diff --git a/python/daqconf/core/config_file.py b/python/daqconf/core/config_file.py index 3866e18a..12e8ffc8 100755 --- a/python/daqconf/core/config_file.py +++ b/python/daqconf/core/config_file.py @@ -2,14 +2,13 @@ import math import sys import glob -from rich.console import Console +# from rich.console import Console from collections import defaultdict from os.path import exists, join import json -import string from pathlib import Path +from . console import console -console = Console() # Set moo schema search path from dunedaq.env import get_moo_model_path import moo.io @@ -20,6 +19,48 @@ import moo.otypes import moo.oschema + +class ConfigSet: + + + def get(self,conf): + if conf in self.confs: + return self.confs[conf] + else: + myconf = self.base_config + for pname, pval in self.full_input[conf]: + myconf[pname] = pval + return myconf + + def create_all_configs(self): + for cname, pars in self.full_input.items(): + if(cname==self.base_name): continue + self.confs[cname] = dict(self.base_config) + for pname, pval in pars.items(): + self.confs[cname][pname] = pval + + def get_all_configs(self): + return self.confs + + def list_all_configs(self): + print(self.confs.keys()) + + def __init__(self,conf_file,base_name='common'): + + self.base_name = base_name + with open(conf_file,"r+") as f: + self.full_input = json.load(f) + + try: + self.base_config = self.full_input[self.base_name] + except KeyError as e: + print(f"No '{self.base_name}' config in {conf_file}.") + raise e + + self.confs = {self.base_name: self.base_config} + self.create_all_configs() + + def _strict_recursive_update(dico1, dico2): for k, v in dico2.items(): if not k in dico1: @@ -37,10 +78,10 @@ def _strict_recursive_update(dico1, dico2): dico1[k] = v return dico1 + def parse_json(filename, schemed_object): console.log(f"Parsing config json file {filename}") - filepath = Path(filename) # basepath = filepath.parent @@ -56,7 +97,9 @@ def parse_json(filename, schemed_object): for k in new_parameters: # look for keys that are associated to dicts in the schemed_obj but here are strings v = new_parameters[k] + if isinstance(v,str) and k in subkeys: + # It's a string! It's a reference! Try loading it subfile_path = Path(os.path.expandvars(v)).expanduser() if not subfile_path.is_absolute(): @@ -71,7 +114,24 @@ def parse_json(filename, schemed_object): except Exception as e: raise RuntimeError(f"Couldn't parse {subfile_path}, error: {str(e)}") new_parameters[k] = new_subpars + + elif '' in v: + cname = k + pars = v[''] + scname = pars['config_name'] + scfile = pars['config_file'] if 'config_file' in pars else f'{cname}_configs.json' + scbase = pars['config_base'] if 'config_base' in pars else 'common' + + + scfile = Path(os.path.expandvars(scfile)).expanduser() + if not scfile.is_absolute(): + scfile = filepath.parent / scfile + + if not scfile.exists(): + raise RuntimeError(f'Cannot find the file {v} ({scfile})') + scset = ConfigSet(scfile,scbase) + new_parameters[k] = scset.get(scname) try: # Validate the heck out of this but that doesn't change the object itself (ARG) @@ -84,51 +144,6 @@ def parse_json(filename, schemed_object): return schemed_object -# def _recursive_section(sections, data): -# if len(sections) == 1: -# d = data -# for k,v in d.items(): -# if v == "true" or v == "True": -# d[k] = True -# if v == "false" or v == "False": -# d[k] = False -# return {sections[0]: d} -# else: -# return {sections[0]: _recursive_section(sections[1:], data)} - -# def parse_ini(filename, schemed_object): -# console.log(f"Parsing config ini file {filename}") - -# import configparser -# config = configparser.ConfigParser() -# try: -# config.read(filename) -# except Exception as e: -# raise RuntimeError(f"Couldn't parse {filename}, error: {str(e)}") - -# config_dict = {} - -# for sect in config.sections(): -# sections = sect.split('.') -# data = {k:v for k,v in config.items(sect)} -# if sections[0] in config_dict: -# config_dict[sections[0]].update(_recursive_section(sections, data)[sections[0]]) -# else: -# config_dict[sections[0]] = _recursive_section(sections, data)[sections[0]] - -# try: -# new_parameters = config_dict -# # validate the heck out of this but that doesn't change the object itself (ARG) -# _strict_recursive_update(schemed_object.pod(), new_parameters) -# # now its validated, update the object with moo -# schemed_object.update(new_parameters) -# return schemed_object -# except Exception as e: -# raise RuntimeError(f'Couldn\'t update the object {schemed_object} with the file {filename},\nError: {e}') - - - - def parse_config_file(filename, configurer_conf): from os.path import exists, splitext diff --git a/python/daqconf/core/fragment_producers.py b/python/daqconf/core/fragment_producers.py index 17b1abf0..fad66286 100644 --- a/python/daqconf/core/fragment_producers.py +++ b/python/daqconf/core/fragment_producers.py @@ -3,10 +3,7 @@ import moo.io moo.io.default_load_path = get_moo_model_path() -from rich.console import Console - import moo.otypes -import re moo.otypes.load_types('trigger/moduleleveltrigger.jsonnet') moo.otypes.load_types('dfmodules/triggerrecordbuilder.jsonnet') @@ -17,8 +14,6 @@ from daqconf.core.conf_utils import Direction from daqconf.core.sourceid import source_id_raw_str, ensure_subsystem_string -console = Console() - def set_mlt_links(the_system, mlt_app_name="trigger", verbose=False): """ The MLT needs to know the full list of fragment producers in the diff --git a/python/daqconf/core/metadata.py b/python/daqconf/core/metadata.py index 1f58e320..310e88ea 100755 --- a/python/daqconf/core/metadata.py +++ b/python/daqconf/core/metadata.py @@ -1,10 +1,9 @@ import json import os import sys -from rich.console import Console from os.path import exists, join -console = Console() +from rich.console import Console def write_metadata_file(json_dir, generator, config_file): console.log("Generating metadata file") diff --git a/python/daqconf/core/sourceid.py b/python/daqconf/core/sourceid.py index 4bee3bfd..ccd49a76 100644 --- a/python/daqconf/core/sourceid.py +++ b/python/daqconf/core/sourceid.py @@ -6,7 +6,8 @@ from enum import Enum from collections import namedtuple, defaultdict -console = Console() +from .console import console + from daqdataformats import SourceID from detchannelmaps import * diff --git a/schema/daqconf/detectorgen.jsonnet b/schema/daqconf/detectorgen.jsonnet index 8476047a..f63d0e6f 100644 --- a/schema/daqconf/detectorgen.jsonnet +++ b/schema/daqconf/detectorgen.jsonnet @@ -11,12 +11,12 @@ local nc = moo.oschema.numeric_constraints; // A temporary schema construction context. local cs = { - tpg_channel_map: s.enum( "TPGChannelMap", ["VDColdboxChannelMap", "ProtoDUNESP1ChannelMap", "PD2HDChannelMap", "HDColdboxChannelMap"]), + tpc_channel_map: s.enum("TPCChannelMap", ["VDColdboxChannelMap", "ProtoDUNESP1ChannelMap", "PD2HDChannelMap", "HDColdboxChannelMap"]), detector : s.record("detector", [ s.field( "op_env", types.string, default='swtest', doc="Operational environment - used for HDF5 Attribute inside the files"), s.field( "clock_speed_hz", types.freq, default=62500000), - s.field( "tpg_channel_map", self.tpg_channel_map, default="PD2HDChannelMap", doc="Channel map for TPG"), + s.field( "tpc_channel_map", self.tpc_channel_map, default="PD2HDChannelMap", doc="Channel map for TPG"), ], doc="Global common settings"), diff --git a/schema/daqconf/hsigen.jsonnet b/schema/daqconf/hsigen.jsonnet index a9ffccbe..3e851747 100644 --- a/schema/daqconf/hsigen.jsonnet +++ b/schema/daqconf/hsigen.jsonnet @@ -12,6 +12,7 @@ local nc = moo.oschema.numeric_constraints; local cs = { hsi: s.record("hsi", [ + s.field( "random_trigger_rate_hz", types.rate, default=1.0, doc='Fake HSI only: rate at which fake HSIEvents are sent. 0 - disable HSIEvent generation. Former -t'), # timing hsi options s.field( "use_timing_hsi", types.flag, default=false, doc='Flag to control whether real hardware timing HSI config is generated. Default is false'), s.field( "host_timing_hsi", types.host, default='localhost', doc='Host to run the HSI app on'), diff --git a/schema/daqconf/triggergen.jsonnet b/schema/daqconf/triggergen.jsonnet index 042d9c1e..37ec0807 100644 --- a/schema/daqconf/triggergen.jsonnet +++ b/schema/daqconf/triggergen.jsonnet @@ -94,7 +94,7 @@ local cs = { ]), trigger: s.record("trigger",[ - s.field( "trigger_rate_hz", types.rate, default=1.0, doc='Fake HSI only: rate at which fake HSIEvents are sent. 0 - disable HSIEvent generation. Former -t'), + // s.field( "trigger_rate_hz", types.rate, default=1.0, doc='Fake HSI only: rate at which fake HSIEvents are sent. 0 - disable HSIEvent generation. Former -t'), s.field( "trigger_window_before_ticks",types.count, default=1000, doc="Trigger window before marker. Former -b"), s.field( "trigger_window_after_ticks", types.count, default=1000, doc="Trigger window after marker. Former -a"), s.field( "host_trigger", types.host, default='localhost', doc='Host to run the trigger app on'), diff --git a/scripts/daqconf_multiru_gen b/scripts/daqconf_multiru_gen index 11a38634..efe44fc5 100755 --- a/scripts/daqconf_multiru_gen +++ b/scripts/daqconf_multiru_gen @@ -7,7 +7,8 @@ from rich.console import Console from os.path import exists, abspath, dirname, basename from pathlib import Path -console = Console() +from daqconf.core.console import console + # console.log("daqconf - loading base modules") from daqconf.core.system import System @@ -775,6 +776,8 @@ def cli( config_file.name if config_file else "default.json", confgen.daqconf_multiru_gen( # :facepalm: boot = boot, + detector = detector, + daq_common = daq_common, dataflow = dataflow, dqm = dqm, hsi = hsi, From 5a659b26e77e429162b1a03b1eeec48646a904fb Mon Sep 17 00:00:00 2001 From: Alessandro Thea Date: Tue, 27 Jun 2023 17:16:17 +0200 Subject: [PATCH 44/90] Improved romap schema description --- schema/daqconf/detreadoutmap.jsonnet | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/schema/daqconf/detreadoutmap.jsonnet b/schema/daqconf/detreadoutmap.jsonnet index 6af7f76d..23841fd6 100644 --- a/schema/daqconf/detreadoutmap.jsonnet +++ b/schema/daqconf/detreadoutmap.jsonnet @@ -36,15 +36,15 @@ local cs = { ], doc="A FELIX readout stream configuration"), eth_conf: s.record("EthStreamParameters", [ - s.field("protocol", self.eth_protocol, "udp", doc="Ethernet protocol"), + s.field("protocol", self.eth_protocol, "udp", doc="Ethernet protocol used. udp or zmq"), s.field("mode", self.mode, "fix_rate", doc="fix_rate, var_rate"), s.field("rx_iface", self.short, 0, doc="Reaout interface"), - s.field("rx_host", self.host, "localhost", doc="Reaout hostname"), - s.field("rx_mac", self.mac, "00:00:00:00:00:00", doc="Reaout Destination MAC"), - s.field("rx_ip", self.ipv4, "0.0.0.0", doc="Reaout Destination IP"), - s.field("tx_host", self.host, "localhost", doc="Transmitter hostname"), - s.field("tx_mac", self.mac, "00:00:00:00:00:00", doc="Reaout Source MAC"), - s.field("tx_ip", self.ipv4, "0.0.0.0", doc="Reaout Source IP"), + s.field("rx_host", self.host, "localhost", doc="Readout hostname"), + s.field("rx_mac", self.mac, "00:00:00:00:00:00", doc="Destination MAC on readout host"), + s.field("rx_ip", self.ipv4, "0.0.0.0", doc="Destination IP on readout host"), + s.field("tx_host", self.host, "localhost", doc="Transmitter control host"), + s.field("tx_mac", self.mac, "00:00:00:00:00:00", doc="Transmitter MAC"), + s.field("tx_ip", self.ipv4, "0.0.0.0", doc="Transmitter IP"), ], doc="A Ethernet readout stream configuration"), From ab4248606a104538047174eb051a78e881d82753 Mon Sep 17 00:00:00 2001 From: Alessandro Thea Date: Tue, 27 Jun 2023 17:18:22 +0200 Subject: [PATCH 45/90] adding missing console module --- python/daqconf/core/console.py | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 python/daqconf/core/console.py diff --git a/python/daqconf/core/console.py b/python/daqconf/core/console.py new file mode 100644 index 00000000..745d8c8b --- /dev/null +++ b/python/daqconf/core/console.py @@ -0,0 +1,2 @@ +from rich.console import Console +console = Console() \ No newline at end of file From 5f02b42eb1f783da2665ae89dac0d409c98add07 Mon Sep 17 00:00:00 2001 From: Michal Rigan Date: Tue, 27 Jun 2023 10:24:05 -0500 Subject: [PATCH 46/90] adding related options to full config json --- config/daqconf_full_config.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/config/daqconf_full_config.json b/config/daqconf_full_config.json index 68648d1e..5b74a4cc 100644 --- a/config/daqconf_full_config.json +++ b/config/daqconf_full_config.json @@ -141,6 +141,8 @@ "mlt_send_timed_out_tds": true, "mlt_max_td_length_ms": 1000, "mlt_ignore_tc": [], + "mlt_use_bitwords": false, + "mlt_trigger_bitwords": [], "use_custom_maker": false, "ctcm_trigger_types": [4], "ctcm_trigger_intervals": [62500000], From fc87e01a177eba1c515c88fab9360d1bc34d59f9 Mon Sep 17 00:00:00 2001 From: Michal Rigan Date: Tue, 27 Jun 2023 10:28:07 -0500 Subject: [PATCH 47/90] removed dev printouts --- python/daqconf/apps/trigger_gen.py | 1 - 1 file changed, 1 deletion(-) diff --git a/python/daqconf/apps/trigger_gen.py b/python/daqconf/apps/trigger_gen.py index 46a86ed7..7c8e069f 100644 --- a/python/daqconf/apps/trigger_gen.py +++ b/python/daqconf/apps/trigger_gen.py @@ -312,7 +312,6 @@ def get_trigger_app(CLOCK_SPEED_HZ: int = 62_500_000, ### get trigger bitwords for mlt MLT_TRIGGER_FLAGS = get_trigger_bitwords(MLT_TRIGGER_BITWORDS) - print(MLT_TRIGGER_FLAGS) # We need to populate the list of links based on the fragment # producers available in the system. This is a bit of a From 59f24a7e11a55c5c38c718f5d74fa76d3ee68d99 Mon Sep 17 00:00:00 2001 From: Alessandro Thea Date: Tue, 27 Jun 2023 17:45:29 +0200 Subject: [PATCH 48/90] import fixed --- python/daqconf/core/metadata.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/python/daqconf/core/metadata.py b/python/daqconf/core/metadata.py index 310e88ea..20c54693 100755 --- a/python/daqconf/core/metadata.py +++ b/python/daqconf/core/metadata.py @@ -3,12 +3,11 @@ import sys from os.path import exists, join -from rich.console import Console +from .console import console def write_metadata_file(json_dir, generator, config_file): console.log("Generating metadata file") - # Backwards compatibility if isinstance(json_dir, str): from pathlib import Path From bf2c66cc20a40fd3d66391f98ea94dde7268bbf0 Mon Sep 17 00:00:00 2001 From: Kurt Biery Date: Tue, 27 Jun 2023 16:00:50 -0500 Subject: [PATCH 49/90] Added daq_common and timing_session_name arguments to the get_timing_hsi_app call in daqconf_multiru_gen to get things working... --- scripts/daqconf_multiru_gen | 2 ++ 1 file changed, 2 insertions(+) diff --git a/scripts/daqconf_multiru_gen b/scripts/daqconf_multiru_gen index efe44fc5..2f39f23f 100755 --- a/scripts/daqconf_multiru_gen +++ b/scripts/daqconf_multiru_gen @@ -430,6 +430,8 @@ def cli( hsi = hsi, detector = detector, source_id = timing_hsi_source_id, + daq_common = daq_common, + timing_session_name = timing.timing_session_name, DEBUG=debug ) if debug: console.log("timing hsi cmd data:", the_system.apps["timinghsi"]) From 78c10d9cc252b18a78629dbd7f21d8db0443be79 Mon Sep 17 00:00:00 2001 From: Alessandro Thea Date: Wed, 28 Jun 2023 14:00:15 +0200 Subject: [PATCH 50/90] adding a missing console import --- python/daqconf/core/fragment_producers.py | 1 + schema/daqconf/confgen.jsonnet | 62 ++++++++++++----------- 2 files changed, 34 insertions(+), 29 deletions(-) diff --git a/python/daqconf/core/fragment_producers.py b/python/daqconf/core/fragment_producers.py index fad66286..2fc99ce9 100644 --- a/python/daqconf/core/fragment_producers.py +++ b/python/daqconf/core/fragment_producers.py @@ -13,6 +13,7 @@ from daqconf.core.conf_utils import Direction from daqconf.core.sourceid import source_id_raw_str, ensure_subsystem_string +from .console import console def set_mlt_links(the_system, mlt_app_name="trigger", verbose=False): """ diff --git a/schema/daqconf/confgen.jsonnet b/schema/daqconf/confgen.jsonnet index 6e572372..f4ca4da1 100755 --- a/schema/daqconf/confgen.jsonnet +++ b/schema/daqconf/confgen.jsonnet @@ -3,6 +3,10 @@ local moo = import "moo.jsonnet"; + +local stypes = import "daqconf/types.jsonnet"; +local types = moo.oschema.hier(stypes).dunedaq.daqconf.types; + local sctb = import "ctbmodules/ctbmodule.jsonnet"; local ctbmodule = moo.oschema.hier(sctb).dunedaq.ctbmodules.ctbmodule; @@ -38,38 +42,38 @@ local nc = moo.oschema.numeric_constraints; // A temporary schema construction context. local cs = { - port: s.number( "Port", "i4", doc="A TCP/IP port number"), - freq: s.number( "Frequency", "u4", doc="A frequency"), - rate: s.number( "Rate", "f8", doc="A rate as a double"), - count: s.number( "count", "i8", doc="A count of things"), - three_choice: s.number( "threechoice", "i8", nc(minimum=0, exclusiveMaximum=3), doc="A choice between 0, 1, or 2"), - flag: s.boolean( "Flag", doc="Parameter that can be used to enable or disable functionality"), - monitoring_dest: s.enum( "MonitoringDest", ["local", "cern", "pocket"]), - path: s.string( "Path", doc="Location on a filesystem"), - paths: s.sequence( "Paths", self.path, doc="Multiple paths"), - host: s.string( "Host", moo.re.dnshost, doc="A hostname"), - hosts: s.sequence( "Hosts", self.host, "Multiple hosts"), - string: s.string( "Str", doc="Generic string"), - strings: s.sequence( "Strings", self.string, doc="List of strings"), - - tpg_channel_map: s.enum( "TPGChannelMap", ["VDColdboxChannelMap", "ProtoDUNESP1ChannelMap", "PD2HDChannelMap", "HDColdboxChannelMap"]), - dqm_channel_map: s.enum( "DQMChannelMap", ['HD', 'VD', 'PD2HD', 'HDCB']), - dqm_params: s.sequence( "DQMParams", self.count, doc="Parameters for DQM (fixme)"), - tc_types: s.sequence( "TCTypes", self.count, doc="List of TC types"), - tc_type: s.number( "TCType", "i4", nc(minimum=0, maximum=9), doc="Number representing TC type. Currently ranging from 0 to 9"), - tc_interval: s.number( "TCInterval", "i8", nc(minimum=1, maximum=30000000000), doc="The intervals between TCs that are inserted into MLT by CTCM, in clock ticks"), - tc_intervals: s.sequence( "TCIntervals", self.tc_interval, doc="List of TC intervals used by CTCM"), - readout_time: s.number( "ROTime", "i8", doc="A readout time in ticks"), - channel_list: s.sequence( "ChannelList", self.count, doc="List of offline channels to be masked out from the TPHandler"), - tpg_algo_choice: s.enum( "TPGAlgoChoice", ["SimpleThreshold", "AbsRS"], doc="Trigger algorithm choice"), - pm_choice: s.enum( "PMChoice", ["k8s", "ssh"], doc="Process Manager choice: ssh or Kubernetes"), - rte_choice: s.enum( "RTEChoice", ["auto", "release", "devarea"], doc="Kubernetes DAQ application RTE choice"), + // port: s.number( "Port", "i4", doc="A TCP/IP port number"), + // freq: s.number( "Frequency", "u4", doc="A frequency"), + // rate: s.number( "Rate", "f8", doc="A rate as a double"), + // count: s.number( "count", "i8", doc="A count of things"), + // three_choice: s.number( "threechoice", "i8", nc(minimum=0, exclusiveMaximum=3), doc="A choice between 0, 1, or 2"), + // flag: s.boolean( "Flag", doc="Parameter that can be used to enable or disable functionality"), + // monitoring_dest: s.enum( "MonitoringDest", ["local", "cern", "pocket"]), + // path: s.string( "Path", doc="Location on a filesystem"), + // paths: s.sequence( "Paths", self.path, doc="Multiple paths"), + // host: s.string( "Host", moo.re.dnshost, doc="A hostname"), + // hosts: s.sequence( "Hosts", self.host, "Multiple hosts"), + // string: s.string( "Str", doc="Generic string"), + // strings: s.sequence( "Strings", self.string, doc="List of strings"), + + // tpg_channel_map: s.enum( "TPGChannelMap", ["VDColdboxChannelMap", "ProtoDUNESP1ChannelMap", "PD2HDChannelMap", "HDColdboxChannelMap"]), + // dqm_channel_map: s.enum( "DQMChannelMap", ['HD', 'VD', 'PD2HD', 'HDCB']), + // dqm_params: s.sequence( "DQMParams", self.count, doc="Parameters for DQM (fixme)"), + // tc_types: s.sequence( "TCTypes", self.count, doc="List of TC types"), + // tc_type: s.number( "TCType", "i4", nc(minimum=0, maximum=9), doc="Number representing TC type. Currently ranging from 0 to 9"), + // tc_interval: s.number( "TCInterval", "i8", nc(minimum=1, maximum=30000000000), doc="The intervals between TCs that are inserted into MLT by CTCM, in clock ticks"), + // tc_intervals: s.sequence( "TCIntervals", self.tc_interval, doc="List of TC intervals used by CTCM"), + // readout_time: s.number( "ROTime", "i8", doc="A readout time in ticks"), + // channel_list: s.sequence( "ChannelList", self.count, doc="List of offline channels to be masked out from the TPHandler"), + // tpg_algo_choice: s.enum( "TPGAlgoChoice", ["SimpleThreshold", "AbsRS"], doc="Trigger algorithm choice"), + // pm_choice: s.enum( "PMChoice", ["k8s", "ssh"], doc="Process Manager choice: ssh or Kubernetes"), + // rte_choice: s.enum( "RTEChoice", ["auto", "release", "devarea"], doc="Kubernetes DAQ application RTE choice"), ctb_hsi: s.record("ctb_hsi", [ # ctb options - s.field( "use_ctb_hsi", self.flag, default=false, doc='Flag to control whether CTB HSI config is generated. Default is false'), - s.field( "host_ctb_hsi", self.host, default='localhost', doc='Host to run the HSI app on'), + s.field( "use_ctb_hsi", types.flag, default=false, doc='Flag to control whether CTB HSI config is generated. Default is false'), + s.field( "host_ctb_hsi", types.host, default='localhost', doc='Host to run the HSI app on'), s.field( "hlt_triggers", ctbmodule.Hlt_trigger_seq, []), s.field( "beam_llt_triggers", ctbmodule.Llt_mask_trigger_seq, []), s.field( "crt_llt_triggers", ctbmodule.Llt_count_trigger_seq, []), @@ -96,4 +100,4 @@ local cs = { }; // Output a topologically sorted array. -sboot + sdetector + sdaqcommon + stiming + shsi + sreadout + strigger + sdataflow + sdqm + sctb + moo.oschema.sort_select(cs) +stypes + sboot + sdetector + sdaqcommon + stiming + shsi + sreadout + strigger + sdataflow + sdqm + sctb + moo.oschema.sort_select(cs) From d75e6584da8f12c555d88fcf66ff62ec57915c5a Mon Sep 17 00:00:00 2001 From: Alessandro Thea Date: Wed, 28 Jun 2023 16:16:55 +0200 Subject: [PATCH 51/90] typo fixed --- python/daqconf/apps/readout_gen.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/daqconf/apps/readout_gen.py b/python/daqconf/apps/readout_gen.py index 0615c146..dca139b0 100644 --- a/python/daqconf/apps/readout_gen.py +++ b/python/daqconf/apps/readout_gen.py @@ -914,7 +914,7 @@ def generate( cvmfs = Path('/cvmfs') ddf_path = Path(cfg.default_data_file) if not cvmfs in ddf_path.parents: - dir_name.add(ddf_path.parent) + dir_names.add(ddf_path.parent) for _,file in data_file_map: f = Path(file) From a88d88e64f4a7e157ceb88101d18e98bfc1e2235 Mon Sep 17 00:00:00 2001 From: Eric Flumerfelt Date: Wed, 28 Jun 2023 13:43:22 -0400 Subject: [PATCH 52/90] Add missing parameter for tpstream writer --- scripts/daqconf_multiru_gen | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/daqconf_multiru_gen b/scripts/daqconf_multiru_gen index 2f39f23f..45ba9fe3 100755 --- a/scripts/daqconf_multiru_gen +++ b/scripts/daqconf_multiru_gen @@ -624,6 +624,7 @@ def cli( app_name=tpw_name, file_label=file_label, source_id=dfidx, + SRC_GEO_ID_MAP=dro_map.get_src_geo_map(), DEBUG=debug ) From 5f112dcfff6bafb7e49a4ce9d35cf684f055f35c Mon Sep 17 00:00:00 2001 From: Alessandro Thea Date: Thu, 29 Jun 2023 16:43:22 +0200 Subject: [PATCH 53/90] Adding custom lcore configuration --- python/daqconf/apps/readout_gen.py | 120 +++++++++++++++++------------ schema/daqconf/readoutgen.jsonnet | 20 ++++- 2 files changed, 88 insertions(+), 52 deletions(-) diff --git a/python/daqconf/apps/readout_gen.py b/python/daqconf/apps/readout_gen.py index dca139b0..77c82525 100644 --- a/python/daqconf/apps/readout_gen.py +++ b/python/daqconf/apps/readout_gen.py @@ -122,15 +122,20 @@ def streams_by_host(self): return iface_map - def streams_by_iface(self): + def streams_by_rxiface(self): + """Group streams by interface + + Returns: + dict: A map of streams with the same destination ip, mac and host + """ iface_map = group_by_key(self.desc.streams, lambda s: (s.parameters.rx_ip, s.parameters.rx_mac, s.parameters.rx_host)) return iface_map - def streams_by_iface_and_tx_endpoint(self): + def streams_by_rxiface_and_tx_endpoint(self): - s_by_if = self.streams_by_iface() + s_by_if = self.streams_by_rxiface() m = {} for k,v in s_by_if.items(): m[k] = group_by_key(v, lambda s: (s.parameters.tx_ip, s.parameters.tx_mac, s.parameters.tx_host)) @@ -141,9 +146,10 @@ def streams_by_iface_and_tx_endpoint(self): # m = group_by_key(self.desc.streams, lambda s: (getattr(s.parameters, self.desc._host_label_map[s.kind]), getattr(s.parameters, self.desc._iflabel_map[s.kind]), s.kind, s.geo_id.det_id)) # return m - def build_conf(self, eal_arg_list, rxqueues_per_lcore): + def build_conf(self, eal_arg_list, lcores_id_set): - streams_by_if_and_tx = self.streams_by_iface_and_tx_endpoint() + + streams_by_if_and_tx = self.streams_by_rxiface_and_tx_endpoint() ifcfgs = [] for (rx_ip, rx_mac, _),txs in streams_by_if_and_tx.items(): @@ -166,7 +172,7 @@ def build_conf(self, eal_arg_list, rxqueues_per_lcore): nrc.Source( id=sid, # FIXME what is this ID? ip_addr=tx_ip, - lcore=(sid//rxqueues_per_lcore)+self.lcore_offset, + lcore=lcores_id_set[sid % len(lcores_id_set)], rx_q=sid, src_info=si, src_streams_mapping=ssm @@ -188,44 +194,44 @@ def build_conf(self, eal_arg_list, rxqueues_per_lcore): return conf - def build_conf_by_host(self, eal_arg_list): - - streams_by_if_and_tx = self.streams_by_host() - - ifcfgs = [] - for (rx_ip, rx_mac, _),txs in streams_by_if_and_tx.items(): - srcs = [] - # Sid is used for the "Source.id". What is it? - - for sid,((tx_ip,_,_),streams) in enumerate(txs.items()): - ssm = nrc.SrcStreamsMapping([ - nrc.StreamMap(source_id=s.src_id, stream_id=s.geo_id.stream_id) - for s in streams - ]) - geo_id = streams[0].geo_id - si = nrc.SrcGeoInfo( - det_id=geo_id.det_id, - crate_id=geo_id.crate_id, - slot_id=geo_id.slot_id - ) - - srcs.append( - nrc.Source( - id=sid, # FIXME what is this ID? - ip_addr=tx_ip, - lcore=sid+self.lcore_offset, - rx_q=sid, - src_info=si, - src_streams_mapping=ssm - ) - ) - ifcfgs.append( - nrc.Interface( - ip_addr=rx_ip, - mac_addr=rx_mac, - expected_sources=srcs - ) - ) + # def build_conf_by_host(self, eal_arg_list): + + # streams_by_if_and_tx = self.streams_by_host() + + # ifcfgs = [] + # for (rx_ip, rx_mac, _),txs in streams_by_if_and_tx.items(): + # srcs = [] + # # Sid is used for the "Source.id". What is it? + + # for sid,((tx_ip,_,_),streams) in enumerate(txs.items()): + # ssm = nrc.SrcStreamsMapping([ + # nrc.StreamMap(source_id=s.src_id, stream_id=s.geo_id.stream_id) + # for s in streams + # ]) + # geo_id = streams[0].geo_id + # si = nrc.SrcGeoInfo( + # det_id=geo_id.det_id, + # crate_id=geo_id.crate_id, + # slot_id=geo_id.slot_id + # ) + + # srcs.append( + # nrc.Source( + # id=sid, # FIXME what is this ID? + # ip_addr=tx_ip, + # lcore=sid+self.lcore_offset, + # rx_q=sid, + # src_info=si, + # src_streams_mapping=ssm + # ) + # ) + # ifcfgs.append( + # nrc.Interface( + # ip_addr=rx_ip, + # mac_addr=rx_mac, + # expected_sources=srcs + # ) + # ) conf = nrc.Conf( @@ -251,17 +257,22 @@ def __init__(self, readout_cfg, det_cfg, daq_cfg): self.det_cfg = det_cfg self.daq_cfg = daq_cfg - excpt = {} + numa_excpt = {} for ex in self.ro_cfg.numa_config['exceptions']: - excpt[(ex['host'], ex['card'])] = ex - self.excpt = excpt + numa_excpt[(ex['host'], ex['card'])] = ex + self.numa_excpt = numa_excpt + + lcores_excpt = {} + for ex in self.ro_cfg.dpdk_lcores_config['exceptions']: + lcores_excpt[(ex['host'], ex['iface'])] = ex + self.lcores_excpt = lcores_excpt def get_numa_cfg(self, RU_DESCRIPTOR): cfg = self.ro_cfg try: - ex = self.excpt[(RU_DESCRIPTOR.host_name, RU_DESCRIPTOR.iface)] + ex = self.numa_excpt[(RU_DESCRIPTOR.host_name, RU_DESCRIPTOR.iface)] numa_id = ex['numa_id'] latency_numa = ex['latency_buffer_numa_aware'] latency_preallocate = ex['latency_buffer_preallocation'] @@ -273,6 +284,15 @@ def get_numa_cfg(self, RU_DESCRIPTOR): flx_card_override = -1 return (numa_id, latency_numa, latency_preallocate, flx_card_override) + def get_lcore_config(self, RU_DESCRIPTOR): + cfg = self.ro_cfg + try: + ex = self.lcores_excpt[(RU_DESCRIPTOR.host_name, RU_DESCRIPTOR.iface)] + lcore_id_set = list(set(ex['lcore_id_set'])) + except KeyError: + lcore_id_set = list(set(cfg.dpdk_lcore_config['default_lcore_id_set'])) + return lcore_id_set + ### # Fake Card Reader creator @@ -428,12 +448,14 @@ def create_dpdk_cardreader( nic_reader_name = f"nic_reader_{RU_DESCRIPTOR.iface}" + lcores_id_set = self.get_lcore_config(RU_DESCRIPTOR) + modules = [DAQModule( name=nic_reader_name, plugin="NICReceiver", conf=eth_ru_bldr.build_conf( eal_arg_list=cfg.dpdk_eal_args, - rxqueues_per_lcore=cfg.dpdk_rxqueues_per_lcore + lcores_id_set=lcores_id_set ), )] diff --git a/schema/daqconf/readoutgen.jsonnet b/schema/daqconf/readoutgen.jsonnet index 11731b35..2ab79db5 100644 --- a/schema/daqconf/readoutgen.jsonnet +++ b/schema/daqconf/readoutgen.jsonnet @@ -11,7 +11,7 @@ local nc = moo.oschema.numeric_constraints; // A temporary schema construction context. local cs = { - channel_list: s.sequence( "ChannelList", types.count, doc="List of offline channels to be masked out from the TPHandler"), + id_list: s.sequence( "IDList", types.count, doc="List of Ids"), data_file_entry: s.record("data_file_entry", [ s.field( "data_file", types.path, default='./frames.bin', doc="File containing data frames to be replayed by the fake cards. Former -d. Uses the asset manager, can also be 'asset://checksum/somelonghash', or 'file://somewhere/frames.bin' or 'frames.bin'"), @@ -37,6 +37,18 @@ local cs = { s.field( "exceptions", self.numa_exceptions, default=[], doc="Exceptions to the default NUMA ID"), ]), + dpdk_lcore_exception: s.record( "DPDKLCoreException", [ + s.field( "host", types.host, default='localhost', doc="Host of exception"), + s.field( "iface", types.count, default=0, doc="Card ID of exception"), + s.field( "lcore_id_set", self.id_list, default=[], doc='List of IDs per core'), + ]), + dpdk_lcore_exceptions: s.sequence( "DPDKLCoreExceptions", self.dpdk_lcore_exception, doc="Exceptions to the default LCore config"), + + dpdk_lcore_config: s.record("DPDKLCoreConfig", [ + s.field( "default_lcore_id_set", self.id_list, default=[1,2,3,4], doc='List of IDs per core'), + s.field( "exceptions", self.dpdk_lcore_exceptions, default=[], doc="Exceptions to the default NUMA ID"), + ]), + readout: s.record("readout", [ s.field( "detector_readout_map_file", types.path, default='./DetectorReadoutMap.json', doc="File containing detector hardware map for configuration to run"), s.field( "use_fake_data_producers", types.flag, default=false, doc="Use fake data producers that respond with empty fragments immediately instead of (fake) cards and DLHs"), @@ -48,7 +60,9 @@ local cs = { s.field( "data_files", self.data_files, default=[], doc="Files to use by detector type"), // DPDK s.field( "dpdk_eal_args", types.string, default='-l 0-1 -n 3 -- -m [0:1].0 -j', doc='Args passed to the EAL in DPDK'), - s.field( "dpdk_rxqueues_per_lcore", types.count, default=1, doc='Number of rx queues per core'), + // s.field( "dpdk_rxqueues_per_lcore", types.count, default=1, doc='Number of rx queues per core'), + // s.field( "dpdk_lcore_id_set", self.id_list, default=1, doc='List of IDs per core'), + s.field( "dpdk_lcores_config", self.dpdk_lcore_config, default=self.dpdk_lcore_config, doc='Configuration of DPDK LCore IDs'), // FLX s.field( "numa_config", self.numa_config, default=self.numa_config, doc='Configuration of FELIX NUMA IDs'), // DLH @@ -60,7 +74,7 @@ local cs = { s.field( "enable_tpg", types.flag, default=false, doc="Enable TPG"), s.field( "tpg_threshold", types.count, default=120, doc="Select TPG threshold"), s.field( "tpg_algorithm", types.string, default="SimpleThreshold", doc="Select TPG algorithm (SimpleThreshold, AbsRS)"), - s.field( "tpg_channel_mask", self.channel_list, default=[], doc="List of offline channels to be masked out from the TPHandler"), + s.field( "tpg_channel_mask", self.id_list, default=[], doc="List of offline channels to be masked out from the TPHandler"), s.field( "enable_raw_recording", types.flag, default=false, doc="Add queues and modules necessary for the record command"), s.field( "raw_recording_output_dir", types.path, default='.', doc="Output directory where recorded data is written to. Data for each link is written to a separate file") ]), From 137262337b746571e7891db4539ffcdc701d77e5 Mon Sep 17 00:00:00 2001 From: Alessandro Thea Date: Thu, 29 Jun 2023 17:11:34 +0200 Subject: [PATCH 54/90] sorting lcore_ids fixed --- python/daqconf/apps/readout_gen.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/python/daqconf/apps/readout_gen.py b/python/daqconf/apps/readout_gen.py index 77c82525..24c3367c 100644 --- a/python/daqconf/apps/readout_gen.py +++ b/python/daqconf/apps/readout_gen.py @@ -288,10 +288,12 @@ def get_lcore_config(self, RU_DESCRIPTOR): cfg = self.ro_cfg try: ex = self.lcores_excpt[(RU_DESCRIPTOR.host_name, RU_DESCRIPTOR.iface)] - lcore_id_set = list(set(ex['lcore_id_set'])) + lcore_id_set = ex['lcore_id_set'] except KeyError: - lcore_id_set = list(set(cfg.dpdk_lcore_config['default_lcore_id_set'])) - return lcore_id_set + lcore_id_set = cfg.dpdk_lcore_config['default_lcore_id_set'] + + + return list(dict.fromkeys(lcore_id_set)) ### From 7c649ee3e1813eb8538351736844d6bcc2f491fb Mon Sep 17 00:00:00 2001 From: Jonathan Hancock <56547447+JonathanHancock0@users.noreply.github.com> Date: Fri, 30 Jun 2023 13:47:15 +0200 Subject: [PATCH 55/90] Create ConfigDatabase.md First draft of the documentation, will be updated later when more features are added to the config viewer. --- docs/ConfigDatabase.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 docs/ConfigDatabase.md diff --git a/docs/ConfigDatabase.md b/docs/ConfigDatabase.md new file mode 100644 index 00000000..81f8b712 --- /dev/null +++ b/docs/ConfigDatabase.md @@ -0,0 +1,19 @@ +# Interacting with the Configuration Database + +## Uploading a config +To use configuration files with a nanorc instance run on Kubernetes, it first needs to be uploaded to the MongoDB running in the cluster. +To do this, simply run `upload-conf ` + +Keep in mind that the config directory can contain underscores, but the name it will be given in the database cannot (hyphens are fine). + +## Viewing configurations +To inspect the contents of the database, run `daqconf_viewer` after setting up the software environment. This will open a graphical UI in the terminal. +![Config Viewer](ConfViewerSC.png) +A list of all configuration names is show on the very left. +Clicking one of them will generate a list of buttons at the top corresponding to the saved versions of the config. +Press one of those buttons to bring up the config file the the dsplay. By clicking the arrows, the contents of each sub-schema can be expanded. + +Pressing the D key with the config open will take you to a very similar screen, albeit with green lines instead of red. +If another config was selected using the previously defined process, then a "diff" of the two will be generated, showing all the +differences between the two in a format similar to how commits are displayed on github. +Finally, once you are done press q to quit (or use ctrl+c). From 760564d8b3a0389f84ff7205c5667ec150238c65 Mon Sep 17 00:00:00 2001 From: Jonathan Hancock <56547447+JonathanHancock0@users.noreply.github.com> Date: Fri, 30 Jun 2023 13:50:33 +0200 Subject: [PATCH 56/90] Added a screenshot of the config viewer --- docs/ConfViewerScreenshot.png | Bin 0 -> 81624 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 docs/ConfViewerScreenshot.png diff --git a/docs/ConfViewerScreenshot.png b/docs/ConfViewerScreenshot.png new file mode 100644 index 0000000000000000000000000000000000000000..32383d3698f1edf370a491d2e30f7d8793b27d17 GIT binary patch literal 81624 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 literal 0 HcmV?d00001 From 571b87ac97ced8d267fefac33b3196260d9997b9 Mon Sep 17 00:00:00 2001 From: Jonathan Hancock <56547447+JonathanHancock0@users.noreply.github.com> Date: Fri, 30 Jun 2023 13:54:46 +0200 Subject: [PATCH 57/90] Fixed image, minor edits --- docs/ConfigDatabase.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/ConfigDatabase.md b/docs/ConfigDatabase.md index 81f8b712..8305d860 100644 --- a/docs/ConfigDatabase.md +++ b/docs/ConfigDatabase.md @@ -1,19 +1,19 @@ # Interacting with the Configuration Database ## Uploading a config -To use configuration files with a nanorc instance run on Kubernetes, it first needs to be uploaded to the MongoDB running in the cluster. -To do this, simply run `upload-conf ` +To use configuration files with a nanorc instance run on Kubernetes, the config first needs to be uploaded to the MongoDB running in the cluster. +To do this, simply run `upload-conf `. Keep in mind that the config directory can contain underscores, but the name it will be given in the database cannot (hyphens are fine). ## Viewing configurations To inspect the contents of the database, run `daqconf_viewer` after setting up the software environment. This will open a graphical UI in the terminal. -![Config Viewer](ConfViewerSC.png) -A list of all configuration names is show on the very left. -Clicking one of them will generate a list of buttons at the top corresponding to the saved versions of the config. -Press one of those buttons to bring up the config file the the dsplay. By clicking the arrows, the contents of each sub-schema can be expanded. +![Config Viewer](ConfViewerScreenshot.png) +A list of all configuration names is shown on the left. +Clicking one of them will generate a list of buttons at the top, corresponding to the saved versions of the config. +Press one of those buttons to bring up the config file in the display box. By clicking the arrows, the contents of each sub-schema can be expanded. -Pressing the D key with the config open will take you to a very similar screen, albeit with green lines instead of red. -If another config was selected using the previously defined process, then a "diff" of the two will be generated, showing all the +Pressing the D key after picking a config will take you to a very similar screen, albeit with green lines instead of red. +If a second config is selected using the previously defined process, then a "diff" of the two will be generated, showing all the differences between the two in a format similar to how commits are displayed on github. Finally, once you are done press q to quit (or use ctrl+c). From def5d638aa29a4f465608c853a7c263d0dfbdca6 Mon Sep 17 00:00:00 2001 From: Jonathan Hancock <56547447+JonathanHancock0@users.noreply.github.com> Date: Fri, 30 Jun 2023 14:02:34 +0200 Subject: [PATCH 58/90] Inserted a link to conf database info --- docs/InstructionsForCasualUsers.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/InstructionsForCasualUsers.md b/docs/InstructionsForCasualUsers.md index a8080d60..b11a81a3 100644 --- a/docs/InstructionsForCasualUsers.md +++ b/docs/InstructionsForCasualUsers.md @@ -21,6 +21,8 @@ As of Oct-4-2022, here are the steps that should be used when you first create y * and * `hdf5_dump.py -n 3 -p all -f swtest_run000101_0000_*.hdf5` +If you intend to run _nanorc_ on the Kubernetes cluster, then [these instructions](ConfigDatabase.md) may be useful. + When you return to this work area (for example, after logging out and back in), you can skip the 'setup' steps in the instructions above. For example: 1. `cd ` From 8ba998e2ccff48039c73ae70a947f671b7b3e5bc Mon Sep 17 00:00:00 2001 From: Jonathan Hancock <56547447+JonathanHancock0@users.noreply.github.com> Date: Fri, 30 Jun 2023 14:04:51 +0200 Subject: [PATCH 59/90] Added a line explaining how to start nanorc with k8s --- docs/ConfigDatabase.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/ConfigDatabase.md b/docs/ConfigDatabase.md index 8305d860..5aa06122 100644 --- a/docs/ConfigDatabase.md +++ b/docs/ConfigDatabase.md @@ -1,9 +1,11 @@ # Interacting with the Configuration Database ## Uploading a config -To use configuration files with a nanorc instance run on Kubernetes, the config first needs to be uploaded to the MongoDB running in the cluster. +To use configuration files with a _nanorc_ instance run on Kubernetes, the config first needs to be uploaded to the MongoDB running in the cluster. To do this, simply run `upload-conf `. +_nanorc_ should then be started with `nanorc --pm k8s://np04-srv-015:31000 db://name-for-the-conf partition-name` + Keep in mind that the config directory can contain underscores, but the name it will be given in the database cannot (hyphens are fine). ## Viewing configurations From 03c81914f2dcacab94b0451dd088242920a1b50c Mon Sep 17 00:00:00 2001 From: Alessandro Thea Date: Fri, 30 Jun 2023 17:45:13 +0200 Subject: [PATCH 60/90] Fixed a bug in the dpdk lcore logic --- python/daqconf/apps/readout_gen.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/daqconf/apps/readout_gen.py b/python/daqconf/apps/readout_gen.py index 24c3367c..689e1e7d 100644 --- a/python/daqconf/apps/readout_gen.py +++ b/python/daqconf/apps/readout_gen.py @@ -290,7 +290,7 @@ def get_lcore_config(self, RU_DESCRIPTOR): ex = self.lcores_excpt[(RU_DESCRIPTOR.host_name, RU_DESCRIPTOR.iface)] lcore_id_set = ex['lcore_id_set'] except KeyError: - lcore_id_set = cfg.dpdk_lcore_config['default_lcore_id_set'] + lcore_id_set = cfg.dpdk_lcores_config['default_lcore_id_set'] return list(dict.fromkeys(lcore_id_set)) From 5c8ddcdc74d87349b0fcb1817cc406319e174cfc Mon Sep 17 00:00:00 2001 From: Alessandro Thea Date: Mon, 3 Jul 2023 10:54:23 +0200 Subject: [PATCH 61/90] Add tx endpoint sorting --- python/daqconf/apps/readout_gen.py | 7 ++++++- scripts/dromap_editor | 8 ++++++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/python/daqconf/apps/readout_gen.py b/python/daqconf/apps/readout_gen.py index 689e1e7d..6a3e17b4 100644 --- a/python/daqconf/apps/readout_gen.py +++ b/python/daqconf/apps/readout_gen.py @@ -156,7 +156,12 @@ def build_conf(self, eal_arg_list, lcores_id_set): srcs = [] # Sid is used for the "Source.id". What is it? - for sid,((tx_ip,_,_),streams) in enumerate(txs.items()): + # Transmitters are sorted by tx ip address. + # This is not good for understanding what is what, so we sort them by minimum + # src_id + txs_sorted_by_src = sorted(txs.items(), key=lambda x: min(x[1], key=lambda y: y.src_id)) + + for sid,((tx_ip,_,_),streams) in enumerate(txs_sorted_by_src): ssm = nrc.SrcStreamsMapping([ nrc.StreamMap(source_id=s.src_id, stream_id=s.geo_id.stream_id) for s in streams diff --git a/scripts/dromap_editor b/scripts/dromap_editor index 73ff5397..db4f6def 100755 --- a/scripts/dromap_editor +++ b/scripts/dromap_editor @@ -265,8 +265,12 @@ def ipy(obj): m = obj - import IPython - IPython.embed(colors="neutral") + try: + import IPython + IPython.embed(colors="neutral") + except ModuleNotFoundError: + print("[red]Error: IPython is not installed[/red]") + raise SystemExit(-1) if __name__ == "__main__": cli(obj=dromap.DetReadoutMapService()) From 44d882ed2114b4d5a9517a78a54fea4a58c7e11d Mon Sep 17 00:00:00 2001 From: Michal Rigan Date: Mon, 3 Jul 2023 12:01:38 -0500 Subject: [PATCH 62/90] fix for type in schema file --- schema/daqconf/triggergen.jsonnet | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/schema/daqconf/triggergen.jsonnet b/schema/daqconf/triggergen.jsonnet index eabfe335..c5bf25cc 100644 --- a/schema/daqconf/triggergen.jsonnet +++ b/schema/daqconf/triggergen.jsonnet @@ -123,7 +123,7 @@ local cs = { s.field( "mlt_ignore_tc", self.tc_types, default=[], doc="Optional list of TC types to be ignored in MLT"), s.field( "mlt_use_readout_map", types.flag, default=false, doc="Option to use custom readout map in MLT"), s.field( "mlt_td_readout_map", self.tc_readout_map, default=self.tc_readout_map, doc="The readout windows assigned to TDs in MLT, based on TC type."), - s.field( "mlt_use_bitwords", self.flag, default=false, doc="Option to use bitwords (ie trigger types, coincidences) when forming trigger decisions in MLT" ), + s.field( "mlt_use_bitwords", types.flag, default=false, doc="Option to use bitwords (ie trigger types, coincidences) when forming trigger decisions in MLT" ), s.field( "mlt_trigger_bitwords", self.bitwords, default=[], doc="Optional dictionary of bitwords to use when forming trigger decisions in MLT" ), s.field( "use_custom_maker", types.flag, default=false, doc="Option to use a Custom Trigger Candidate Maker (plugin)"), s.field( "ctcm_trigger_types", self.tc_types, default=[4], doc="Optional list of TC types to be used by the Custom Trigger Candidate Maker (plugin)"), From 7512390282755b93b84f19e9534bd1734b787acb Mon Sep 17 00:00:00 2001 From: Eric Flumerfelt Date: Mon, 3 Jul 2023 13:36:44 -0400 Subject: [PATCH 63/90] Fix typo and incorrect dict iteration --- python/daqconf/apps/readout_gen.py | 2 +- scripts/daqconf_multiru_gen | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/python/daqconf/apps/readout_gen.py b/python/daqconf/apps/readout_gen.py index 6a3e17b4..657dee0b 100644 --- a/python/daqconf/apps/readout_gen.py +++ b/python/daqconf/apps/readout_gen.py @@ -945,7 +945,7 @@ def generate( if not cvmfs in ddf_path.parents: dir_names.add(ddf_path.parent) - for _,file in data_file_map: + for file in data_file_map.values(): f = Path(file) if not cvmfs in f.parents: dir_names.add(f.parent) diff --git a/scripts/daqconf_multiru_gen b/scripts/daqconf_multiru_gen index 45ba9fe3..e2ad2e66 100755 --- a/scripts/daqconf_multiru_gen +++ b/scripts/daqconf_multiru_gen @@ -490,7 +490,7 @@ def cli( dqm_app_names = [] #-------------------------------------------------------------------------- - # Readout generatioo + # Readout generation #-------------------------------------------------------------------------- roapp_gen = ReadoutAppGenerator(readout, detector, daq_common) for ru_i,(ru_name, ru_desc) in enumerate(ru_descs.items()): From ee588ff13ed45f29a5042ee358352a2524c6e3af Mon Sep 17 00:00:00 2001 From: Michal Rigan Date: Tue, 4 Jul 2023 04:38:30 -0500 Subject: [PATCH 64/90] fix for import of TriggerBits and small change to error message --- python/daqconf/apps/trigger_gen.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/python/daqconf/apps/trigger_gen.py b/python/daqconf/apps/trigger_gen.py index 3264dcbd..6b2fe0c6 100644 --- a/python/daqconf/apps/trigger_gen.py +++ b/python/daqconf/apps/trigger_gen.py @@ -34,7 +34,7 @@ from daqconf.core.conf_utils import Direction, Queue from daqconf.core.sourceid import TAInfo, TPInfo, TCInfo -from trgdataformats._daq_trgdataformats_py import TriggerBits as trgbs +from trgdataformats import TriggerBits as trgbs #FIXME maybe one day, triggeralgs will define schemas... for now allow a dictionary of 4byte int, 4byte floats, and strings moo.otypes.make_type(schema='number', dtype='i4', name='temp_integer', path='temptypes') @@ -82,8 +82,8 @@ def get_trigger_bitwords(bitwords): tmp_bits = [] for bit_name in bitword: bit_value = trgbs.string_to_fragment_type_value(bit_name) - if bit_value == -1: - raise RuntimeError(f'One or more of provided MLT trigger bitwords is incorrect! Please recheck the names...') + if bit_value == 0: + raise RuntimeError(f'One (or more) of provided MLT trigger bitwords is unknown! Please recheck the names...') else: tmp_bits.append(bit_value) final_bit_flags.append(tmp_bits) From d4094a01193965c8b8f9683db19e7c56745b7440 Mon Sep 17 00:00:00 2001 From: Alessandro Thea Date: Tue, 4 Jul 2023 16:44:50 +0200 Subject: [PATCH 65/90] adding support for self-triggered data format --- python/daqconf/apps/readout_gen.py | 254 ++++++++++++++--------------- 1 file changed, 124 insertions(+), 130 deletions(-) diff --git a/python/daqconf/apps/readout_gen.py b/python/daqconf/apps/readout_gen.py index 6a3e17b4..118d93c4 100644 --- a/python/daqconf/apps/readout_gen.py +++ b/python/daqconf/apps/readout_gen.py @@ -28,49 +28,39 @@ from ..core.sourceid import SourceIDBroker from ..core.daqmodule import DAQModule from ..core.app import App, ModuleGraph -from ..detreadoutmap import ReadoutUnitDescriptor +from ..detreadoutmap import ReadoutUnitDescriptor, group_by_key # from detdataformats._daq_detdataformats_py import * from detdataformats import DetID -from ..detreadoutmap import group_by_key - - ## Compute the frament types from detector infos def compute_data_types( - det_id : int, - clk_freq_hz: int, - kind: str + stream_entry ): - det_str = DetID.subdetector_to_string(DetID.Subdetector(det_id)) - - fe_type = None - fakedata_frag_type = None - queue_frag_type = None - fakedata_time_tick=None - fakedata_frame_size=None - - # if ((det_str == "HD_TPC" or det_str== "VD_Bottom_TPC") and clk_freq_hz== 50000000): - # fe_type = "wib" - # queue_frag_type="WIBFrame" - # fakedata_frag_type = "ProtoWIB" - # fakedata_time_tick=25 - # fakedata_frame_size=434 + det_str = DetID.subdetector_to_string(DetID.Subdetector(stream_entry.det_id)) + + # Far detector types - if ((det_str == "HD_TPC" or det_str == "VD_Bottom_TPC") and clk_freq_hz== 62500000 and kind=='flx' ): + if (det_str in ("HD_TPC","VD_Bottom_TPC") and stream_entry.kind=='flx' ): fe_type = "wib2" queue_frag_type="WIB2Frame" fakedata_frag_type = "WIB" fakedata_time_tick=32 fakedata_frame_size=472 - elif ((det_str == "HD_TPC" or det_str == "VD_Bottom_TPC") and clk_freq_hz== 62500000 and kind=='eth' ): + elif (("HD_TPC","VD_Bottom_TPC") and stream_entry.kind=='eth' ): fe_type = "wibeth" queue_frag_type="WIBEthFrame" fakedata_frag_type = "WIBEth" fakedata_time_tick=2048 fakedata_frame_size=7200 - elif det_str == "HD_PDS" or det_str == "VD_Cathode_PDS" or det_str =="VD_Membrane_PDS": + elif det_str in ("HD_PDS", "VD_Cathode_PDS", "VD_Membrane_PDS") and stream_entry.parameters.mode == "var_rate": + fe_type = "pds" + fakedata_frag_type = "DAPHNE" + queue_frag_type = "PDSFrame" + fakedata_time_tick=None + fakedata_frame_size=472 + elif det_str in ("HD_PDS", "VD_Cathode_PDS", "VD_Membrane_PDS") and stream_entry.parameters.mode == "fix_rate": fe_type = "pds_stream" fakedata_frag_type = "DAPHNE" queue_frag_type = "PDSStreamFrame" @@ -97,7 +87,7 @@ def compute_data_types( # fakedata_time_tick=None # fakedata_frame_size=None else: - raise ValueError(f"No match for {det_str}, {clk_freq_hz}, {kind}") + raise ValueError(f"No match for {det_str}, {stream_entry.kind}") return fe_type, queue_frag_type, fakedata_frag_type, fakedata_time_tick, fakedata_frame_size @@ -198,56 +188,7 @@ def build_conf(self, eal_arg_list, lcores_id_set): ) return conf - - # def build_conf_by_host(self, eal_arg_list): - - # streams_by_if_and_tx = self.streams_by_host() - - # ifcfgs = [] - # for (rx_ip, rx_mac, _),txs in streams_by_if_and_tx.items(): - # srcs = [] - # # Sid is used for the "Source.id". What is it? - - # for sid,((tx_ip,_,_),streams) in enumerate(txs.items()): - # ssm = nrc.SrcStreamsMapping([ - # nrc.StreamMap(source_id=s.src_id, stream_id=s.geo_id.stream_id) - # for s in streams - # ]) - # geo_id = streams[0].geo_id - # si = nrc.SrcGeoInfo( - # det_id=geo_id.det_id, - # crate_id=geo_id.crate_id, - # slot_id=geo_id.slot_id - # ) - - # srcs.append( - # nrc.Source( - # id=sid, # FIXME what is this ID? - # ip_addr=tx_ip, - # lcore=sid+self.lcore_offset, - # rx_q=sid, - # src_info=si, - # src_streams_mapping=ssm - # ) - # ) - # ifcfgs.append( - # nrc.Interface( - # ip_addr=rx_ip, - # mac_addr=rx_mac, - # expected_sources=srcs - # ) - # ) - - - conf = nrc.Conf( - ifaces = ifcfgs, - eal_arg_list=eal_arg_list - ) - - return conf - -# ) - + # Time to wait on pop() QUEUE_POP_WAIT_MS = 10 # This affects stop time, as each link will wait this long before stop @@ -306,8 +247,8 @@ def get_lcore_config(self, RU_DESCRIPTOR): ### def create_fake_cardreader( self, - FRONTEND_TYPE: str, - QUEUE_FRAGMENT_TYPE: str, + # FRONTEND_TYPE: str, + # QUEUE_FRAGMENT_TYPE: str, DATA_FILES: dict, RU_DESCRIPTOR # ReadoutUnitDescriptor @@ -338,15 +279,27 @@ def create_fake_cardreader( modules = [DAQModule(name = "fake_source", plugin = "FDFakeCardReader", conf = conf)] - queues = [ - Queue( - f"fake_source.output_{s.src_id}", - f"datahandler_{s.src_id}.raw_input", - QUEUE_FRAGMENT_TYPE, - f'{FRONTEND_TYPE}_link_{s.src_id}', 100000 - ) for s in RU_DESCRIPTOR.streams - ] + # queues = [ + # Queue( + # f"fake_source.output_{s.src_id}", + # f"datahandler_{s.src_id}.raw_input", + # QUEUE_FRAGMENT_TYPE, + # f'{FRONTEND_TYPE}_link_{s.src_id}', 100000 + # ) for s in RU_DESCRIPTOR.streams + # ] + queues = [] + for s in RU_DESCRIPTOR.streams: + FRONTEND_TYPE, QUEUE_FRAGMENT_TYPE, _, _, _ = compute_data_types(s) + queues.append( + Queue( + f"fake_source.output_{s.src_id}", + f"datahandler_{s.src_id}.raw_input", + QUEUE_FRAGMENT_TYPE, + f'{FRONTEND_TYPE}_link_{s.src_id}', 100000 + ) + ) + return modules, queues @@ -368,15 +321,15 @@ def create_felix_cardreader( """ links_slr0 = [] links_slr1 = [] - sids_slr0 = [] - sids_slr1 = [] + strms_slr0 = [] + strms_slr1 = [] for stream in RU_DESCRIPTOR.streams: if stream.parameters.slr == 0: links_slr0.append(stream.parameters.link) - sids_slr0.append(stream.src_id) + strms_slr0.append(stream) if stream.parameters.slr == 1: links_slr1.append(stream.parameters.link) - sids_slr1.append(stream.src_id) + strms_slr1.append(stream) links_slr0.sort() links_slr1.sort() @@ -413,27 +366,53 @@ def create_felix_cardreader( ) )] + # # Queues for card reader 1 + # queues += [ + # Queue( + # f'flxcard_0.output_{idx}', + # f"datahandler_{idx}.raw_input", + # QUEUE_FRAGMENT_TYPE, + # f'{FRONTEND_TYPE}_link_{idx}', + # 100000 + # ) for idx in strms_slr0 + # ] + # # Queues for card reader 2 + # queues += [ + # Queue( + # f'flxcard_1.output_{idx}', + # f"datahandler_{idx}.raw_input", + # QUEUE_FRAGMENT_TYPE, + # f'{FRONTEND_TYPE}_link_{idx}', + # 100000 + # ) for idx in strms_slr1 + # ] + # Queues for card reader 1 - queues += [ - Queue( - f'flxcard_0.output_{idx}', - f"datahandler_{idx}.raw_input", - QUEUE_FRAGMENT_TYPE, - f'{FRONTEND_TYPE}_link_{idx}', - 100000 - ) for idx in sids_slr0 - ] + for s in strms_slr0: + FRONTEND_TYPE, QUEUE_FRAGMENT_TYPE, _, _, _ = compute_data_types(s) + queues.append( + Queue( + f'flxcard_0.output_{s.src_id}', + f"datahandler_{s.src_id}.raw_input", + QUEUE_FRAGMENT_TYPE, + f'{FRONTEND_TYPE}_link_{s.src_id}', + 100000 + ) + ) # Queues for card reader 2 - queues += [ - Queue( - f'flxcard_1.output_{idx}', - f"datahandler_{idx}.raw_input", - QUEUE_FRAGMENT_TYPE, - f'{FRONTEND_TYPE}_link_{idx}', - 100000 - ) for idx in sids_slr1 - ] - + for s in strms_slr1: + FRONTEND_TYPE, QUEUE_FRAGMENT_TYPE, _, _, _ = compute_data_types(s) + queues.append( + Queue( + f'flxcard_1.output_{s.src_id}', + f"datahandler_{s.src_id}.raw_input", + QUEUE_FRAGMENT_TYPE, + f'{FRONTEND_TYPE}_link_{s.src_id}', + 100000 + ) + ) + + return modules, queues @@ -467,15 +446,27 @@ def create_dpdk_cardreader( )] # Queues - queues = [ - Queue( - f"{nic_reader_name}.output_{stream.src_id}", - f"datahandler_{stream.src_id}.raw_input", QUEUE_FRAGMENT_TYPE, - f'{FRONTEND_TYPE}_stream_{stream.src_id}', 100000 - ) - for stream in RU_DESCRIPTOR.streams - ] + # queues = [ + # Queue( + # f"{nic_reader_name}.output_{stream.src_id}", + # f"datahandler_{stream.src_id}.raw_input", QUEUE_FRAGMENT_TYPE, + # f'{FRONTEND_TYPE}_stream_{stream.src_id}', 100000 + # ) + # for stream in RU_DESCRIPTOR.streams + # ] + queues = [] + for s in RU_DESCRIPTOR.streams: + FRONTEND_TYPE, QUEUE_FRAGMENT_TYPE, _, _, _ = compute_data_types(s) + queues.append( + Queue( + f"fake_source.output_{s.src_id}", + f"datahandler_{s.src_id}.raw_input", + QUEUE_FRAGMENT_TYPE, + f'{FRONTEND_TYPE}_link_{s.src_id}', 100000 + ) + ) + return modules, queues @@ -799,7 +790,7 @@ def generate( RU_DESCRIPTOR (ReadoutUnitDescriptor): A readout unit descriptor object SOURCEID_BROKER (SourceIDBroker): The source ID brocker data_file_map (dict): Map of pattern files to application - data_timeout_requests (_type_): Data timeout request + data_timeout_requests (int): Data timeout request Raises: RuntimeError: _description_ @@ -814,10 +805,11 @@ def generate( DATA_FILES = data_file_map DATA_REQUEST_TIMEOUT=data_timeout_requests - FRONTEND_TYPE, QUEUE_FRAGMENT_TYPE, _, _, _ = compute_data_types(RU_DESCRIPTOR.det_id, self.det_cfg.clock_speed_hz, RU_DESCRIPTOR.kind) + # FRONTEND_TYPE, QUEUE_FRAGMENT_TYPE, _, _, _ = compute_data_types(RU_DESCRIPTOR.det_id, self.det_cfg.clock_speed_hz, RU_DESCRIPTOR.kind) # TPG is automatically disabled for non wib2 frontends - TPG_ENABLED = TPG_ENABLED and (FRONTEND_TYPE=='wib2' or FRONTEND_TYPE=='wibeth') + # TPG_ENABLED = TPG_ENABLED and (FRONTEND_TYPE=='wib2' or FRONTEND_TYPE=='wibeth') + TPG_ENABLED = TPG_ENABLED and (RU_DESCRIPTOR.det_id == DetID.Subdetector.kHD_TPC.value) modules = [] queues = [] @@ -976,19 +968,21 @@ def create_fake_readout_app( modules = [] queues = [] - _, _, fakedata_fragment_type, fakedata_time_tick, fakedata_frame_size = compute_data_types(RU_DESCRIPTOR.det_id, CLOCK_SPEED_HZ, RU_DESCRIPTOR.kind) + # _, _, fakedata_fragment_type, fakedata_time_tick, fakedata_frame_size = compute_data_types(RU_DESCRIPTOR.det_id, CLOCK_SPEED_HZ, RU_DESCRIPTOR.kind) for stream in RU_DESCRIPTOR.streams: - modules += [DAQModule(name = f"fakedataprod_{stream.src_id}", - plugin='FakeDataProd', - conf = fdp.ConfParams( - system_type = "Detector_Readout", - source_id = stream.src_id, - time_tick_diff = fakedata_time_tick, - frame_size = fakedata_frame_size, - response_delay = 0, - fragment_type = fakedata_fragment_type, - ))] + _, _, fakedata_fragment_type, fakedata_time_tick, fakedata_frame_size = compute_data_types(stream) + + modules += [DAQModule(name = f"fakedataprod_{stream.src_id}", + plugin='FakeDataProd', + conf = fdp.ConfParams( + system_type = "Detector_Readout", + source_id = stream.src_id, + time_tick_diff = fakedata_time_tick, + frame_size = fakedata_frame_size, + response_delay = 0, + fragment_type = fakedata_fragment_type, + ))] mgraph = ModuleGraph(modules, queues=queues) From 74e4ac1922112ba99f253c5b743ec457b1bea2c2 Mon Sep 17 00:00:00 2001 From: Alessandro Thea Date: Tue, 4 Jul 2023 16:51:42 +0200 Subject: [PATCH 66/90] fixes --- python/daqconf/apps/readout_gen.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/python/daqconf/apps/readout_gen.py b/python/daqconf/apps/readout_gen.py index 118d93c4..59af2d67 100644 --- a/python/daqconf/apps/readout_gen.py +++ b/python/daqconf/apps/readout_gen.py @@ -38,7 +38,7 @@ def compute_data_types( stream_entry ): - det_str = DetID.subdetector_to_string(DetID.Subdetector(stream_entry.det_id)) + det_str = DetID.subdetector_to_string(DetID.Subdetector(stream_entry.geo_id.det_id)) # Far detector types @@ -308,8 +308,8 @@ def create_fake_cardreader( ### def create_felix_cardreader( self, - FRONTEND_TYPE: str, - QUEUE_FRAGMENT_TYPE: str, + # FRONTEND_TYPE: str, + # QUEUE_FRAGMENT_TYPE: str, CARD_ID_OVERRIDE: int, NUMA_ID: int, RU_DESCRIPTOR # ReadoutUnitDescriptor @@ -418,8 +418,8 @@ def create_felix_cardreader( def create_dpdk_cardreader( self, - FRONTEND_TYPE: str, - QUEUE_FRAGMENT_TYPE: str, + # FRONTEND_TYPE: str, + # QUEUE_FRAGMENT_TYPE: str, RU_DESCRIPTOR # ReadoutUnitDescriptor ) -> tuple[list, list]: """ @@ -823,8 +823,8 @@ def generate( # Create the card readers if cfg.use_fake_cards: fakecr_mods, fakecr_queues = self.create_fake_cardreader( - FRONTEND_TYPE=FRONTEND_TYPE, - QUEUE_FRAGMENT_TYPE=QUEUE_FRAGMENT_TYPE, + # FRONTEND_TYPE=FRONTEND_TYPE, + # QUEUE_FRAGMENT_TYPE=QUEUE_FRAGMENT_TYPE, DATA_FILES=DATA_FILES, RU_DESCRIPTOR=RU_DESCRIPTOR ) @@ -833,8 +833,8 @@ def generate( else: if RU_DESCRIPTOR.kind == 'flx': flx_mods, flx_queues = self.create_felix_cardreader( - FRONTEND_TYPE=FRONTEND_TYPE, - QUEUE_FRAGMENT_TYPE=QUEUE_FRAGMENT_TYPE, + # FRONTEND_TYPE=FRONTEND_TYPE, + # QUEUE_FRAGMENT_TYPE=QUEUE_FRAGMENT_TYPE, CARD_ID_OVERRIDE=card_override, NUMA_ID=numa_id, RU_DESCRIPTOR=RU_DESCRIPTOR @@ -844,8 +844,8 @@ def generate( elif RU_DESCRIPTOR.kind == 'eth' and RU_DESCRIPTOR.streams[0].parameters.protocol == "udp": dpdk_mods, dpdk_queues = self.create_dpdk_cardreader( - FRONTEND_TYPE=FRONTEND_TYPE, - QUEUE_FRAGMENT_TYPE=QUEUE_FRAGMENT_TYPE, + # FRONTEND_TYPE=FRONTEND_TYPE, + # QUEUE_FRAGMENT_TYPE=QUEUE_FRAGMENT_TYPE, RU_DESCRIPTOR=RU_DESCRIPTOR ) cr_mods += dpdk_mods From fc9a0e3886d352d7a9acdd5a19af40e207cb9af0 Mon Sep 17 00:00:00 2001 From: Alessandro Thea Date: Tue, 4 Jul 2023 21:12:43 +0200 Subject: [PATCH 67/90] Fixes --- python/daqconf/apps/readout_gen.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/python/daqconf/apps/readout_gen.py b/python/daqconf/apps/readout_gen.py index 59af2d67..afa2c00d 100644 --- a/python/daqconf/apps/readout_gen.py +++ b/python/daqconf/apps/readout_gen.py @@ -456,14 +456,14 @@ def create_dpdk_cardreader( # ] queues = [] - for s in RU_DESCRIPTOR.streams: - FRONTEND_TYPE, QUEUE_FRAGMENT_TYPE, _, _, _ = compute_data_types(s) + for stream in RU_DESCRIPTOR.streams: + FRONTEND_TYPE, QUEUE_FRAGMENT_TYPE, _, _, _ = compute_data_types(stream) queues.append( Queue( - f"fake_source.output_{s.src_id}", - f"datahandler_{s.src_id}.raw_input", + f"{nic_reader_name}.output_{stream.src_id}", + f"datahandler_{stream.src_id}.raw_input", QUEUE_FRAGMENT_TYPE, - f'{FRONTEND_TYPE}_link_{s.src_id}', 100000 + f'{FRONTEND_TYPE}_stream_{stream.src_id}', 100000 ) ) From 123f61eb38014a95e04eb2534cccb4423add9557 Mon Sep 17 00:00:00 2001 From: Alessandro Thea Date: Wed, 5 Jul 2023 14:37:46 +0200 Subject: [PATCH 68/90] Bumping tag version --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 73739304..2d6dfd41 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,5 @@ cmake_minimum_required(VERSION 3.12) -project(daqconf VERSION 5.6.1) +project(daqconf VERSION 6.0.0) find_package(daq-cmake REQUIRED ) From 9823b12ef506a6fbf2155f6fbaace039448c07b1 Mon Sep 17 00:00:00 2001 From: Kurt Biery Date: Thu, 6 Jul 2023 16:59:48 -0500 Subject: [PATCH 69/90] Put back missing comparison for det_str in readout_gen compute_data_types --- python/daqconf/apps/readout_gen.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/daqconf/apps/readout_gen.py b/python/daqconf/apps/readout_gen.py index 45064ba1..46ac1a4e 100644 --- a/python/daqconf/apps/readout_gen.py +++ b/python/daqconf/apps/readout_gen.py @@ -48,7 +48,7 @@ def compute_data_types( fakedata_frag_type = "WIB" fakedata_time_tick=32 fakedata_frame_size=472 - elif (("HD_TPC","VD_Bottom_TPC") and stream_entry.kind=='eth' ): + elif (det_str in ("HD_TPC","VD_Bottom_TPC") and stream_entry.kind=='eth' ): fe_type = "wibeth" queue_frag_type="WIBEthFrame" fakedata_frag_type = "WIBEth" From e9b51e1651d57ae48d10285a1665846698fbeffe Mon Sep 17 00:00:00 2001 From: Kurt Biery Date: Fri, 7 Jul 2023 15:31:32 -0500 Subject: [PATCH 70/90] Updated version to v6.0.1 --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 2d6dfd41..995d3e1e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,5 @@ cmake_minimum_required(VERSION 3.12) -project(daqconf VERSION 6.0.0) +project(daqconf VERSION 6.0.1) find_package(daq-cmake REQUIRED ) From 7e9fa20285b5f9e373fba6786759f3fd45572ec0 Mon Sep 17 00:00:00 2001 From: Jonathan Hancock Date: Tue, 11 Jul 2023 11:26:48 +0200 Subject: [PATCH 71/90] Newest versions of a conf are shown first by default, the user can press v to reverse the ordering. --- scripts/daqconf_viewer | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/scripts/daqconf_viewer b/scripts/daqconf_viewer index e02e513c..ca68aca8 100755 --- a/scripts/daqconf_viewer +++ b/scripts/daqconf_viewer @@ -94,6 +94,7 @@ class Versions(Vertical): super().__init__(**kwargs) self.hostname = hostname self.current_conf = None + self.reverse = True def compose(self) -> ComposeResult: yield TitleBox(f'Configuration versions') @@ -111,7 +112,10 @@ class Versions(Vertical): async with httpx.AsyncClient() as client: payload = {'name': self.current_conf} r = await client.get(f'{self.hostname}/listVersions', auth=auth, params=payload, timeout=5) - self.vlist = r.json()['versions'] #This is a list of ints + numlist = r.json()['versions'] #This is a list of ints + if self.reverse: + numlist.reverse() + self.vlist = numlist except Exception as e: if isinstance(e, asyncio.CancelledError): self.display_error(f"Couldn't retrieve versions from {self.hostname}/listVersions") @@ -300,9 +304,10 @@ class ConfViewer(App): BINDINGS = [ ("d", "make_diff", "Diff"), ("q", "quit", "Quit"), + ("v", "flip_versions", "Reverse Version Order") ] - def __init__(self, host, port, **kwargs): + def __init__(self, host, port, dir, **kwargs): super().__init__(**kwargs) self.hostname = f"{host}:{port}" @@ -325,12 +330,18 @@ class ConfViewer(App): oldconf = dis.confdata self.push_screen('diff') + def action_flip_versions(self): + '''Tells the versions widget to display them the other way around''' + ver = self.screen.query_one(Versions) + ver.reverse = not ver.reverse + CONTEXT_SETTINGS = dict(help_option_names=['-h', '--help']) @click.command(context_settings=CONTEXT_SETTINGS) @click.option('--host', default="http://np04-srv-023", help='Machine hosting the config service') @click.option('--port', default="31011", help='Port that the config service listens on') -def start(host:str, port:str): - app = ConfViewer(host, port) +@click.option('--dir', default = "", help='Top-level directory to look for local files in') +def start(host:str, port:str, dir:str): + app = ConfViewer(host, port, dir) app.run() if __name__ == "__main__": From 648486e78a2b11dd532a5302d240c7e91f29cb35 Mon Sep 17 00:00:00 2001 From: Jonathan Hancock Date: Tue, 11 Jul 2023 14:52:45 +0200 Subject: [PATCH 72/90] The title of the diff display now shows what is being compared. The user can search for configs containing a particular term. --- scripts/daqconf_viewer | 53 +++++++++++++++++++++++++++++++++++------- 1 file changed, 44 insertions(+), 9 deletions(-) diff --git a/scripts/daqconf_viewer b/scripts/daqconf_viewer index ca68aca8..99659c7b 100755 --- a/scripts/daqconf_viewer +++ b/scripts/daqconf_viewer @@ -8,23 +8,28 @@ import sys from rich.text import Text from rich.markdown import Markdown -from difflib import context_diff, ndiff, unified_diff +from difflib import unified_diff from textual import log, events from textual.app import App, ComposeResult -from textual.containers import Horizontal, Content, Container, Vertical +from textual.containers import Content, Container, Horizontal, Vertical from textual.widget import Widget -from textual.widgets import Button, Header, Footer, Static, Label, ListView, ListItem, Tree +from textual.widgets import Button, Footer, Header, Input, Label, ListItem, ListView, Static, Tree from textual.reactive import reactive, Reactive from textual.screen import Screen auth = ("fooUsr", "barPass") oldconf = None +oldconfname = None +oldconfver = None class TitleBox(Static): def __init__(self, title, **kwargs): super().__init__(Markdown(f'# {title}')) + def update(self, text): + super().update(Markdown(f'# {text}')) + class LabelItem(ListItem): def __init__(self, label: str) -> None: super().__init__() @@ -39,12 +44,14 @@ class Configs(Static): def __init__(self, hostname, **kwargs): super().__init__(**kwargs) self.hostname = hostname + self.term = "" def on_mount(self) -> None: self.set_interval(0.1, self.update_configs) def compose(self) -> ComposeResult: yield TitleBox('Configurations') + yield Input(placeholder='Search Configs') yield ListView(LabelItem("This shouldn't be visible!")) async def update_configs(self) -> None: @@ -58,10 +65,27 @@ class Configs(Static): #in this case, as it will not be able to find the relevent widgets. if isinstance(e, asyncio.CancelledError): return - self.display_error(f"Couldn't retrieve configs from {self.hostname}/listConfigs") + self.display_error(f"Couldn't retrieve configs from {self.hostname}/listConfigs\nError: {e}") def watch_conflist(self, conflist:list[str]): - label_list = [LabelItem(c) for c in conflist] + self.display_conflist() + + async def on_input_changed(self, message: Input.Changed) -> None: + '''This event occurs whenever the user types in the search box.''' + self.term = message.value + self.display_conflist() + + def display_conflist(self) -> None: + ''' + We regenerate the list whenever the actual list of configs changes, or whenever the user types in the search box. + #If the box is empty, don't filter, else we require that the search term is in the name + ''' + if self.term == "": + filtered = self.conflist + else: + filtered = [name for name in self.conflist if self.term in name] + + label_list = [LabelItem(f) for f in filtered] the_list = self.query_one(ListView) the_list.clear() for item in label_list: @@ -118,7 +142,7 @@ class Versions(Vertical): self.vlist = numlist except Exception as e: if isinstance(e, asyncio.CancelledError): - self.display_error(f"Couldn't retrieve versions from {self.hostname}/listVersions") + self.display_error(f"Couldn't retrieve versions from {self.hostname}/listVersions\nError: {e}") def watch_vlist(self, vlist:list[int]) -> None: bb = self.query_one(Horizontal) @@ -231,9 +255,20 @@ class DiffDisplay(Vertical): self.version = None def compose(self) -> ComposeResult: - yield TitleBox('Diff') + yield TitleBox("Diff") yield Vertical(Static(id='diffbox')) + def on_mount(self) -> None: + self.set_interval(0.1, self.update_title) + + def update_title(self) -> None: + if str(self.version): #If we didn't convert to a string, v0 would trigger the else statement + difftext = f"Comparing {str(oldconfname)} (v{oldconfver}) with {self.confname} (v{self.version})" + else: + difftext = f"Comparing {str(oldconfname)} (v{oldconfver}) with..." + title = self.query_one(TitleBox) + title.update(difftext) + async def get_json(self, conf, ver) -> None: self.confname = conf self.version = ver @@ -326,8 +361,8 @@ class ConfViewer(App): '''Saves the current config to a global variable, then pushes the diff screen.''' dis = self.query_one(Display) if dis.confdata != None: - global oldconf - oldconf = dis.confdata + global oldconf, oldconfname, oldconfver + oldconf, oldconfname, oldconfver = dis.confdata, dis.confname, dis.version self.push_screen('diff') def action_flip_versions(self): From 433ea75e542b45a1372017dd72ec4337b41513fb Mon Sep 17 00:00:00 2001 From: Jonathan Hancock Date: Tue, 11 Jul 2023 14:57:44 +0200 Subject: [PATCH 73/90] Bug fix for the diff display, previously it wasn't showing the right message with no second conf selected. --- scripts/daqconf_viewer | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/daqconf_viewer b/scripts/daqconf_viewer index 99659c7b..cb49d1d9 100755 --- a/scripts/daqconf_viewer +++ b/scripts/daqconf_viewer @@ -262,7 +262,7 @@ class DiffDisplay(Vertical): self.set_interval(0.1, self.update_title) def update_title(self) -> None: - if str(self.version): #If we didn't convert to a string, v0 would trigger the else statement + if self.version != None: difftext = f"Comparing {str(oldconfname)} (v{oldconfver}) with {self.confname} (v{self.version})" else: difftext = f"Comparing {str(oldconfname)} (v{oldconfver}) with..." From d44e0d6b7a0a261d838c23083de9b83e22669037 Mon Sep 17 00:00:00 2001 From: Jonathan Hancock Date: Tue, 11 Jul 2023 17:54:08 +0200 Subject: [PATCH 74/90] The user can now browse and compare local files. A new keybind has been added for switching between local and DB configs. --- scripts/daqconf_viewer | 157 +++++++++++++++++++++++++++++++++++++---- 1 file changed, 144 insertions(+), 13 deletions(-) diff --git a/scripts/daqconf_viewer b/scripts/daqconf_viewer index cb49d1d9..670ea12b 100755 --- a/scripts/daqconf_viewer +++ b/scripts/daqconf_viewer @@ -4,6 +4,7 @@ import copy import click import httpx import json +import os import sys from rich.text import Text @@ -14,7 +15,7 @@ from textual import log, events from textual.app import App, ComposeResult from textual.containers import Content, Container, Horizontal, Vertical from textual.widget import Widget -from textual.widgets import Button, Footer, Header, Input, Label, ListItem, ListView, Static, Tree +from textual.widgets import Button, DirectoryTree, Footer, Header, Input, Label, ListItem, ListView, Static, Tree from textual.reactive import reactive, Reactive from textual.screen import Screen @@ -111,6 +112,46 @@ class Configs(Static): s.update(text) break +class LocalConfigs(Static): + conflist = reactive([]) + + def __init__(self, hostname, path, **kwargs): + super().__init__(**kwargs) + self.hostname = hostname + self.path = path + + def compose(self) -> ComposeResult: + yield TitleBox('Configurations') + yield Input(placeholder='Search Configs') + yield DirectoryTree(self.path) + + async def on_directory_tree_file_selected(self, event: DirectoryTree.FileSelected ) -> None: + location = event.path + filename = location.split('/')[-1] + try: + with open(location) as f: + self.current_conf = json.load(f) + #Look for a display to show the config to + for v in self.screen.query(Vertical): + if isinstance(v, Display) or isinstance(v, DiffDisplay): + await v.get_json_local(filename, self.current_conf) + break + except Exception as e: + self.display_error(f"Config at {location} is not usable\n Error: {e}") + + def display_error(self, text): + '''If something goes wrong with getting the configs, we hijack the display to tell the user.''' + for v in self.screen.query(Vertical): + if isinstance(v, Display): + e_json = {'error': text} + v.confdata = e_json + break + if isinstance(v, DiffDisplay): + for s in v.query(Static): + if s.id == 'diffbox': + s.update(text) + break + class Versions(Vertical): vlist = reactive([]) @@ -199,6 +240,11 @@ class Display(Vertical): except: self.confdata = {"error": f"Couldn't retrieve the configuration at {self.hostname}/retrieveVersion (payload: {payload}"} + async def get_json_local(self, name, conf) -> None: + self.confname = name + self.version = -1 + self.confdata = conf + def json_into_tree(cls, node, json_data): """Takes a JSON, and puts it into the tree.""" from rich.highlighter import ReprHighlighter @@ -262,10 +308,14 @@ class DiffDisplay(Vertical): self.set_interval(0.1, self.update_title) def update_title(self) -> None: + #If the config is local, we set the version number to -1 + vold = "(local)" if oldconfver == -1 else f"(v{oldconfver})" + vnew = "(local)" if self.version == -1 else f"(v{self.version})" + if self.version != None: - difftext = f"Comparing {str(oldconfname)} (v{oldconfver}) with {self.confname} (v{self.version})" + difftext = f"Comparing {str(oldconfname)} {vold} with {self.confname} {vnew}" else: - difftext = f"Comparing {str(oldconfname)} (v{oldconfver}) with..." + difftext = f"Comparing {str(oldconfname)} {vold} with..." title = self.query_one(TitleBox) title.update(difftext) @@ -281,6 +331,11 @@ class DiffDisplay(Vertical): except: self.confdata = {"error": f"Couldn't retrieve the configuration at {self.hostname}/retrieveVersion (payload: {payload})"} + async def get_json_local(self, name, conf) -> None: + self.confname = name + self.version = -1 + self.confdata = conf + async def watch_confdata(self, confdata:dict) -> None: '''Turns the jsons into a string format with newlines, then generates a diff of the two.''' if confdata: @@ -319,8 +374,40 @@ class DiffDisplay(Vertical): def on_button_pressed(self) -> None: self.remove() +class LocalDiffScreen(Screen): + BINDINGS = [ + ("l", "switch_local", "DB Files"), + ("d", "end_diff", "Return") + ] + + def __init__(self, hostname, path, **kwargs): + super().__init__(**kwargs) + self.hostname = hostname + self.path = path + + def compose(self) -> ComposeResult: + yield LocalConfigs(hostname=self.hostname, path=self.path, classes='greencontainer configs', id='diffconfigs') + yield Versions(hostname=self.hostname, classes='greencontainer versions', id='diffversions') + yield DiffDisplay(hostname=self.hostname, classes='greencontainer display', id='diffdisplay') + + yield Header(show_clock=True) + yield Footer() + + def action_switch_local(self) -> None: + self.app.pop_screen() + self.app.push_screen('diff') + + def action_end_diff(self) -> None: + self.app.pop_screen() + self.app.push_screen('lconf') + + class DiffScreen(Screen): - BINDINGS = [("d", "app.pop_screen", "Return")] + BINDINGS = [ + ("l", "switch_local", "Local Files"), + ("d", "app.pop_screen", "Return") + ] + def __init__(self, hostname, **kwargs): super().__init__(**kwargs) self.hostname = hostname @@ -333,21 +420,56 @@ class DiffScreen(Screen): yield Header(show_clock=True) yield Footer() + def action_switch_local(self) -> None: + self.app.pop_screen() + self.app.push_screen('ldiff') + + +class LocalConfScreen(Screen): + BINDINGS = [ + ("l", "app.pop_screen", "DB Files"), + ("d", "make_diff", "Diff") + ] + def __init__(self, hostname, path, **kwargs): + super().__init__(**kwargs) + self.hostname = hostname + self.path = path + + def compose(self) -> ComposeResult: + yield LocalConfigs(hostname=self.hostname, path=self.path, classes='redcontainer configs', id='localconfigs') + yield Versions(hostname=self.hostname, classes='redcontainer versions', id='localversions') + yield Display(hostname=self.hostname, classes='redcontainer display', id='localdisplay') + + yield Header(show_clock=True) + yield Footer() + + def action_make_diff(self) -> None: + '''Saves the current config to a global variable, then pushes the (local) diff screen.''' + dis = self.query_one(Display) + if dis.confdata != None: + global oldconf, oldconfname, oldconfver + oldconf, oldconfname, oldconfver = dis.confdata, dis.confname, dis.version + self.app.pop_screen() + self.app.push_screen('ldiff') class ConfViewer(App): CSS_PATH = "daqconf_viewer.css" BINDINGS = [ + ("l", "switch_local", "Local Files"), ("d", "make_diff", "Diff"), + ("v", "flip_versions", "Reverse Version Order"), ("q", "quit", "Quit"), - ("v", "flip_versions", "Reverse Version Order") ] def __init__(self, host, port, dir, **kwargs): super().__init__(**kwargs) self.hostname = f"{host}:{port}" + self.path = dir def on_mount(self) -> None: + self.install_screen(LocalConfScreen(hostname=self.hostname, path=self.path), name="lconf") self.install_screen(DiffScreen(hostname=self.hostname), name="diff") + self.install_screen(LocalDiffScreen(hostname=self.hostname, path=self.path), name="ldiff") def compose(self) -> ComposeResult: yield Configs(hostname=self.hostname, classes='redcontainer configs', id='regconfigs') @@ -357,24 +479,33 @@ class ConfViewer(App): yield Header(show_clock=True) yield Footer() - def action_make_diff(self): + def action_switch_local(self) -> None: + self.push_screen('lconf') + + def action_make_diff(self) -> None: '''Saves the current config to a global variable, then pushes the diff screen.''' - dis = self.query_one(Display) + dis = self.screen.query_one(Display) if dis.confdata != None: global oldconf, oldconfname, oldconfver - oldconf, oldconfname, oldconfver = dis.confdata, dis.confname, dis.version + oldconf, oldconfname, oldconfver = dis.confdata, dis.confname, dis.version self.push_screen('diff') - def action_flip_versions(self): - '''Tells the versions widget to display them the other way around''' - ver = self.screen.query_one(Versions) - ver.reverse = not ver.reverse + def action_flip_versions(self) -> None: + ''' + Tells the versions widget to display them the other way around. + If that widget doesn't exist on this scren, do nothing. + ''' + try: + ver = self.screen.query_one(Versions) + ver.reverse = not ver.reverse + except: + pass CONTEXT_SETTINGS = dict(help_option_names=['-h', '--help']) @click.command(context_settings=CONTEXT_SETTINGS) @click.option('--host', default="http://np04-srv-023", help='Machine hosting the config service') @click.option('--port', default="31011", help='Port that the config service listens on') -@click.option('--dir', default = "", help='Top-level directory to look for local files in') +@click.option('--dir', default = "./", help='Top-level directory to look for local files in') def start(host:str, port:str, dir:str): app = ConfViewer(host, port, dir) app.run() From d14c1c94eed26b4c631018e5bf4de6f50f4fe777 Mon Sep 17 00:00:00 2001 From: Jonathan Hancock Date: Wed, 12 Jul 2023 12:38:04 +0200 Subject: [PATCH 75/90] Removed the version widgets from the local screens. The top node of the file system now displays only the name of the directory (no parent dirs, .., ~, . etc) --- scripts/daqconf_viewer | 36 +++++++++++++++++++++++++----------- 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/scripts/daqconf_viewer b/scripts/daqconf_viewer index 670ea12b..5a402c2f 100755 --- a/scripts/daqconf_viewer +++ b/scripts/daqconf_viewer @@ -4,20 +4,20 @@ import copy import click import httpx import json -import os import sys -from rich.text import Text -from rich.markdown import Markdown from difflib import unified_diff +from pathlib import Path +from rich.markdown import Markdown +from rich.text import Text from textual import log, events from textual.app import App, ComposeResult from textual.containers import Content, Container, Horizontal, Vertical -from textual.widget import Widget -from textual.widgets import Button, DirectoryTree, Footer, Header, Input, Label, ListItem, ListView, Static, Tree from textual.reactive import reactive, Reactive from textual.screen import Screen +from textual.widget import Widget +from textual.widgets import Button, DirectoryTree, Footer, Header, Input, Label, ListItem, ListView, Static, Tree auth = ("fooUsr", "barPass") oldconf = None @@ -112,18 +112,34 @@ class Configs(Static): s.update(text) break +class ShortNodeTree(DirectoryTree): + '''We inherit everything from the dirtree, but we want to abbreviate the top node.''' + def process_label(self, label): + '''If a node is a/b/c, just display c''' + if '/' in label: + good_label = label.split('/')[-1] + else: + good_label = label + if isinstance(good_label, str): + text_label = Text.from_markup(good_label) + else: + text_label = good_label + first_line = text_label.split()[0] + return first_line + class LocalConfigs(Static): conflist = reactive([]) def __init__(self, hostname, path, **kwargs): super().__init__(**kwargs) self.hostname = hostname - self.path = path + path_obj = Path(path) + self.path = str(path_obj.resolve()) def compose(self) -> ComposeResult: yield TitleBox('Configurations') yield Input(placeholder='Search Configs') - yield DirectoryTree(self.path) + yield ShortNodeTree(self.path) async def on_directory_tree_file_selected(self, event: DirectoryTree.FileSelected ) -> None: location = event.path @@ -386,9 +402,8 @@ class LocalDiffScreen(Screen): self.path = path def compose(self) -> ComposeResult: - yield LocalConfigs(hostname=self.hostname, path=self.path, classes='greencontainer configs', id='diffconfigs') - yield Versions(hostname=self.hostname, classes='greencontainer versions', id='diffversions') - yield DiffDisplay(hostname=self.hostname, classes='greencontainer display', id='diffdisplay') + yield LocalConfigs(hostname=self.hostname, path=self.path, classes='greencontainer configs', id='localdiffconfigs') + yield DiffDisplay(hostname=self.hostname, classes='greencontainer display', id='localdiffdisplay') yield Header(show_clock=True) yield Footer() @@ -437,7 +452,6 @@ class LocalConfScreen(Screen): def compose(self) -> ComposeResult: yield LocalConfigs(hostname=self.hostname, path=self.path, classes='redcontainer configs', id='localconfigs') - yield Versions(hostname=self.hostname, classes='redcontainer versions', id='localversions') yield Display(hostname=self.hostname, classes='redcontainer display', id='localdisplay') yield Header(show_clock=True) From 6a31cae13edf7aab4b0ba355f101b789665aefd0 Mon Sep 17 00:00:00 2001 From: John Christian Freeman Date: Wed, 12 Jul 2023 17:46:27 +0200 Subject: [PATCH 76/90] JCF: as discussed with Alessandro and Roland this morning, update readout_gen.py to ensure the dpdklibs-based stats reporting configuration (w/ default values) is added to the overall configuration) --- python/daqconf/apps/readout_gen.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/python/daqconf/apps/readout_gen.py b/python/daqconf/apps/readout_gen.py index 46ac1a4e..214cf1e5 100644 --- a/python/daqconf/apps/readout_gen.py +++ b/python/daqconf/apps/readout_gen.py @@ -177,7 +177,8 @@ def build_conf(self, eal_arg_list, lcores_id_set): nrc.Interface( ip_addr=rx_ip, mac_addr=rx_mac, - expected_sources=srcs + expected_sources=srcs, + stats_reporting_cfg=nrc.StatsReporting() ) ) From 33fd41561793d1af8d602600a74743b9b5c11312 Mon Sep 17 00:00:00 2001 From: Jonathan Hancock <56547447+JonathanHancock0@users.noreply.github.com> Date: Thu, 13 Jul 2023 13:43:30 +0200 Subject: [PATCH 77/90] Replaced the screenshot with a new one --- docs/ConfViewerScreenshot.png | Bin 81624 -> 68825 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/docs/ConfViewerScreenshot.png b/docs/ConfViewerScreenshot.png index 32383d3698f1edf370a491d2e30f7d8793b27d17..08680d4f5f1796e03424d815ab01afc1a37635b0 100644 GIT binary patch literal 68825 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 From aa48c69f4e11c146f0c13937c13527959cd874e9 Mon Sep 17 00:00:00 2001 From: Jonathan Hancock <56547447+JonathanHancock0@users.noreply.github.com> Date: Thu, 13 Jul 2023 14:10:33 +0200 Subject: [PATCH 78/90] Update ConfigDatabase.md Added descriptions for all the new features, and the command line arguments. --- docs/ConfigDatabase.md | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/docs/ConfigDatabase.md b/docs/ConfigDatabase.md index 5aa06122..54b3d017 100644 --- a/docs/ConfigDatabase.md +++ b/docs/ConfigDatabase.md @@ -9,13 +9,22 @@ _nanorc_ should then be started with `nanorc --pm k8s://np04-srv-015:31000 db:// Keep in mind that the config directory can contain underscores, but the name it will be given in the database cannot (hyphens are fine). ## Viewing configurations -To inspect the contents of the database, run `daqconf_viewer` after setting up the software environment. This will open a graphical UI in the terminal. +To inspect the contents of the database, run `daqconf_viewer` after setting up the software environment. This will open a graphical UI in the terminal. There are three optional arguments that can be provided: +* --host to manually enter the host of the microservice (defaults to http://np04-srv-023) +* --port to manually enter the port that the service listens on (defaults to 31011) +* --dir to tell the config viewer where to look for local config files (defaults to ./) ![Config Viewer](ConfViewerScreenshot.png) -A list of all configuration names is shown on the left. -Clicking one of them will generate a list of buttons at the top, corresponding to the saved versions of the config. -Press one of those buttons to bring up the config file in the display box. By clicking the arrows, the contents of each sub-schema can be expanded. + +A list of all configuration names is shown on the left. It can be filtered using the search bar above it. +Clicking one of these names will generate a list of buttons at the top, corresponding to the saved versions of the config. +The versions are displayed in descending order by default, but this can be changed with the V key. +Click one of the buttons to bring up the config file in the display box. By clicking the arrows, the contents of each sub-schema can be expanded. + +Additionally, you can press the L key to switch between viewing the database and viewing local files. The config list will be replaced with a tree representing the contents of the directory given with --dir. +![Local Configs](ConfViewerLocalScreenshot.png) Pressing the D key after picking a config will take you to a very similar screen, albeit with green lines instead of red. If a second config is selected using the previously defined process, then a "diff" of the two will be generated, showing all the differences between the two in a format similar to how commits are displayed on github. +Again, the L key can be used to switch to local files, allowing for comparisons of local and DB configs in any combination. Finally, once you are done press q to quit (or use ctrl+c). From c0b015740ccc78cdf1eae2f701b800c6d0ee6561 Mon Sep 17 00:00:00 2001 From: Jonathan Hancock <56547447+JonathanHancock0@users.noreply.github.com> Date: Thu, 13 Jul 2023 14:12:06 +0200 Subject: [PATCH 79/90] Added a screenshot of browsing the local files. --- docs/ConfViewerLocalScreenshot.png | Bin 0 -> 35726 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 docs/ConfViewerLocalScreenshot.png diff --git a/docs/ConfViewerLocalScreenshot.png b/docs/ConfViewerLocalScreenshot.png new file mode 100644 index 0000000000000000000000000000000000000000..c84d43d217ff719fe9c4270af73b85276a629f44 GIT binary patch literal 35726 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 P9q0V7M%sC2FW>u5gZ)L2 literal 0 HcmV?d00001 From 324bab97745dcf785fbe446a69c22bdaf16a5eae Mon Sep 17 00:00:00 2001 From: John Freeman Date: Thu, 13 Jul 2023 08:09:50 -0500 Subject: [PATCH 80/90] JCF: bump tag for next candidate release; includes bugfix from PR #369 --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 995d3e1e..2c07cd58 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,5 @@ cmake_minimum_required(VERSION 3.12) -project(daqconf VERSION 6.0.1) +project(daqconf VERSION 6.0.2) find_package(daq-cmake REQUIRED ) From 8abcb12260cbe266ff4e206f8263a2424223b702 Mon Sep 17 00:00:00 2001 From: Jonathan Hancock <56547447+JonathanHancock0@users.noreply.github.com> Date: Thu, 13 Jul 2023 15:10:27 +0200 Subject: [PATCH 81/90] Update README.md Added a link to the conf viewer documentation on the main readme, so that new users can find out about it. --- docs/README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/README.md b/docs/README.md index ae66f4cd..98667c91 100644 --- a/docs/README.md +++ b/docs/README.md @@ -6,10 +6,14 @@ The focus of this documentation is on providing instructions for using the tools [Instructions for casual or first-time users](InstructionsForCasualUsers.md) -and for a slightly more in-depth look into how to generate configurations for a DAQ system, take a look at: +For a slightly more in-depth look into how to generate configurations for a DAQ system, take a look at: [Configuration options for casual or first-time users](ConfigurationsForCasualUsers.md) +If you want to view existing configs stored in the MongoDB, take a look at: + +[Interacting with the Configuration Database](ConfigDatabase.md) + Traditionally multiple command line options were passed to `daqconf_multiru_gen` in order to control how it generated configurations. However, for the `dunedaq-v3.2.0` release (September 2022) we're switching to passing a single JSON file whose contents contain the information needed to control `daqconf_multiru_gen`. For `daqconf_multiru_gen` users who want to learn about how to make the switch to this new approach, take a look at [these migration instructions](MigratingToNewConfgen.md). Finally, here's nice visual representation of the type of DAQ system which can be configured: From 32a588866f7b3991e8c467396372702e93bd7d57 Mon Sep 17 00:00:00 2001 From: Pierre Lasorak Date: Thu, 13 Jul 2023 15:29:37 +0200 Subject: [PATCH 82/90] get rid of the entrypoint --- python/daqconf/core/conf_utils.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/python/daqconf/core/conf_utils.py b/python/daqconf/core/conf_utils.py index b141ee3f..5b8f4f50 100644 --- a/python/daqconf/core/conf_utils.py +++ b/python/daqconf/core/conf_utils.py @@ -531,10 +531,10 @@ def add_k8s_app_boot_data( boot_data.update({"apps": apps_desc}) boot_data.update({"order": boot_order}) - if 'rte_script' in boot_data: - boot_data['exec']['daq_application_k8s']['cmd'] = ['daq_application'] - else: - boot_data['exec']['daq_application_k8s']['cmd'] = ['/dunedaq/run/app-entrypoint.sh'] + # if 'rte_script' in boot_data: + # boot_data['exec']['daq_application_k8s']['cmd'] = ['daq_application'] + # else: + # boot_data['exec']['daq_application_k8s']['cmd'] = ['/dunedaq/run/app-entrypoint.sh'] boot_data["exec"]["daq_application_k8s"]["image"] = image @@ -604,7 +604,7 @@ def generate_boot( app_env.update({ p:'getenv' for p in capture_paths }) - + app_env.update({ v:'getenv' for v in boot_conf.capture_env_vars }) From daa46f78a4451fdcba2f21e827c46b56cc0bf781 Mon Sep 17 00:00:00 2001 From: Pierre Lasorak Date: Thu, 13 Jul 2023 16:17:54 +0200 Subject: [PATCH 83/90] get rid of obscure rte argument --- python/daqconf/core/conf_utils.py | 33 +++++++++++++++++-------------- schema/daqconf/bootgen.jsonnet | 3 --- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/python/daqconf/core/conf_utils.py b/python/daqconf/core/conf_utils.py index 5b8f4f50..0bfb60e1 100644 --- a/python/daqconf/core/conf_utils.py +++ b/python/daqconf/core/conf_utils.py @@ -650,17 +650,17 @@ def generate_boot( if boot_conf.disable_trace: del boot["exec"][daq_app_exec_name]["env"]["TRACE_FILE"] + boot['rte_script'] = get_rte_script() + # match boot_conf.k8s_rte: + # case 'auto': + # if (release_or_dev() == 'rel'): + # boot['rte_script'] = get_rte_script() - match boot_conf.k8s_rte: - case 'auto': - if (release_or_dev() == 'rel'): - boot['rte_script'] = get_rte_script() + # case 'release': + # boot['rte_script'] = get_rte_script() - case 'release': - boot['rte_script'] = get_rte_script() - - case 'devarea': - pass + # case 'devarea': + # pass @@ -869,14 +869,17 @@ def release_or_dev(): return 'rel' def get_rte_script(): - from os import path - - ver = get_version() - releases_dir = get_releases_dir() + from os import path,getenv + script = '' + if release_or_dev() == 'rel': + ver = get_version() + releases_dir = get_releases_dir() + script = path.join(releases_dir, ver, 'daq_app_rte.sh') - script = path.join(releases_dir, ver, 'daq_app_rte.sh') + else: + dbt_install_dir = getenv('DBT_INSTALL_DIR') + script = path.join(dbt_install_dir, 'daq_app_rte.sh') if not path.exists(script): raise RuntimeError(f'Couldn\'t understand where to find the rte script tentative: {script}') - return script diff --git a/schema/daqconf/bootgen.jsonnet b/schema/daqconf/bootgen.jsonnet index ec548826..ad3e9aab 100644 --- a/schema/daqconf/bootgen.jsonnet +++ b/schema/daqconf/bootgen.jsonnet @@ -12,8 +12,6 @@ local nc = moo.oschema.numeric_constraints; local cs = { monitoring_dest: s.enum( "MonitoringDest", ["local", "cern", "pocket"]), pm_choice: s.enum( "PMChoice", ["k8s", "ssh"], doc="Process Manager choice: ssh or Kubernetes"), - rte_choice: s.enum( "RTEChoice", ["auto", "release", "devarea"], doc="Kubernetes DAQ application RTE choice"), - boot: s.record("boot", [ @@ -30,7 +28,6 @@ local cs = { # K8S s.field( "k8s_image", types.string, default="dunedaq/c8-minimal", doc="Which docker image to use"), - s.field( "k8s_rte", self.rte_choice, default="auto", doc="0 - Use an RTE script if not in a dev environment, 1 - Always use RTE, 2 - never use RTE"), # Connectivity Service s.field( "use_connectivity_service", types.flag, default=true, doc="Whether to use the ConnectivityService to manage connections"), From 86dfe70aed65351fa0019d7a74d73ef1083ae68a Mon Sep 17 00:00:00 2001 From: Jonathan Hancock Date: Wed, 19 Jul 2023 14:50:56 +0200 Subject: [PATCH 84/90] Adding widgets for run registry screen --- scripts/daqconf_viewer | 67 ++++++++++++++++++++++++++++++++------ scripts/daqconf_viewer.css | 4 +++ 2 files changed, 61 insertions(+), 10 deletions(-) diff --git a/scripts/daqconf_viewer b/scripts/daqconf_viewer index 5a402c2f..99f7565a 100755 --- a/scripts/daqconf_viewer +++ b/scripts/daqconf_viewer @@ -390,6 +390,32 @@ class DiffDisplay(Vertical): def on_button_pressed(self) -> None: self.remove() +class RunSelection(Vertical): + def __init__(self, hostname, **kwargs): + super().__init__(**kwargs) + self.hostname = hostname + + def compose(self) -> ComposeResult: + yield Input(placeholder="Enter a run number") + yield Button("Get Data, "variant='primary') + + async def on_button_pressed (self, event: Button.Pressed) -> None: + box = self.query_one(Input) + number = box.value + try: + async with httpx.AsyncClient() as client: + r1 = await client.get(f'{self.hostname}/getRunMeta/{number}', auth=auth, timeout=5) + self.rundata = r1.json() + except: + pass + try: + async with httpx.AsyncClient() as client: + r2 = await client.get(f'{self.hostname}/getRunBlob/{number}', auth=auth, timeout=5) + #Download and extract to a temporary directory + + +class RunInfo() + class LocalDiffScreen(Screen): BINDINGS = [ ("l", "switch_local", "DB Files"), @@ -466,25 +492,41 @@ class LocalConfScreen(Screen): self.app.pop_screen() self.app.push_screen('ldiff') +class RunRegistryScreen(Screen): + BINDINGS = [("r", "app.pop_screen", "Return")] + + def __init__(self, hostname, path, **kwargs): + super().__init__(**kwargs) + self.hostname = hostname + + def compose(self) -> ComposeResult: + yield RunSelection(hostname=self.hostname, classes='orangecontainer' id="runselect") + yield RunInfo(classes='orangecontainer' id="runinfo") + #yield LocalConfigs(hostname=self.hostname, path=AAAAAAAAAA, classes='orangecontainer configs', id='regconfigs') + #yield Display(hostname=self.hostname, classes='orangecontainer display', id='regdisplay') + + class ConfViewer(App): CSS_PATH = "daqconf_viewer.css" BINDINGS = [ ("l", "switch_local", "Local Files"), ("d", "make_diff", "Diff"), ("v", "flip_versions", "Reverse Version Order"), + ("r", "run_reg", "Display Run Registry"), ("q", "quit", "Quit"), ] - def __init__(self, host, port, dir, **kwargs): + def __init__(self, chost, cport, rhost, rport, dir, **kwargs): super().__init__(**kwargs) - self.hostname = f"{host}:{port}" + self.confhost = f"{chost}:{cport}" + self.reghost = f"{rhost}:{rport}" self.path = dir def on_mount(self) -> None: - self.install_screen(LocalConfScreen(hostname=self.hostname, path=self.path), name="lconf") - self.install_screen(DiffScreen(hostname=self.hostname), name="diff") - self.install_screen(LocalDiffScreen(hostname=self.hostname, path=self.path), name="ldiff") - + self.install_screen(LocalConfScreen(hostname=self.confhost, path=self.path), name="lconf") + self.install_screen(DiffScreen(hostname=self.conf), name="diff") + self.install_screen(LocalDiffScreen(hostname=self.confhost, path=self.path), name="ldiff") + self.install_screen(RunRegistryScreen(hostmane=self.reghost), name="runreg") def compose(self) -> ComposeResult: yield Configs(hostname=self.hostname, classes='redcontainer configs', id='regconfigs') yield Versions(hostname=self.hostname, classes='redcontainer versions', id='regversions') @@ -515,13 +557,18 @@ class ConfViewer(App): except: pass + def action_run_reg(self) -> None: + self.push_screen('runreg') + CONTEXT_SETTINGS = dict(help_option_names=['-h', '--help']) @click.command(context_settings=CONTEXT_SETTINGS) -@click.option('--host', default="http://np04-srv-023", help='Machine hosting the config service') -@click.option('--port', default="31011", help='Port that the config service listens on') -@click.option('--dir', default = "./", help='Top-level directory to look for local files in') +@click.option('--conf-host', default="http://np04-srv-023", help='Machine hosting the config service') +@click.option('--conf-port', default="31011", help='Port that the config service listens on') +@click.option('--reg-host', default="http://dunedaq-microservices.cern.ch", help='Machine hosting the run registry service') +@click.option('--reg-port', default="5005", help='Port that the run registry service listens on') +@click.option('--dir', default = "./", help='Top-level directory to look for local config files in') def start(host:str, port:str, dir:str): - app = ConfViewer(host, port, dir) + app = ConfViewer(conf-host, conf-port, reg-host, reg-port, dir) app.run() if __name__ == "__main__": diff --git a/scripts/daqconf_viewer.css b/scripts/daqconf_viewer.css index 3ad1c36f..cd2265fb 100644 --- a/scripts/daqconf_viewer.css +++ b/scripts/daqconf_viewer.css @@ -51,6 +51,10 @@ Screen { border: wide red; } +.orangecontainer{ + border: wide orange; +} + .greencontainer{ border: wide green; } From 3a01e95b96e5c50110e0a0fde4a54081b92f592a Mon Sep 17 00:00:00 2001 From: Jonathan Hancock Date: Fri, 21 Jul 2023 16:04:25 +0200 Subject: [PATCH 85/90] A run can now be selected, and the metadata/configurations viewed by pressing the button. --- scripts/daqconf_viewer | 151 ++++++++++++++++++++++++++++++------- scripts/daqconf_viewer.css | 25 ++++++ 2 files changed, 147 insertions(+), 29 deletions(-) diff --git a/scripts/daqconf_viewer b/scripts/daqconf_viewer index 99f7565a..a69fb167 100755 --- a/scripts/daqconf_viewer +++ b/scripts/daqconf_viewer @@ -3,8 +3,12 @@ import asyncio import copy import click import httpx +import io import json +import os import sys +import tarfile +import tempfile from difflib import unified_diff from pathlib import Path @@ -23,6 +27,7 @@ auth = ("fooUsr", "barPass") oldconf = None oldconfname = None oldconfver = None +dir_object = None class TitleBox(Static): def __init__(self, title, **kwargs): @@ -138,7 +143,6 @@ class LocalConfigs(Static): def compose(self) -> ComposeResult: yield TitleBox('Configurations') - yield Input(placeholder='Search Configs') yield ShortNodeTree(self.path) async def on_directory_tree_file_selected(self, event: DirectoryTree.FileSelected ) -> None: @@ -168,6 +172,55 @@ class LocalConfigs(Static): s.update(text) break +class RegistryConfigs(Static): + conflist = reactive([]) + + def __init__(self, hostname, **kwargs): + super().__init__(**kwargs) + self.hostname = hostname + self.path = None + + def compose(self) -> ComposeResult: + yield TitleBox('Configurations') + + def new_directory(self, path) -> None: + path_obj = Path(path) + self.path = str(path_obj.resolve()) + for p in self.query(ShortNodeTree): #Delete any existing file trees + p.remove() + self.mount(ShortNodeTree(self.path)) + + async def on_directory_tree_file_selected(self, event: DirectoryTree.FileSelected ) -> None: + location = event.path + filename = location.split('/')[-1] + try: + with open(location) as f: + self.current_conf = json.load(f) + #Look for a display to show the config to + for v in self.screen.query(Vertical): + if isinstance(v, Display) or isinstance(v, DiffDisplay): + await v.get_json_local(filename, self.current_conf) + break + except Exception as e: + self.display_error(f"Config at {location} is not usable\n Error: {e}") + + def clear(self): + for p in self.query(ShortNodeTree): + p.remove() + + def display_error(self, text): + '''If something goes wrong with getting the configs, we hijack the display to tell the user.''' + for v in self.screen.query(Vertical): + if isinstance(v, Display): + e_json = {'error': text} + v.confdata = e_json + break + if isinstance(v, DiffDisplay): + for s in v.query(Static): + if s.id == 'diffbox': + s.update(text) + break + class Versions(Vertical): vlist = reactive([]) @@ -297,9 +350,9 @@ class Display(Vertical): add_node("", node, json_data) def watch_confdata(self, confdata:dict) -> None: + tree = self.query_one(Tree) + tree.clear() if confdata: - tree = self.query_one(Tree) - tree.clear() self.json_into_tree(tree.root, confdata) tree.root.expand() @@ -397,24 +450,59 @@ class RunSelection(Vertical): def compose(self) -> ComposeResult: yield Input(placeholder="Enter a run number") - yield Button("Get Data, "variant='primary') + yield Button("Get Data", variant='primary') async def on_button_pressed (self, event: Button.Pressed) -> None: box = self.query_one(Input) number = box.value + + async with httpx.AsyncClient() as client: + route = f'{self.hostname}/runregistry/getRunMeta/{number}' + r1 = await client.get(route , auth=auth, timeout=5) + r2 = await client.get(f'{self.hostname}/runregistry/getRunBlob/{number}', auth=auth, timeout=5) + runmeta = r1.json() #Format is [[headers], [[row]]], but that's still a JSON + headers = runmeta[0] try: - async with httpx.AsyncClient() as client: - r1 = await client.get(f'{self.hostname}/getRunMeta/{number}', auth=auth, timeout=5) - self.rundata = r1.json() + data = runmeta[1][0] #We will assume we only get one row at once (true for this sort of query) except: - pass - try: - async with httpx.AsyncClient() as client: - r2 = await client.get(f'{self.hostname}/getRunBlob/{number}', auth=auth, timeout=5) - #Download and extract to a temporary directory - + data = None + info = self.screen.query_one(RunInfo) + info.update(headers, data) + + rc = self.screen.query_one(RegistryConfigs) + if r2.status_code == 500: + rc.clear() + dis = self.screen.query_one(Display) + dis.confdata = None + return + + f = tempfile.NamedTemporaryFile(mode="w+b",suffix='.tar.gz', delete=False) + f.write(r2.content) + fname = f.name + f.close() + + global dir_object #This is a global variable, since otherwise garbage collection deletes the directory! + dir_object = tempfile.TemporaryDirectory() + temp_name = dir_object.name + tar = tarfile.open(fname, "r:gz") + tar.extractall(temp_name) + tar.close() + os.unlink(f.name) + + rc.new_directory(temp_name) + + +class RunInfo(Static): + def update(self, head, row): + text = "" + if row: + for i, val in enumerate(row): + text += f"{head[i]}: {val}\n" + else: + text = '\n'.join([h+':' for h in head]) + text.rstrip() + super().update(text) -class RunInfo() class LocalDiffScreen(Screen): BINDINGS = [ @@ -456,7 +544,7 @@ class DiffScreen(Screen): def compose(self) -> ComposeResult: yield Configs(hostname=self.hostname, classes='greencontainer configs', id='diffconfigs') yield Versions(hostname=self.hostname, classes='greencontainer versions', id='diffversions') - yield DiffDisplay(hostname=self.hostname, classes='greencontainer display', id='diffdisplay') + yield DiffDisplay(hostname=self.hostname, classes='greencontainer smalldisplay', id='diffdisplay') yield Header(show_clock=True) yield Footer() @@ -495,17 +583,21 @@ class LocalConfScreen(Screen): class RunRegistryScreen(Screen): BINDINGS = [("r", "app.pop_screen", "Return")] - def __init__(self, hostname, path, **kwargs): + def __init__(self, hostname, **kwargs): super().__init__(**kwargs) self.hostname = hostname - def compose(self) -> ComposeResult: - yield RunSelection(hostname=self.hostname, classes='orangecontainer' id="runselect") - yield RunInfo(classes='orangecontainer' id="runinfo") - #yield LocalConfigs(hostname=self.hostname, path=AAAAAAAAAA, classes='orangecontainer configs', id='regconfigs') - #yield Display(hostname=self.hostname, classes='orangecontainer display', id='regdisplay') + def compose(self) -> ComposeResult: + yield RunSelection(hostname=self.hostname, classes='orangecontainer', id="runselect") + yield RunInfo(classes='orangecontainer', id="runinfo") + yield RegistryConfigs(hostname=self.hostname, classes='orangecontainer shortconfigs', id='regconfigs') + yield Display(hostname=self.hostname, classes='orangecontainer smalldisplay', id='regdisplay') + yield Header(show_clock=True) + yield Footer() +#TODO Make bindings not show on screens that don't need them +#TODO Make the app not send a message about cleaning up tmpdirs when it closes class ConfViewer(App): CSS_PATH = "daqconf_viewer.css" BINDINGS = [ @@ -513,7 +605,7 @@ class ConfViewer(App): ("d", "make_diff", "Diff"), ("v", "flip_versions", "Reverse Version Order"), ("r", "run_reg", "Display Run Registry"), - ("q", "quit", "Quit"), + ("q", "quit", "Quit") ] def __init__(self, chost, cport, rhost, rport, dir, **kwargs): @@ -524,13 +616,14 @@ class ConfViewer(App): def on_mount(self) -> None: self.install_screen(LocalConfScreen(hostname=self.confhost, path=self.path), name="lconf") - self.install_screen(DiffScreen(hostname=self.conf), name="diff") + self.install_screen(DiffScreen(hostname=self.confhost), name="diff") self.install_screen(LocalDiffScreen(hostname=self.confhost, path=self.path), name="ldiff") - self.install_screen(RunRegistryScreen(hostmane=self.reghost), name="runreg") + self.install_screen(RunRegistryScreen(hostname=self.reghost), name="runreg") + def compose(self) -> ComposeResult: - yield Configs(hostname=self.hostname, classes='redcontainer configs', id='regconfigs') - yield Versions(hostname=self.hostname, classes='redcontainer versions', id='regversions') - yield Display(hostname=self.hostname, classes='redcontainer display', id='regdisplay') + yield Configs(hostname=self.confhost, classes='redcontainer configs', id='configs') + yield Versions(hostname=self.confhost, classes='redcontainer versions', id='versions') + yield Display(hostname=self.confhost, classes='redcontainer smalldisplay', id='display') yield Header(show_clock=True) yield Footer() @@ -567,8 +660,8 @@ CONTEXT_SETTINGS = dict(help_option_names=['-h', '--help']) @click.option('--reg-host', default="http://dunedaq-microservices.cern.ch", help='Machine hosting the run registry service') @click.option('--reg-port', default="5005", help='Port that the run registry service listens on') @click.option('--dir', default = "./", help='Top-level directory to look for local config files in') -def start(host:str, port:str, dir:str): - app = ConfViewer(conf-host, conf-port, reg-host, reg-port, dir) +def start(conf_host:str, conf_port:str, reg_host:str, reg_port:str, dir:str): + app = ConfViewer(conf_host, conf_port, reg_host, reg_port, dir) app.run() if __name__ == "__main__": diff --git a/scripts/daqconf_viewer.css b/scripts/daqconf_viewer.css index cd2265fb..60deecee 100644 --- a/scripts/daqconf_viewer.css +++ b/scripts/daqconf_viewer.css @@ -7,6 +7,18 @@ Screen { height: 100%; } +RunSelection { + row-span: 1; + column-span: 2; + height: 100%; +} + +RunInfo { + row-span: 1; + column-span: 2; + height: 100%; +} + #buttonbox { overflow-x: auto; } @@ -33,6 +45,12 @@ Screen { height: 100%; } +.shortconfigs { + row-span: 3; + column-span: 1; + height: 100%; +} + .versions { row-span: 1; column-span: 3; @@ -47,6 +65,13 @@ Screen { align-horizontal: center; } +.smalldisplay { + row-span: 3; + column-span: 3; + height: 100%; + align-horizontal: center; +} + .redcontainer{ border: wide red; } From 797673a737bb3099fae36e1b9ff06442f98c09ac Mon Sep 17 00:00:00 2001 From: Jonathan Hancock Date: Mon, 24 Jul 2023 16:11:10 +0200 Subject: [PATCH 86/90] Added buttons to move to the next/previous run Screens now only have the key bindings that they need --- scripts/daqconf_viewer | 180 +++++++++++++++++++++++++++---------- scripts/daqconf_viewer.css | 4 + 2 files changed, 136 insertions(+), 48 deletions(-) diff --git a/scripts/daqconf_viewer b/scripts/daqconf_viewer index a69fb167..7589373b 100755 --- a/scripts/daqconf_viewer +++ b/scripts/daqconf_viewer @@ -17,6 +17,7 @@ from rich.text import Text from textual import log, events from textual.app import App, ComposeResult +from textual.binding import Binding from textual.containers import Content, Container, Horizontal, Vertical from textual.reactive import reactive, Reactive from textual.screen import Screen @@ -447,19 +448,74 @@ class RunSelection(Vertical): def __init__(self, hostname, **kwargs): super().__init__(**kwargs) self.hostname = hostname + self.current = None def compose(self) -> ComposeResult: + yield TitleBox("Run Number") yield Input(placeholder="Enter a run number") - yield Button("Get Data", variant='primary') + yield Horizontal ( + Button("<--", id="back", variant='primary'), + Button("Get Data", id="get", variant='primary'), + Button("-->", id="forward", variant='primary'), + classes = "runbuttons" + ) + async def on_mount(self) -> None: + async with httpx.AsyncClient() as client: + route = f'{self.hostname}/runregistry/getRunMetaLast/1' + r = await client.get(route, auth=auth, timeout=5) + runmeta = r.json() + headers = runmeta[0] + try: + data = runmeta[1][0] + await self.show_data("get", data[0]) + except: + self.display_error("No data about most recent run") - async def on_button_pressed (self, event: Button.Pressed) -> None: + async def on_button_pressed(self, event: Button.Pressed) -> None: + button_id = event.button.id + await self.show_data(button_id) + + async def show_data(self, button_id, number=None): box = self.query_one(Input) - number = box.value + if number == None: + number = box.value + else: + box.value = str(number) + + match button_id: + case "get": + if number == "": #If no number has been entered, then we should do nothing. + self.display_error("Please enter a run number!") + return + try: + number = int(number) + except: + self.display_error("Run number must be an integer") + return + if number < 1: + self.display_error("Run numbers start at 1!") + return + self.current = number + + case "back": + if self.current == 1 or self.current == None: #If we are at the start, the back button does nothing + return + self.current -= 1 + number = self.current + box.value = str(number) + + case "forward": + if self.current == None: + return + self.current += 1 + number = self.current + box.value = str(number) async with httpx.AsyncClient() as client: - route = f'{self.hostname}/runregistry/getRunMeta/{number}' - r1 = await client.get(route , auth=auth, timeout=5) - r2 = await client.get(f'{self.hostname}/runregistry/getRunBlob/{number}', auth=auth, timeout=5) + route1 = f'{self.hostname}/runregistry/getRunMeta/{number}' + route2 = f'{self.hostname}/runregistry/getRunBlob/{number}' + r1 = await client.get(route1, auth=auth, timeout=5) + r2 = await client.get(route2, auth=auth, timeout=5) runmeta = r1.json() #Format is [[headers], [[row]]], but that's still a JSON headers = runmeta[0] try: @@ -472,10 +528,12 @@ class RunSelection(Vertical): rc = self.screen.query_one(RegistryConfigs) if r2.status_code == 500: rc.clear() - dis = self.screen.query_one(Display) - dis.confdata = None + self.display_error(f"No config data found for run {number}") return + dis = self.screen.query_one(Display) + dis.confdata = None + f = tempfile.NamedTemporaryFile(mode="w+b",suffix='.tar.gz', delete=False) f.write(r2.content) fname = f.name @@ -490,9 +548,21 @@ class RunSelection(Vertical): os.unlink(f.name) rc.new_directory(temp_name) + self.current = int(number) + def display_error(self, text): + '''If something goes wrong with getting the configs, we hijack the display to tell the user.''' + for v in self.screen.query(Vertical): + if isinstance(v, Display): + e_json = {'error': text} + v.confdata = e_json + break + +class RunInfo(Vertical): + def compose(self) -> ComposeResult: + yield TitleBox("Run Metadata") + yield Static(id='md') -class RunInfo(Static): def update(self, head, row): text = "" if row: @@ -501,7 +571,10 @@ class RunInfo(Static): else: text = '\n'.join([h+':' for h in head]) text.rstrip() - super().update(text) + for s in self.query(Static): + if s.id == "md": + s.update(text) + break class LocalDiffScreen(Screen): @@ -580,45 +653,17 @@ class LocalConfScreen(Screen): self.app.pop_screen() self.app.push_screen('ldiff') -class RunRegistryScreen(Screen): - BINDINGS = [("r", "app.pop_screen", "Return")] - - def __init__(self, hostname, **kwargs): - super().__init__(**kwargs) - self.hostname = hostname - - def compose(self) -> ComposeResult: - yield RunSelection(hostname=self.hostname, classes='orangecontainer', id="runselect") - yield RunInfo(classes='orangecontainer', id="runinfo") - yield RegistryConfigs(hostname=self.hostname, classes='orangecontainer shortconfigs', id='regconfigs') - yield Display(hostname=self.hostname, classes='orangecontainer smalldisplay', id='regdisplay') - - yield Header(show_clock=True) - yield Footer() - -#TODO Make bindings not show on screens that don't need them -#TODO Make the app not send a message about cleaning up tmpdirs when it closes -class ConfViewer(App): - CSS_PATH = "daqconf_viewer.css" +class BaseScreen(Screen): BINDINGS = [ - ("l", "switch_local", "Local Files"), - ("d", "make_diff", "Diff"), - ("v", "flip_versions", "Reverse Version Order"), - ("r", "run_reg", "Display Run Registry"), - ("q", "quit", "Quit") + ("l", "switch_local", "Local Files"), + ("d", "make_diff", "Diff"), + ("v", "flip_versions", "Reverse Version Order"), + ("r", "run_reg", "Display Run Registry"), ] - def __init__(self, chost, cport, rhost, rport, dir, **kwargs): + def __init__(self, hostname, **kwargs): super().__init__(**kwargs) - self.confhost = f"{chost}:{cport}" - self.reghost = f"{rhost}:{rport}" - self.path = dir - - def on_mount(self) -> None: - self.install_screen(LocalConfScreen(hostname=self.confhost, path=self.path), name="lconf") - self.install_screen(DiffScreen(hostname=self.confhost), name="diff") - self.install_screen(LocalDiffScreen(hostname=self.confhost, path=self.path), name="ldiff") - self.install_screen(RunRegistryScreen(hostname=self.reghost), name="runreg") + self.confhost = hostname def compose(self) -> ComposeResult: yield Configs(hostname=self.confhost, classes='redcontainer configs', id='configs') @@ -629,7 +674,7 @@ class ConfViewer(App): yield Footer() def action_switch_local(self) -> None: - self.push_screen('lconf') + self.app.push_screen('lconf') def action_make_diff(self) -> None: '''Saves the current config to a global variable, then pushes the diff screen.''' @@ -637,7 +682,7 @@ class ConfViewer(App): if dis.confdata != None: global oldconf, oldconfname, oldconfver oldconf, oldconfname, oldconfver = dis.confdata, dis.confname, dis.version - self.push_screen('diff') + self.app.push_screen('diff') def action_flip_versions(self) -> None: ''' @@ -651,7 +696,46 @@ class ConfViewer(App): pass def action_run_reg(self) -> None: - self.push_screen('runreg') + self.app.push_screen('runreg') + +class RunRegistryScreen(Screen): + BINDINGS = [("r", "app.pop_screen", "Return")] + + def __init__(self, hostname, **kwargs): + super().__init__(**kwargs) + self.hostname = hostname + + def compose(self) -> ComposeResult: + yield RunSelection(hostname=self.hostname, classes='orangecontainer', id="runselect") + yield RunInfo(classes='orangecontainer', id="runinfo") + yield RegistryConfigs(hostname=self.hostname, classes='orangecontainer shortconfigs', id='regconfigs') + yield Display(hostname=self.hostname, classes='orangecontainer smalldisplay', id='regdisplay') + + yield Header(show_clock=True) + yield Footer() + + def nothing(self) -> None: + '''This function doesn't do anything, the fake bindings call it.''' + pass + +class ConfViewer(App): + CSS_PATH = "daqconf_viewer.css" + BINDINGS = [("q", "quit", "Quit")] + + def __init__(self, chost, cport, rhost, rport, dir, **kwargs): + super().__init__(**kwargs) + self.confhost = f"{chost}:{cport}" + self.reghost = f"{rhost}:{rport}" + self.path = dir + + def on_mount(self) -> None: + self.install_screen(BaseScreen(hostname=self.confhost), name="base") + self.install_screen(LocalConfScreen(hostname=self.confhost, path=self.path), name="lconf") + self.install_screen(DiffScreen(hostname=self.confhost), name="diff") + self.install_screen(LocalDiffScreen(hostname=self.confhost, path=self.path), name="ldiff") + self.install_screen(RunRegistryScreen(hostname=self.reghost), name="runreg") + self.push_screen("base") + CONTEXT_SETTINGS = dict(help_option_names=['-h', '--help']) @click.command(context_settings=CONTEXT_SETTINGS) diff --git a/scripts/daqconf_viewer.css b/scripts/daqconf_viewer.css index 60deecee..f902c51a 100644 --- a/scripts/daqconf_viewer.css +++ b/scripts/daqconf_viewer.css @@ -39,6 +39,10 @@ RunInfo { width: 100%; } +.runbuttons { + align_horizontal: center; +} + .configs { row-span: 4; column-span: 1; From 4b74446b644a5f1468b72888c2e9e2f41fbbd7c1 Mon Sep 17 00:00:00 2001 From: Jonathan Hancock Date: Wed, 26 Jul 2023 12:27:35 +0200 Subject: [PATCH 87/90] Temporary directories are now deleted when quitting, so that there is no warning.\nFixed the CSS so that there is no scrolling on the run selection widgets --- scripts/daqconf_viewer | 16 ++++++++++---- scripts/daqconf_viewer.css | 45 +++++++++++++++++++------------------- 2 files changed, 34 insertions(+), 27 deletions(-) diff --git a/scripts/daqconf_viewer b/scripts/daqconf_viewer index 7589373b..3a417018 100755 --- a/scripts/daqconf_viewer +++ b/scripts/daqconf_viewer @@ -590,7 +590,7 @@ class LocalDiffScreen(Screen): def compose(self) -> ComposeResult: yield LocalConfigs(hostname=self.hostname, path=self.path, classes='greencontainer configs', id='localdiffconfigs') - yield DiffDisplay(hostname=self.hostname, classes='greencontainer display', id='localdiffdisplay') + yield DiffDisplay(hostname=self.hostname, classes='greencontainer bigdisplay', id='localdiffdisplay') yield Header(show_clock=True) yield Footer() @@ -617,7 +617,7 @@ class DiffScreen(Screen): def compose(self) -> ComposeResult: yield Configs(hostname=self.hostname, classes='greencontainer configs', id='diffconfigs') yield Versions(hostname=self.hostname, classes='greencontainer versions', id='diffversions') - yield DiffDisplay(hostname=self.hostname, classes='greencontainer smalldisplay', id='diffdisplay') + yield DiffDisplay(hostname=self.hostname, classes='greencontainer display', id='diffdisplay') yield Header(show_clock=True) yield Footer() @@ -639,7 +639,7 @@ class LocalConfScreen(Screen): def compose(self) -> ComposeResult: yield LocalConfigs(hostname=self.hostname, path=self.path, classes='redcontainer configs', id='localconfigs') - yield Display(hostname=self.hostname, classes='redcontainer display', id='localdisplay') + yield Display(hostname=self.hostname, classes='redcontainer bigdisplay', id='localdisplay') yield Header(show_clock=True) yield Footer() @@ -668,7 +668,7 @@ class BaseScreen(Screen): def compose(self) -> ComposeResult: yield Configs(hostname=self.confhost, classes='redcontainer configs', id='configs') yield Versions(hostname=self.confhost, classes='redcontainer versions', id='versions') - yield Display(hostname=self.confhost, classes='redcontainer smalldisplay', id='display') + yield Display(hostname=self.confhost, classes='redcontainer display', id='display') yield Header(show_clock=True) yield Footer() @@ -736,6 +736,14 @@ class ConfViewer(App): self.install_screen(RunRegistryScreen(hostname=self.reghost), name="runreg") self.push_screen("base") + def action_quit(self): + """ + Called when the quit button is pressed. + We redefine it here so that we can add the removal of the temporary directory. + """ + dir_object.cleanup() + self.exit() + CONTEXT_SETTINGS = dict(help_option_names=['-h', '--help']) @click.command(context_settings=CONTEXT_SETTINGS) diff --git a/scripts/daqconf_viewer.css b/scripts/daqconf_viewer.css index f902c51a..f67864ab 100644 --- a/scripts/daqconf_viewer.css +++ b/scripts/daqconf_viewer.css @@ -2,20 +2,21 @@ Screen { layout: grid; layers: below above; - grid-size: 4 4; + grid-size: 12 12; grid-gutter: 0; height: 100%; } RunSelection { - row-span: 1; - column-span: 2; + row-span: 4; + column-span: 6; height: 100%; + content-align: center middle; } RunInfo { - row-span: 1; - column-span: 2; + row-span: 4; + column-span: 6; height: 100%; } @@ -31,47 +32,45 @@ RunInfo { height: 20; } -#diffscreen{ - layer: above; - align: center middle; - background: cadetblue; - height: 100%; - width: 100%; -} - .runbuttons { align_horizontal: center; } .configs { - row-span: 4; - column-span: 1; + row-span: 12; + column-span: 3; height: 100%; } .shortconfigs { - row-span: 3; - column-span: 1; + row-span: 8; + column-span: 3; height: 100%; } .versions { - row-span: 1; - column-span: 3; + row-span: 3; + column-span: 9; height: 100%; align-vertical: middle; } +.bigdisplay { + row-span: 12; + column-span: 9; + height: 100%; + align-horizontal: center; +} .display { - row-span: 4; - column-span: 3; + row-span: 9; + column-span: 9; height: 100%; align-horizontal: center; } .smalldisplay { - row-span: 3; - column-span: 3; + row-span: 8; + column-span: 9; height: 100%; align-horizontal: center; } From 93e23f3ab1164386d33b024d4d663b05c0efe5d5 Mon Sep 17 00:00:00 2001 From: Jonathan Hancock <56547447+JonathanHancock0@users.noreply.github.com> Date: Thu, 27 Jul 2023 10:53:44 +0200 Subject: [PATCH 88/90] Mentioned the run registry --- docs/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/README.md b/docs/README.md index 98667c91..7d9fe6d3 100644 --- a/docs/README.md +++ b/docs/README.md @@ -10,7 +10,7 @@ For a slightly more in-depth look into how to generate configurations for a DAQ [Configuration options for casual or first-time users](ConfigurationsForCasualUsers.md) -If you want to view existing configs stored in the MongoDB, take a look at: +If you want to view existing configs stored in the MongoDB, or run configurations accessible through the run-registry microservice, take a look at: [Interacting with the Configuration Database](ConfigDatabase.md) From 196708ffda7301b28744708aefc7e7516a68ac42 Mon Sep 17 00:00:00 2001 From: Jonathan Hancock <56547447+JonathanHancock0@users.noreply.github.com> Date: Thu, 27 Jul 2023 11:15:48 +0200 Subject: [PATCH 89/90] Added a new section about the run reg screen, and mentioned the new CLI options --- docs/ConfigDatabase.md | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/docs/ConfigDatabase.md b/docs/ConfigDatabase.md index 54b3d017..29c4cd0d 100644 --- a/docs/ConfigDatabase.md +++ b/docs/ConfigDatabase.md @@ -9,9 +9,9 @@ _nanorc_ should then be started with `nanorc --pm k8s://np04-srv-015:31000 db:// Keep in mind that the config directory can contain underscores, but the name it will be given in the database cannot (hyphens are fine). ## Viewing configurations -To inspect the contents of the database, run `daqconf_viewer` after setting up the software environment. This will open a graphical UI in the terminal. There are three optional arguments that can be provided: -* --host to manually enter the host of the microservice (defaults to http://np04-srv-023) -* --port to manually enter the port that the service listens on (defaults to 31011) +To inspect the contents of the database, run `daqconf_viewer` after setting up the software environment. This will open a graphical UI in the terminal. There are five optional arguments that can be provided: +* --conf-host and reg-host to manually enter the host of the microservices (defaults to http://np04-srv-023 and http://dunedaq-microservices.cern.ch) +* --conf-port and reg-port to manually enter the port that the service listens on (defaults to 31011 and 5005) * --dir to tell the config viewer where to look for local config files (defaults to ./) ![Config Viewer](ConfViewerScreenshot.png) @@ -28,3 +28,9 @@ If a second config is selected using the previously defined process, then a "dif differences between the two in a format similar to how commits are displayed on github. Again, the L key can be used to switch to local files, allowing for comparisons of local and DB configs in any combination. Finally, once you are done press q to quit (or use ctrl+c). + +## Interacting with the run registry +Pressing the R key while on the first screen will take you to the run registry screen. Metadata for the chosen run is shown in the top right, and the associated config files are displayed in a tree on the bottom left. Selecting one brings up the JSON on the bottom right, with expandable schema as before. +![Run Reg Viewer](RunRegScreenshot.png) + +When the application is initialised, the most recent run is shown by default. To navigate to different runs, simply click the back/forward buttons, or type the desired run into the input box and click the Get Data button. The R key can be used to return to browsing the MongoDB when you are done. From 5dffdd1a190959638087e6b3413f7f848b1c931b Mon Sep 17 00:00:00 2001 From: Jonathan Hancock <56547447+JonathanHancock0@users.noreply.github.com> Date: Thu, 27 Jul 2023 11:16:18 +0200 Subject: [PATCH 90/90] Add files via upload --- docs/RunRegScreenshot.png | Bin 0 -> 74698 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 docs/RunRegScreenshot.png diff --git a/docs/RunRegScreenshot.png b/docs/RunRegScreenshot.png new file mode 100644 index 0000000000000000000000000000000000000000..43b34bdc7d189728d06b047522ace61152a8e08d GIT binary patch literal 74698 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%0ADAD^~Q@Y!}?%WYiP&-7_G&fE*Qp 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

J!`)wKs-bc{4)QMzGJOW(^l9J<5<6Oa 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

UhM}VmAe@|}QEb}zuZ13-ep13q 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 Ud?z@8--c*h(!Q8=!SccX0>wA8@&Et; literal 0 HcmV?d00001