Files
slsDetectorPackage/slsDetectorSoftware/generator/gen_commands.py
T
Martin Mueller 0837de2a5a CTB frequency rounding, CTB frequency measurement, CTB frequency units (#1423)
* round CTB clocks to next closest possible value, added freq measurement

* added time for firmware to measrue actual value after frequency change

* add check for backwards compatibility

* change CTB and XCTB clock values to MHz, TODO: units and validation errors

* changed runclk command to use units and float, TODO: dbit, adcclk, why is everything called StringTo ?

* do the same for dbit and adcclk

* added tolerance to exptime, fixed test

* update default values in server defs

* added virtual check in Altera_PLL, update testcases

* change python and pyctbgui to accept and return floating point MHz

* update help and comments

* Dev/ctb clocks fix (#1434)

* introduced new type Hz, typetraits, String conversions, command generation (not yet generated)

* incorrect unit typo

* cmd generation and compiles

* default to MHz, removed space between units for consistency with timers, min and max checks for clks

* in python, but need to change the default to Hz again for clean code and intuition

* allow ints, doubles, implicit conversions

* dont allow raw ints, doubles and implicit conversions

* fixed tests

* added operators for Hz in python

* fix test for min clk for xilinx ctb

* fix test

* fix python tests

* fixed xilinx period and default clks

* test fix

* removed the 3 clock cycle check for ctb and implemented properly the max adc clk frq for altera ctb

* removing 3 clock cycle code from xilinx as well

* formatting

* loadpattern before 3 clk cycles code

* actualtime and measurement time to be implemented in 100ns already in fw

* fix tests

* pyzmq dependency forthe tests

* fixed pyctbgui for freq

* insert tolerance check again

* also added tolerance check for patwaittime

* formatting

* minor: rounding test

* removed Rep redundant in ToString for freq

* intro frequency unit enums, removed unnecessary template behavior for ToString with freq unit, switching from parsing string unit argument to the enum argument for ToString, adding parsing string to unit at CLI boundary

* minor, and binaries

* minor, default clk vals are 0 but set up at detector setup

* get frequency only for that unit

* tolerance process

* missed in previous commit

* some more changes to exptime and validations

* ctb is probably done

* periodleft and delayleft

* fixed xilinx freq conv as well

* fixed m3 bug, binaries

* xilinx: setup also done in stop server so that the clk is not 0

* missed a test marker

* binaries in

* review fixes, simpler validation of timers in ctb and xilinx ctb

* typo fix

* format

* fix tests

---------

Co-authored-by: Martin Mueller <martin.mueller@psi.ch>
Co-authored-by: Dhanya Thattil <dhanya.thattil@psi.ch>
2026-05-06 15:52:13 +02:00

303 lines
14 KiB
Python

import argparse
import os
import subprocess
from pathlib import Path
import yaml
from autocomplete.autocomplete import type_info
from cpp_codegen.codegen import codegen, if_block, for_block, function, else_block
from infer_action.check_infer import check_infer
from autocomplete.autocomplete import type_values
GEN_PATH = Path(__file__).parent
COMMANDS_PATH = GEN_PATH / 'extended_commands.yaml'
DEPRECATED_COMMANDS_PATH = GEN_PATH / 'deprecated_commands.yaml'
REMOVED_COMMANDS_PATH = GEN_PATH / 'removed_commands.yaml'
CPP_INPUT_PATH = GEN_PATH / 'Caller.in.cpp'
HEADER_INPUT_PATH = GEN_PATH / 'Caller.in.h'
CPP_OUTPUT_PATH = GEN_PATH.parent / 'src' / 'Caller.cpp'
HEADER_OUTPUT_PATH = GEN_PATH.parent / 'src' / 'Caller.h'
INFER_HEADER_INPUT_PATH = GEN_PATH / 'inferAction.in.h'
INFER_CPP_INPUT_PATH = GEN_PATH / 'inferAction.in.cpp'
INFER_HEADER_OUTPUT_PATH = GEN_PATH.parent / 'src' / 'inferAction.h'
INFER_CPP_OUTPUT_PATH = GEN_PATH.parent / 'src' / 'inferAction.cpp'
def generate(
commands_path=COMMANDS_PATH,
cpp_input_path=CPP_INPUT_PATH,
header_input_path=HEADER_INPUT_PATH,
cpp_output_path=CPP_OUTPUT_PATH,
header_output_path=HEADER_OUTPUT_PATH,
infer_header_input_path=INFER_HEADER_INPUT_PATH,
infer_cpp_input_path=INFER_CPP_INPUT_PATH,
infer_header_output_path=INFER_HEADER_OUTPUT_PATH,
infer_cpp_output_path=INFER_CPP_OUTPUT_PATH,
):
commands_config = yaml.unsafe_load(commands_path.open('r'))
deprecated_commands_config = yaml.unsafe_load(DEPRECATED_COMMANDS_PATH.open('r'))
removed_commands_config = yaml.unsafe_load(REMOVED_COMMANDS_PATH.open('r'))
type_dist, non_dist = check_infer(commands=commands_config)
codegen.open(cpp_output_path)
# write call function
codegen.write_opening(cpp_input_path)
# iterate over the commands and generate code for each
print(f"[X] found {len(commands_config)} commands")
print('[*] generating code for commands')
for command_name, command in commands_config.items():
if 'is_description' in command and command['is_description']:
continue
with function('std::string', 'Caller::' + command['function_alias'], [('int', 'action')]) as fn:
codegen.write_line('std::ostringstream os;')
# print help
codegen.write_line('// print help')
with if_block('action == slsDetectorDefs::HELP_ACTION'):
if command["help"].startswith('code:'):
codegen.write_line(command["help"].strip('code:'))
else:
codegen.write_line(f'os << R"V0G0N({command["help"]} )V0G0N" << std::endl;')
codegen.write_line('return os.str();')
# inserting arguments if needed
if 'pattern_command' in command and command['pattern_command']:
codegen.write_line(f'GetLevelAndInsertIntoArgs("{command["pattern_command"]}");')
# check if action and arguments are valid
codegen.write_line('// check if action and arguments are valid')
first = True
for action, action_params in command['actions'].items():
with if_block(f'action == {codegen.actions_dict[action]}', elseif=not first):
check_argc = True
for arg in action_params['args']:
if arg['argc'] == -1:
check_argc = False
break
# check number of arguments
condition = "1" if check_argc else "0"
if check_argc:
for arg in action_params['args']:
condition += f' && args.size() != {arg["argc"]}'
with if_block(condition):
codegen.write_line(f'throw RuntimeError("Wrong number of arguments for action {action}");')
for arg in action_params['args']:
if not check_argc:
continue
with if_block(f'args.size() == {arg["argc"]}'):
# check argument types
if 'extra_variables' in arg:
for var in arg['extra_variables']:
codegen.write_line(f'{var["type"]} {var["name"]} = {var["value"]};')
if 'separate_time_units' in arg and arg['separate_time_units']:
codegen.write_line(f'try {{')
# TODO: refactor this repeating code
codegen.write_line(f'std::string tmp_time({arg["separate_time_units"]["input"]});')
codegen.write_line(f'std::string {arg["separate_time_units"]["output"][1]}'
f' = RemoveUnit(tmp_time);')
codegen.write_line(f'auto {arg["separate_time_units"]["output"][0]} = '
f'StringTo < time::ns > (tmp_time,'
f' {arg["separate_time_units"]["output"][1]});')
codegen.write_line(
f'}} catch (...) {{ throw RuntimeError("Could not convert argument to time::ns");}}')
elif 'convert_to_time' in arg and arg['convert_to_time']:
codegen.write_line(f'try {{')
codegen.write_line(
f'StringTo < time::ns > ({", ".join(arg["convert_to_time"]["input"])});')
codegen.write_line(
f'}} catch (...) {{ throw RuntimeError("Could not convert arguments to time::ns");}}')
elif 'separate_freq_units' in arg and arg['separate_freq_units']:
codegen.write_line(f'try {{')
# TODO: refactor this repeating code
codegen.write_line(f'std::string tmp_freq({arg["separate_freq_units"]["input"]});')
codegen.write_line(f'std::string {arg["separate_freq_units"]["output"][1]}'
f' = RemoveUnit(tmp_freq);')
codegen.write_line(f'auto {arg["separate_freq_units"]["output"][0]} = '
f'StringTo < defs::Hz > (tmp_freq,'
f' {arg["separate_freq_units"]["output"][1]});')
codegen.write_line(
f'}} catch (...) {{ throw RuntimeError("Could not convert argument to defs::Hz");}}')
elif 'convert_to_freq' in arg and arg['convert_to_freq']:
codegen.write_line(f'try {{')
codegen.write_line(
f'StringTo < defs::Hz > ({", ".join(arg["convert_to_freq"]["input"])});')
codegen.write_line(
f'}} catch (...) {{ throw RuntimeError("Could not convert arguments to defs::Hz");}}')
for i in range(len(arg['input'])):
if not arg['cast_input'][i]:
continue
codegen.write_line(f'try {{')
codegen.write_line(f'StringTo<{arg["input_types"][i]}>({arg["input"][i]});')
codegen.write_line(f'}} catch (...) {{')
codegen.write_line(
f' throw RuntimeError("Could not convert argument {i} to {arg["input_types"][i]}");')
codegen.write_line(f'}}')
first = False
with else_block():
codegen.write_line(
f'throw RuntimeError("INTERNAL ERROR: Invalid action: supported actions are {list(command["actions"].keys())}");')
# generate code for each action
codegen.write_line('// generate code for each action')
for action, action_params in command['actions'].items():
if 'detectors' in action_params:
codegen.write_line('auto detector_type = det->getDetectorType().squash();')
with if_block(f'action == {codegen.actions_dict[action]}'):
if 'detectors' in action_params:
first = True
for detector, detector_params in action_params['detectors'].items():
with if_block(f'detector_type == defs::{detector}', elseif=not first):
codegen.write_arg(detector_params, action, command_name)
else_block().__enter__()
if not action_params:
codegen.write_line(f'throw RuntimeError("detector not supported for action: {action}");')
else:
tmp_args = []
if 'args' in action_params:
tmp_args = action_params['args']
codegen.write_arg(tmp_args, action, command_name)
if 'detectors' in action_params:
else_block().__exit__()
codegen.write_line('return os.str();')
# close sls namespace
codegen.write_closing()
codegen.close()
print('[X] .cpp code generated')
deprecated_commands = []
removed_commands = []
codegen.write_header(header_input_path, header_output_path, commands_config, deprecated_commands_config, removed_commands_config)
print('[X] header code generated')
codegen.write_infer_header(infer_header_input_path, infer_header_output_path, commands_config) #TODO: add deprecated commands
print('[X] infer header code generated')
codegen.open(infer_cpp_output_path)
codegen.write_infer_cpp(infer_cpp_input_path, infer_cpp_output_path, commands_config, non_dist, type_dist)
codegen.close()
print('[X] infer cpp code generated')
if __name__ == '__main__':
parser = argparse.ArgumentParser(
description='generate c++ code for cli commands from the commands.yaml file',
)
parser.add_argument('-f', '--format', action='store_true', default=False, dest='format',
help='format header and cpp file using clang-format')
parser.add_argument('-p', '--parse', action='store_true', default=False, dest='parse',
help='parse the commands.yaml file into extended_commands.yaml')
parser.add_argument('-c', '--check', action='store_true', default=False, dest='check',
help='check missing commands')
parser.add_argument('-g', '--generate', action='store_true', default=False, dest='generate', help='generate code (C++ or bash if -a is used)')
parser.add_argument('-a', '--autocomplete', action='store_true', default=False, dest='autocomplete',
help='print bash autocomplete values')
cli_args = parser.parse_args()
if cli_args.autocomplete:
from autocomplete.autocomplete import generate_type_values, generate_bash_autocomplete
if cli_args.generate:
generate_bash_autocomplete()
print('[X] bash autocomplete generated')
generate_bash_autocomplete(
output_path=Path(__file__).parent / 'autocomplete' / 'zsh_autocomplete.sh',
input_path=Path(__file__).parent / 'autocomplete' / 'zsh_autocomplete.in.sh'
)
print('[X] zsh autocomplete generated')
exit(0)
else:
ret = generate_type_values()
print(ret)
exit(0)
if cli_args.check:
from commands_parser.commands_parser import command_parser
commands_config = yaml.unsafe_load(COMMANDS_PATH.open('r'))
# infer action based on number of arguments and types
type_dist, non_dist = check_infer(commands=commands_config)
command_parser.verify_format()
command_parser.parse_all_commands()
# generate list of commands found in sls_detector_get
glist_path = GEN_PATH / 'glist'
ret = subprocess.run([f"sls_detector_get list | tail -n +2 | sort > {glist_path.absolute()}"], shell=True,
capture_output=True, check=True)
if ret.stderr != b'':
print('[!] glist generation failed and glist not found')
exit(1)
if not COMMANDS_PATH.exists():
print('[!] extended_commands.yaml not found')
exit(1)
detglist = set(command['command_name'] for __, command in commands_config.items())
detglist.add('free')
detglist.add('list')
g_path = GEN_PATH / 'glist'
if not g_path.exists():
print('[!] glist not found')
exit(1)
glist = set(g_path.read_text().split('\n'))
if "" in glist:
glist.remove("")
if "" in detglist:
detglist.remove("")
not_found = set()
for command in glist:
if command not in detglist:
not_found.add(command)
print()
if len(not_found) > 0:
print(f'[!] found {len(not_found)} missing')
print(f"not_found: {not_found}")
else:
print(f'[X] found no missing commands')
for command in detglist:
if command not in glist:
print(f'[!] command {command} found in commands.yaml but not found in g list')
exit(0)
if cli_args.parse:
from commands_parser.commands_parser import command_parser
command_parser.verify_format()
command_parser.parse_all_commands()
if cli_args.generate:
generate()
if cli_args.format:
files = [CPP_OUTPUT_PATH, HEADER_OUTPUT_PATH, INFER_HEADER_OUTPUT_PATH, INFER_CPP_OUTPUT_PATH]
for file in files:
os.system(f'clang-format -i {file.absolute()}')
#os.system(f'clang-format -i --style="{{Standard: C++11}}" {file.absolute()}')
print('[X] code formatted')