From 861f027f1a50b58ef35e876193bfa76b8e501dcd Mon Sep 17 00:00:00 2001 From: David Lesnjak Date: Fri, 24 Jul 2026 12:14:01 +0200 Subject: [PATCH 1/3] SDSIO-Server: support selecting playback test cases from CLI and monitor commands --- .../Board/Corstone-300/vsi/python/sdsio.py | 52 +++++++- .../Board/Corstone-320/vsi/python/sdsio.py | 52 +++++++- utilities/sdsio-server.py | 113 +++++++++++++++--- 3 files changed, 187 insertions(+), 30 deletions(-) diff --git a/template/Board/Corstone-300/vsi/python/sdsio.py b/template/Board/Corstone-300/vsi/python/sdsio.py index a84c5ba..566b7e5 100644 --- a/template/Board/Corstone-300/vsi/python/sdsio.py +++ b/template/Board/Corstone-300/vsi/python/sdsio.py @@ -27,7 +27,7 @@ # ---------------------------------------------------------------------------- # # SDSIO server-compatible stream implementation # # ---------------------------------------------------------------------------- # -SDSIO_VSI_VERSION = "3.0.1-dev7" +SDSIO_VSI_VERSION = "3.0.1-dev8" class StreamInfo(NamedTuple): name: str = None @@ -223,6 +223,7 @@ def __init__( exit_after_playback=False, no_progress_info=False, play_list: Optional[list] = None, + playback_test_case: Optional[int] = None, mon_port: Optional[int] = None, write_flush_records: Optional[int] = None, status_bar_factory=None, @@ -260,6 +261,8 @@ def __init__( self._exit_after_playback = exit_after_playback self._send_ci_terminate_on_shutdown = False self._play_list = play_list + self._play_step_limit = len(play_list) if play_list else None + self._single_playback_test_case_selected = False self._mon_port = mon_port self._write_flush_records = write_flush_records # SDS Control Flags @@ -270,7 +273,7 @@ def __init__( if monitor_factory is None: monitor_factory = sdsMonitorInterface if monitor_factory: - self._monitor = monitor_factory(self._mon_port, self._flags) + self._monitor = monitor_factory(self._mon_port, self._flags, self.select_playback_test_case) self._ctrl_input = None if control_input_factory is not False and sys.stdin.isatty(): if control_input_factory is None: @@ -289,6 +292,8 @@ def __init__( except RuntimeError: self._loop = None self._main_task = None + if playback_test_case is not None: + self.select_playback_test_case(playback_test_case) def shutdown(self): self.shutdown_requested.set() @@ -473,9 +478,41 @@ def _file_read_worker(self, sid, name, buf: ByteStreamBuffer, stop_evt): finally: buf.set_eof() + def _get_play_step_limit(self): + if not self._play_list: + return None + if self._play_step_limit is None: + return len(self._play_list) + return min(self._play_step_limit, len(self._play_list)) + + def _is_single_playback_test_case_selected(self) -> bool: + return self._single_playback_test_case_selected + + def select_playback_test_case(self, test_case: int) -> bool: + if test_case == 0: + return True + if self.opened_streams: + logger.error("Playback test case selection failed: streams are currently open.") + return False + if not self._play_list: + logger.error("Playback test case selection failed: no play steps are configured.") + return False + + if test_case < 1 or test_case > len(self._play_list): + logger.error(f"Playback test case selection failed: {test_case} is outside 1-{len(self._play_list)}.") + return False + self._play_step_index = test_case - 1 + self._play_step_limit = test_case + self._single_playback_test_case_selected = True + self._label_list.clear() + self._timestamp_boundaries.clear() + logger.info(f"Selected playback test case {test_case}/{len(self._play_list)}.") + return True + def _create_play_label_list(self, name) -> list[str]: _labels = [] - if self._play_list and self._play_step_index < len(self._play_list): + _play_step_limit = self._get_play_step_limit() + if self._play_list and self._play_step_index < _play_step_limit: _step = self._play_list[self._play_step_index] _labels = list(_step.get('labels', [])) else: @@ -489,7 +526,7 @@ def _has_next_auto_playback_step(self) -> bool: if not self._flags.auto_playback or self.opened_streams: return False if self._play_list: - return self._play_step_index < len(self._play_list) + return self._play_step_index < self._get_play_step_limit() if self._last_playback_stream_name: return bool(self._create_play_label_list(self._last_playback_stream_name)) return False @@ -549,11 +586,14 @@ def _open(self, mode, name): # Get flags, Set working dir _index_based_playback = False if self._play_list: - if self._play_step_index < len(self._play_list): + if self._play_step_index < self._get_play_step_limit(): _step = self._play_list[self._play_step_index] _step_desc = _step.get('step', '') _desc_suffix = f": {_step_desc}" if _step_desc else "" - logger.info(f"Playback step {self._play_step_index + 1}/{len(self._play_list)}{_desc_suffix}.") + if self._is_single_playback_test_case_selected(): + logger.info(f"Playback step {self._play_step_index + 1}{_desc_suffix}.") + else: + logger.info(f"Playback step {self._play_step_index + 1}/{len(self._play_list)}{_desc_suffix}.") _set_flags = _step.get('setflags', 0) _clear_flags = _step.get('clearflags', 0) _recdir = _step.get('recdir', None) diff --git a/template/Board/Corstone-320/vsi/python/sdsio.py b/template/Board/Corstone-320/vsi/python/sdsio.py index a84c5ba..566b7e5 100644 --- a/template/Board/Corstone-320/vsi/python/sdsio.py +++ b/template/Board/Corstone-320/vsi/python/sdsio.py @@ -27,7 +27,7 @@ # ---------------------------------------------------------------------------- # # SDSIO server-compatible stream implementation # # ---------------------------------------------------------------------------- # -SDSIO_VSI_VERSION = "3.0.1-dev7" +SDSIO_VSI_VERSION = "3.0.1-dev8" class StreamInfo(NamedTuple): name: str = None @@ -223,6 +223,7 @@ def __init__( exit_after_playback=False, no_progress_info=False, play_list: Optional[list] = None, + playback_test_case: Optional[int] = None, mon_port: Optional[int] = None, write_flush_records: Optional[int] = None, status_bar_factory=None, @@ -260,6 +261,8 @@ def __init__( self._exit_after_playback = exit_after_playback self._send_ci_terminate_on_shutdown = False self._play_list = play_list + self._play_step_limit = len(play_list) if play_list else None + self._single_playback_test_case_selected = False self._mon_port = mon_port self._write_flush_records = write_flush_records # SDS Control Flags @@ -270,7 +273,7 @@ def __init__( if monitor_factory is None: monitor_factory = sdsMonitorInterface if monitor_factory: - self._monitor = monitor_factory(self._mon_port, self._flags) + self._monitor = monitor_factory(self._mon_port, self._flags, self.select_playback_test_case) self._ctrl_input = None if control_input_factory is not False and sys.stdin.isatty(): if control_input_factory is None: @@ -289,6 +292,8 @@ def __init__( except RuntimeError: self._loop = None self._main_task = None + if playback_test_case is not None: + self.select_playback_test_case(playback_test_case) def shutdown(self): self.shutdown_requested.set() @@ -473,9 +478,41 @@ def _file_read_worker(self, sid, name, buf: ByteStreamBuffer, stop_evt): finally: buf.set_eof() + def _get_play_step_limit(self): + if not self._play_list: + return None + if self._play_step_limit is None: + return len(self._play_list) + return min(self._play_step_limit, len(self._play_list)) + + def _is_single_playback_test_case_selected(self) -> bool: + return self._single_playback_test_case_selected + + def select_playback_test_case(self, test_case: int) -> bool: + if test_case == 0: + return True + if self.opened_streams: + logger.error("Playback test case selection failed: streams are currently open.") + return False + if not self._play_list: + logger.error("Playback test case selection failed: no play steps are configured.") + return False + + if test_case < 1 or test_case > len(self._play_list): + logger.error(f"Playback test case selection failed: {test_case} is outside 1-{len(self._play_list)}.") + return False + self._play_step_index = test_case - 1 + self._play_step_limit = test_case + self._single_playback_test_case_selected = True + self._label_list.clear() + self._timestamp_boundaries.clear() + logger.info(f"Selected playback test case {test_case}/{len(self._play_list)}.") + return True + def _create_play_label_list(self, name) -> list[str]: _labels = [] - if self._play_list and self._play_step_index < len(self._play_list): + _play_step_limit = self._get_play_step_limit() + if self._play_list and self._play_step_index < _play_step_limit: _step = self._play_list[self._play_step_index] _labels = list(_step.get('labels', [])) else: @@ -489,7 +526,7 @@ def _has_next_auto_playback_step(self) -> bool: if not self._flags.auto_playback or self.opened_streams: return False if self._play_list: - return self._play_step_index < len(self._play_list) + return self._play_step_index < self._get_play_step_limit() if self._last_playback_stream_name: return bool(self._create_play_label_list(self._last_playback_stream_name)) return False @@ -549,11 +586,14 @@ def _open(self, mode, name): # Get flags, Set working dir _index_based_playback = False if self._play_list: - if self._play_step_index < len(self._play_list): + if self._play_step_index < self._get_play_step_limit(): _step = self._play_list[self._play_step_index] _step_desc = _step.get('step', '') _desc_suffix = f": {_step_desc}" if _step_desc else "" - logger.info(f"Playback step {self._play_step_index + 1}/{len(self._play_list)}{_desc_suffix}.") + if self._is_single_playback_test_case_selected(): + logger.info(f"Playback step {self._play_step_index + 1}{_desc_suffix}.") + else: + logger.info(f"Playback step {self._play_step_index + 1}/{len(self._play_list)}{_desc_suffix}.") _set_flags = _step.get('setflags', 0) _clear_flags = _step.get('clearflags', 0) _recdir = _step.get('recdir', None) diff --git a/utilities/sdsio-server.py b/utilities/sdsio-server.py index 1d40829..e2b1f92 100644 --- a/utilities/sdsio-server.py +++ b/utilities/sdsio-server.py @@ -40,7 +40,7 @@ import termios import tty -SDSIO_SERVER_VERSION = "3.0.1-dev7" +SDSIO_SERVER_VERSION = "3.0.1-dev8" class StreamInfo(NamedTuple): name: str = None @@ -263,9 +263,10 @@ def stop(self): # SDS IO Monitor interface # # ---------------------------------------------------------------------------- # class sdsMonitorInterface(): - def __init__(self, port, flags: Optional['sdsFlags'] = None): + def __init__(self, port, flags: Optional['sdsFlags'] = None, playback_selector=None): self._port = port self._flags = flags + self._playback_selector = playback_selector self._lock = threading.Lock() # guards self._socket self._recv_buf = bytearray() # only accessed from handle_commands thread self._last_info = (0, 0, b'') # cached (flags, idle_rate, err_data) for new clients @@ -343,7 +344,17 @@ def handle_commands(self): if _cmd == SDSIO_MON_FLAGS: _set_flags = int.from_bytes(self._recv_buf[4:8], 'little') _clear_flags = int.from_bytes(self._recv_buf[8:12], 'little') - logger.info(f"Monitor command received: SDSIO_MON_FLAGS (set=0x{_set_flags:08X}, clear=0x{_clear_flags:08X}).") + _test_case = int.from_bytes(self._recv_buf[12:16], 'little') + _test_case_msg = f", test_case={_test_case}" if _test_case else "" + logger.info(f"Monitor command received: SDSIO_MON_FLAGS (set=0x{_set_flags:08X}, clear=0x{_clear_flags:08X}{_test_case_msg}).") + if _test_case: + if not self._playback_selector: + logger.warning(f"Monitor playback test case request ignored: {_test_case}.") + del self._recv_buf[:16] + continue + if not self._playback_selector(_test_case): + del self._recv_buf[:16] + continue if self._flags: self._flags.apply(_set_flags, _clear_flags) elif _cmd == SDSIO_MON_SHUTDOWN: @@ -633,6 +644,7 @@ def __init__( exit_after_playback=False, no_progress_info=False, play_list: Optional[list] = None, + playback_test_case: Optional[int] = None, mon_port: Optional[int] = None, write_flush_records: Optional[int] = None, status_bar_factory=None, @@ -670,6 +682,8 @@ def __init__( self._exit_after_playback = exit_after_playback self._send_ci_terminate_on_shutdown = False self._play_list = play_list + self._play_step_limit = len(play_list) if play_list else None + self._single_playback_test_case_selected = False self._mon_port = mon_port self._write_flush_records = write_flush_records # SDS Control Flags @@ -680,7 +694,7 @@ def __init__( if monitor_factory is None: monitor_factory = sdsMonitorInterface if monitor_factory: - self._monitor = monitor_factory(self._mon_port, self._flags) + self._monitor = monitor_factory(self._mon_port, self._flags, self.select_playback_test_case) self._ctrl_input = None if control_input_factory is not False and sys.stdin.isatty(): if control_input_factory is None: @@ -699,6 +713,8 @@ def __init__( except RuntimeError: self._loop = None self._main_task = None + if playback_test_case is not None: + self.select_playback_test_case(playback_test_case) def shutdown(self): self.shutdown_requested.set() @@ -883,9 +899,41 @@ def _file_read_worker(self, sid, name, buf: ByteStreamBuffer, stop_evt): finally: buf.set_eof() + def _get_play_step_limit(self): + if not self._play_list: + return None + if self._play_step_limit is None: + return len(self._play_list) + return min(self._play_step_limit, len(self._play_list)) + + def _is_single_playback_test_case_selected(self) -> bool: + return self._single_playback_test_case_selected + + def select_playback_test_case(self, test_case: int) -> bool: + if test_case == 0: + return True + if self.opened_streams: + logger.error("Playback test case selection failed: streams are currently open.") + return False + if not self._play_list: + logger.error("Playback test case selection failed: no play steps are configured.") + return False + + if test_case < 1 or test_case > len(self._play_list): + logger.error(f"Playback test case selection failed: {test_case} is outside 1-{len(self._play_list)}.") + return False + self._play_step_index = test_case - 1 + self._play_step_limit = test_case + self._single_playback_test_case_selected = True + self._label_list.clear() + self._timestamp_boundaries.clear() + logger.info(f"Selected playback test case {test_case}/{len(self._play_list)}.") + return True + def _create_play_label_list(self, name) -> list[str]: _labels = [] - if self._play_list and self._play_step_index < len(self._play_list): + _play_step_limit = self._get_play_step_limit() + if self._play_list and self._play_step_index < _play_step_limit: _step = self._play_list[self._play_step_index] _labels = list(_step.get('labels', [])) else: @@ -899,7 +947,7 @@ def _has_next_auto_playback_step(self) -> bool: if not self._flags.auto_playback or self.opened_streams: return False if self._play_list: - return self._play_step_index < len(self._play_list) + return self._play_step_index < self._get_play_step_limit() if self._last_playback_stream_name: return bool(self._create_play_label_list(self._last_playback_stream_name)) return False @@ -959,11 +1007,14 @@ def _open(self, mode, name): # Get flags, Set working dir _index_based_playback = False if self._play_list: - if self._play_step_index < len(self._play_list): + if self._play_step_index < self._get_play_step_limit(): _step = self._play_list[self._play_step_index] _step_desc = _step.get('step', '') _desc_suffix = f": {_step_desc}" if _step_desc else "" - logger.info(f"Playback step {self._play_step_index + 1}/{len(self._play_list)}{_desc_suffix}.") + if self._is_single_playback_test_case_selected(): + logger.info(f"Playback step {self._play_step_index + 1}{_desc_suffix}.") + else: + logger.info(f"Playback step {self._play_step_index + 1}/{len(self._play_list)}{_desc_suffix}.") _set_flags = _step.get('setflags', 0) _clear_flags = _step.get('clearflags', 0) _recdir = _step.get('recdir', None) @@ -2192,6 +2243,20 @@ def non_negative_int(value): raise argparse.ArgumentTypeError("Value must be 0 or greater!") return _value +def _parse_playback_selection(value): + if value is None: + return False, None + _value = str(value).strip().lower() + if _value in ("", "*", "all"): + return True, None + try: + _test_case = int(_value, 0) + except ValueError: + raise argparse.ArgumentTypeError(f"Invalid playback test case: {value}!") + if _test_case < 1: + raise argparse.ArgumentTypeError("Playback test case must be 1 or greater!") + return True, _test_case + def ip_validator(ip_str): try: ipaddress.ip_address(ip_str) @@ -2275,6 +2340,7 @@ def _format_action_invocation(self, action): "examples:\n" " %(prog)s -c sdsio.yml # Recommended: all config in YAML\n" " %(prog)s -c sdsio.yml --playback # Playback mode\n" + " %(prog)s -c sdsio.yml --playback 2 # Playback test case 2\n" " %(prog)s -c sdsio.yml --mon-port 6060 # With VS Code SDS extension monitor\n" " %(prog)s usb --workdir ./data # USB interface, explicit work dir\n" " %(prog)s socket --port 5050 # Socket interface\n" @@ -2323,8 +2389,8 @@ def _add_info_opts(p, version_text=None): def _add_general_opts(p): """Append a general-opts group to subparser p (called after server-specific groups).""" _g = p.add_argument_group("general-opts") - _g.add_argument("--playback", "-p", dest="auto_playback", action="store_true", - help="Start SDSIO-Server in playback mode (typically used in CI tests)", + _g.add_argument("--playback", "-p", dest="auto_playback", nargs="?", const="all", metavar="", + help="Start SDSIO-Server in playback mode; optionally select a playback test case or '*' for all", default=argparse.SUPPRESS) _g.add_argument("--exit-after-playback", "-x", dest="exit_after_playback", action="store_true", help="Terminate when playback is completed", @@ -2431,8 +2497,8 @@ def _add_general_opts(p): _add_general_opts(_parser_usb) _general = _parser.add_argument_group("general-opts") - _general.add_argument("--playback", "-p", dest="auto_playback", action="store_true", - help="Start SDSIO-Server in playback mode (typically used in CI tests)", default=None) + _general.add_argument("--playback", "-p", dest="auto_playback", nargs="?", const="all", metavar="", + help="Start SDSIO-Server in playback mode; optionally select a playback test case or '*' for all", default=None) _general.add_argument("--exit-after-playback", "-x", dest="exit_after_playback", action="store_true", help="Terminate when playback is completed", default=None) _general.add_argument("--no-progress-info", "-n", dest="no_progress_info", action="store_true", @@ -2602,11 +2668,6 @@ async def main(): if _ctrl_yml_path: logger.info(f"SDSIO configuration YAML: {_ctrl_yml_path}") - # Auto playback - _auto_playback = _args.auto_playback if _args.auto_playback else False - _exit_after_playback = _args.exit_after_playback if _args.exit_after_playback else False - _no_progress_info = _args.no_progress_info if _args.no_progress_info else False - # Playback list _play_list: Optional[list] = _ctrl_data.get('play', None) if _ctrl_data else None if _play_list: @@ -2615,8 +2676,24 @@ async def main(): if _recdir and not path.isabs(_recdir): _step['recdir'] = path.normpath(path.join(_work_dir, _recdir)) + # Auto playback + try: + _auto_playback, _playback_test_case = _parse_playback_selection(_args.auto_playback) + except argparse.ArgumentTypeError as _e: + logger.error(str(_e)) + sys.exit(1) + if _playback_test_case is not None: + if not _play_list: + logger.error("Playback test case selection requires play steps in SDSIO configuration YAML.") + sys.exit(1) + if _playback_test_case > len(_play_list): + logger.error(f"Playback test case {_playback_test_case} is outside 1-{len(_play_list)}.") + sys.exit(1) + _exit_after_playback = _args.exit_after_playback if _args.exit_after_playback else False + _no_progress_info = _args.no_progress_info if _args.no_progress_info else False + _manager = sdsio_manager(work_dir=_work_dir, auto_playback=_auto_playback, exit_after_playback=_exit_after_playback, - no_progress_info=_no_progress_info, play_list=_play_list, + no_progress_info=_no_progress_info, play_list=_play_list, playback_test_case=_playback_test_case, mon_port=_args.monitor_port, write_flush_records=_write_flush_records) try: From dffd56089bff0dd9753d2acfe316b17ac7403676 Mon Sep 17 00:00:00 2001 From: David Lesnjak Date: Tue, 28 Jul 2026 15:18:55 +0200 Subject: [PATCH 2/3] Documentation: add playback test case selection for CLI and monitor commands --- documentation/theory.md | 16 ++++++++++++---- documentation/utilities.md | 19 +++++++++++++++++-- 2 files changed, 29 insertions(+), 6 deletions(-) diff --git a/documentation/theory.md b/documentation/theory.md index 62f358a..8d66c44 100644 --- a/documentation/theory.md +++ b/documentation/theory.md @@ -528,7 +528,7 @@ ID | Name | Description :--:|:-------------------|:------------------------ 1 | SDSIO_MON_OPEN | Information about the SDS file open operation (message) 2 | SDSIO_MON_CLOSE | Information about the SDS file close operation (message) -6 | SDSIO_MON_FLAGS | Monitor program request to the SDSIO-Server to update SDS control flags in the firmware +6 | SDSIO_MON_FLAGS | Monitor program request to the SDSIO-Server to update SDS control flags in the firmware and optionally select a playback test case 7 | SDSIO_MON_INFO | Information update received from the firmware and forwarded to the Monitor program (message) 8 | SDSIO_MON_SHUTDOWN | Monitor program request to the SDSIO-Server to complete current tasks and shut down gracefully @@ -562,14 +562,22 @@ The Message with ID = **2** (SDSIO_MON_CLOSE) is sent whenever an SDS data file The Command with ID = **6** (SDSIO_MON_FLAGS) is used by the Monitor program to request an update of the SDS control flags in the firmware. The `Set Mask` specifies the bits to set in the `sdsFlags` and the `Clear Mask` specifies the bits to clear in the `sdsFlags`. +The `TestCase` value optionally selects a `play:` step from the `*.sdsio.yml` file before the flag update is applied. This command does not generate a response from the SDSIO-Server. ```txt -| WORD | WORD | WORD | WORD | -> 6 | Set Mask | Clear Mask | 0 | -|******|**********|************|******| +| WORD | WORD | WORD | WORD | +> 6 | Set Mask | Clear Mask | TestCase | +|******|**********|************|**********| ``` +`TestCase` values are: + +- `0`: do not change the selected playback step. +- `1..N`: select the corresponding `play:` step by sequential number, starting with `1`. + +Selecting a `TestCase` does not start playback by itself. To select and start a playback test case, set the `SDS_FLAG_START` and `SDS_FLAG_PLAYBACK` bits in `Set Mask` together with the desired `TestCase` value. + **SDSIO_MON_INFO** The Message with ID = **7** (SDSIO_MON_INFO) contains status information received by the SDSIO-Server from the firmware, diff --git a/documentation/utilities.md b/documentation/utilities.md index f30fb34..4f606f1 100644 --- a/documentation/utilities.md +++ b/documentation/utilities.md @@ -153,6 +153,7 @@ The `streams:` node provides additional information about the SDS data streams t The `play:` node specifies one or more playback steps. Each playback step defines the list of labels to play back for each opened SDS stream. +Playback steps are selected by their sequential number in the `play:` list, starting with `1`. For one playback step, all files of the `labels:` list are concatenated and appear as one data stream for the firmware. A pause (where the data stream needs to be closed and opened again by the firmware) is created with another `- step:` section. @@ -242,7 +243,7 @@ configuration: --control, -c <*.sdsio.yml> Configure interface, SDS file directories, and playback steps general-opts: - --playback, -p Start SDSIO-Server in playback mode (typically used in CI tests) + --playback, -p Start SDSIO-Server in playback mode; optionally select a playback test case or '*' for all --exit-after-playback, -x Terminate when playback is completed --no-progress-info, -n Disable dynamic progress indicator --workdir Directory for SDS files (overrides *.sdsio.yml setting; default: current directory) @@ -277,12 +278,26 @@ Start SDSIO-Server using a control file: python sdsio-server.py -c myproject.sdsio.yml ``` -Start SDSIO-Server automatically starting the playback: +Start SDSIO-Server automatically starting playback of all configured `play:` steps: ```bash python sdsio-server.py -c sdsio.yml --playback ``` +Start SDSIO-Server automatically starting playback from all configured `play:` steps explicitly: + +```bash +python sdsio-server.py -c sdsio.yml --playback all +``` + +Start SDSIO-Server automatically starting only the second configured `play:` step: + +```bash +python sdsio-server.py -c sdsio.yml --playback 2 +``` + +The numeric `--playback` value is the sequential number of a `play:` step in `sdsio.yml`, starting with `1`. + Start SDSIO-Server and override the `workdir:` node. ```bash From 7ff0e63cb9d548acf6bda9e3493ae067970996fe Mon Sep 17 00:00:00 2001 From: David Lesnjak Date: Fri, 7 Aug 2026 12:59:12 +0200 Subject: [PATCH 3/3] SDSIO-Server: improve playback step selection; change to zero-based indexing --- documentation/sdsio.md | 2 +- documentation/theory.md | 7 +- documentation/utilities.md | 18 +-- .../Board/Corstone-300/vsi/python/sdsio.py | 80 ++++++------ .../Board/Corstone-320/vsi/python/sdsio.py | 80 ++++++------ utilities/sdsio-server.py | 121 ++++++++++-------- 6 files changed, 161 insertions(+), 147 deletions(-) diff --git a/documentation/sdsio.md b/documentation/sdsio.md index 7e63888..7db2ea0 100644 --- a/documentation/sdsio.md +++ b/documentation/sdsio.md @@ -235,7 +235,7 @@ SDSIO_FVP environment variable not set. Working directory: ...\datatest\SDS Recordings. SDSIO configuration YAML: ...\SDS\datatest.sdsio.yml. sdsFlags = 0xB0000000. -Playback step 1/1: Test 0. +Playback step 0/0: Test 0. Playback: Test_In (Test_In.0.sds). Record: Test_Out (Test_Out.0.p.sds). Closed: Test_In (Test_In.0.sds). diff --git a/documentation/theory.md b/documentation/theory.md index 8d66c44..9b62327 100644 --- a/documentation/theory.md +++ b/documentation/theory.md @@ -562,7 +562,7 @@ The Message with ID = **2** (SDSIO_MON_CLOSE) is sent whenever an SDS data file The Command with ID = **6** (SDSIO_MON_FLAGS) is used by the Monitor program to request an update of the SDS control flags in the firmware. The `Set Mask` specifies the bits to set in the `sdsFlags` and the `Clear Mask` specifies the bits to clear in the `sdsFlags`. -The `TestCase` value optionally selects a `play:` step from the `*.sdsio.yml` file before the flag update is applied. +The `TestCase` value selects a `play:` step from the `*.sdsio.yml` file before the flag update is applied. This command does not generate a response from the SDSIO-Server. ```txt @@ -571,10 +571,9 @@ This command does not generate a response from the SDSIO-Server. |******|**********|************|**********| ``` -`TestCase` values are: +Valid `TestCase` values are: -- `0`: do not change the selected playback step. -- `1..N`: select the corresponding `play:` step by sequential number, starting with `1`. +- `0..N-1`: select the corresponding `play:` step by zero-based index, starting with `0`. Selecting a `TestCase` does not start playback by itself. To select and start a playback test case, set the `SDS_FLAG_START` and `SDS_FLAG_PLAYBACK` bits in `Set Mask` together with the desired `TestCase` value. diff --git a/documentation/utilities.md b/documentation/utilities.md index 4f606f1..255f407 100644 --- a/documentation/utilities.md +++ b/documentation/utilities.md @@ -153,7 +153,7 @@ The `streams:` node provides additional information about the SDS data streams t The `play:` node specifies one or more playback steps. Each playback step defines the list of labels to play back for each opened SDS stream. -Playback steps are selected by their sequential number in the `play:` list, starting with `1`. +Playback steps are selected by their zero-based index in the `play:` list, starting with `0`. For one playback step, all files of the `labels:` list are concatenated and appear as one data stream for the firmware. A pause (where the data stream needs to be closed and opened again by the firmware) is created with another `- step:` section. @@ -243,7 +243,7 @@ configuration: --control, -c <*.sdsio.yml> Configure interface, SDS file directories, and playback steps general-opts: - --playback, -p Start SDSIO-Server in playback mode; optionally select a playback test case or '*' for all + --playback, -p Start SDSIO-Server in playback mode; optionally select a zero-based playback step index or '*' for all --exit-after-playback, -x Terminate when playback is completed --no-progress-info, -n Disable dynamic progress indicator --workdir Directory for SDS files (overrides *.sdsio.yml setting; default: current directory) @@ -281,22 +281,16 @@ python sdsio-server.py -c myproject.sdsio.yml Start SDSIO-Server automatically starting playback of all configured `play:` steps: ```bash -python sdsio-server.py -c sdsio.yml --playback +python sdsio-server.py -c sdsio.yml --playback "*" ``` -Start SDSIO-Server automatically starting playback from all configured `play:` steps explicitly: +Start SDSIO-Server automatically starting only the first configured `play:` step: ```bash -python sdsio-server.py -c sdsio.yml --playback all +python sdsio-server.py -c sdsio.yml --playback 0 ``` -Start SDSIO-Server automatically starting only the second configured `play:` step: - -```bash -python sdsio-server.py -c sdsio.yml --playback 2 -``` - -The numeric `--playback` value is the sequential number of a `play:` step in `sdsio.yml`, starting with `1`. +The numeric `--playback` value is the zero-based index of a `play:` step in `sdsio.yml`, starting with `0`. Start SDSIO-Server and override the `workdir:` node. diff --git a/template/Board/Corstone-300/vsi/python/sdsio.py b/template/Board/Corstone-300/vsi/python/sdsio.py index 566b7e5..2508e51 100644 --- a/template/Board/Corstone-300/vsi/python/sdsio.py +++ b/template/Board/Corstone-300/vsi/python/sdsio.py @@ -246,8 +246,8 @@ def __init__( self._read_buffers = {} # sid -> ByteStreamBuffer self._read_threads = {} # sid -> Thread self._read_stop = {} # sid -> Event - # lock to protect stream_id increment and open checks - self._manager_lock = threading.Lock() + # lock to protect playback selection and stream state transitions + self._manager_lock = threading.RLock() # timestamp of last stream read or write command self.time_last_rw = time.time() # status bar @@ -293,7 +293,8 @@ def __init__( self._loop = None self._main_task = None if playback_test_case is not None: - self.select_playback_test_case(playback_test_case) + if playback_test_case < 0 or not self.select_playback_test_case(playback_test_case): + raise ValueError(f"Invalid playback test case: {playback_test_case}") def shutdown(self): self.shutdown_requested.set() @@ -489,25 +490,24 @@ def _is_single_playback_test_case_selected(self) -> bool: return self._single_playback_test_case_selected def select_playback_test_case(self, test_case: int) -> bool: - if test_case == 0: - return True - if self.opened_streams: - logger.error("Playback test case selection failed: streams are currently open.") - return False - if not self._play_list: - logger.error("Playback test case selection failed: no play steps are configured.") - return False + with self._manager_lock: + if self.opened_streams: + logger.error("Playback test case selection failed: streams are currently open.") + return False + if not self._play_list: + logger.error("Playback test case selection failed: no play steps are configured.") + return False - if test_case < 1 or test_case > len(self._play_list): - logger.error(f"Playback test case selection failed: {test_case} is outside 1-{len(self._play_list)}.") - return False - self._play_step_index = test_case - 1 - self._play_step_limit = test_case - self._single_playback_test_case_selected = True - self._label_list.clear() - self._timestamp_boundaries.clear() - logger.info(f"Selected playback test case {test_case}/{len(self._play_list)}.") - return True + if test_case < 0 or test_case >= len(self._play_list): + logger.error(f"Playback test case selection failed: {test_case} is outside 0-{len(self._play_list) - 1}.") + return False + self._play_step_index = test_case + self._play_step_limit = test_case + 1 + self._single_playback_test_case_selected = True + self._label_list.clear() + self._timestamp_boundaries.clear() + logger.info(f"Selected playback step index {test_case} (step {test_case + 1} of {len(self._play_list)}).") + return True def _create_play_label_list(self, name) -> list[str]: _labels = [] @@ -532,18 +532,19 @@ def _has_next_auto_playback_step(self) -> bool: return False def _request_auto_playback_if_needed(self, target_flags: Optional[int] = None): - _target_flags = self._flags.target_flags if target_flags is None else target_flags - if _target_flags & SDS_FLAG_MASK_START: - return - if self.opened_streams: - return - if self._has_next_auto_playback_step(): - self._flags.request_auto_playback_start() - elif self._flags.auto_playback and self._last_playback_stream_name: - if self._flags.request_auto_playback_terminate(): - _complete_msg = "Playback complete - no more steps remaining." if self._play_list else "Playback complete." - logger.info(_complete_msg) - self._request_exit_after_playback("playback complete") + with self._manager_lock: + _target_flags = self._flags.target_flags if target_flags is None else target_flags + if _target_flags & SDS_FLAG_MASK_START: + return + if self.opened_streams: + return + if self._has_next_auto_playback_step(): + self._flags.request_auto_playback_start() + elif self._flags.auto_playback and self._last_playback_stream_name: + if self._flags.request_auto_playback_terminate(): + _complete_msg = "Playback complete - no more steps remaining." if self._play_list else "Playback complete." + logger.info(_complete_msg) + self._request_exit_after_playback("playback complete") def _request_exit_after_playback(self, _reason: str): if not self._exit_after_playback: @@ -554,6 +555,10 @@ def _request_exit_after_playback(self, _reason: str): if self._loop and self._main_task: self._loop.call_soon_threadsafe(self._main_task.cancel) def _open(self, mode, name): + with self._manager_lock: + return self._open_locked(mode, name) + + def _open_locked(self, mode, name): _cmd = CMD_OPEN # prepare error response _resp_err = bytearray() @@ -590,10 +595,7 @@ def _open(self, mode, name): _step = self._play_list[self._play_step_index] _step_desc = _step.get('step', '') _desc_suffix = f": {_step_desc}" if _step_desc else "" - if self._is_single_playback_test_case_selected(): - logger.info(f"Playback step {self._play_step_index + 1}{_desc_suffix}.") - else: - logger.info(f"Playback step {self._play_step_index + 1}/{len(self._play_list)}{_desc_suffix}.") + logger.info(f"Playback step index {self._play_step_index} (step {self._play_step_index + 1} of {len(self._play_list)}){_desc_suffix}.") _set_flags = _step.get('setflags', 0) _clear_flags = _step.get('clearflags', 0) _recdir = _step.get('recdir', None) @@ -731,6 +733,10 @@ def _open(self, mode, name): return _resp def _close(self, sid): + with self._manager_lock: + return self._close_locked(sid) + + def _close_locked(self, sid): _resp = bytearray() _stream = self.opened_streams[sid] _name = _stream.name diff --git a/template/Board/Corstone-320/vsi/python/sdsio.py b/template/Board/Corstone-320/vsi/python/sdsio.py index 566b7e5..2508e51 100644 --- a/template/Board/Corstone-320/vsi/python/sdsio.py +++ b/template/Board/Corstone-320/vsi/python/sdsio.py @@ -246,8 +246,8 @@ def __init__( self._read_buffers = {} # sid -> ByteStreamBuffer self._read_threads = {} # sid -> Thread self._read_stop = {} # sid -> Event - # lock to protect stream_id increment and open checks - self._manager_lock = threading.Lock() + # lock to protect playback selection and stream state transitions + self._manager_lock = threading.RLock() # timestamp of last stream read or write command self.time_last_rw = time.time() # status bar @@ -293,7 +293,8 @@ def __init__( self._loop = None self._main_task = None if playback_test_case is not None: - self.select_playback_test_case(playback_test_case) + if playback_test_case < 0 or not self.select_playback_test_case(playback_test_case): + raise ValueError(f"Invalid playback test case: {playback_test_case}") def shutdown(self): self.shutdown_requested.set() @@ -489,25 +490,24 @@ def _is_single_playback_test_case_selected(self) -> bool: return self._single_playback_test_case_selected def select_playback_test_case(self, test_case: int) -> bool: - if test_case == 0: - return True - if self.opened_streams: - logger.error("Playback test case selection failed: streams are currently open.") - return False - if not self._play_list: - logger.error("Playback test case selection failed: no play steps are configured.") - return False + with self._manager_lock: + if self.opened_streams: + logger.error("Playback test case selection failed: streams are currently open.") + return False + if not self._play_list: + logger.error("Playback test case selection failed: no play steps are configured.") + return False - if test_case < 1 or test_case > len(self._play_list): - logger.error(f"Playback test case selection failed: {test_case} is outside 1-{len(self._play_list)}.") - return False - self._play_step_index = test_case - 1 - self._play_step_limit = test_case - self._single_playback_test_case_selected = True - self._label_list.clear() - self._timestamp_boundaries.clear() - logger.info(f"Selected playback test case {test_case}/{len(self._play_list)}.") - return True + if test_case < 0 or test_case >= len(self._play_list): + logger.error(f"Playback test case selection failed: {test_case} is outside 0-{len(self._play_list) - 1}.") + return False + self._play_step_index = test_case + self._play_step_limit = test_case + 1 + self._single_playback_test_case_selected = True + self._label_list.clear() + self._timestamp_boundaries.clear() + logger.info(f"Selected playback step index {test_case} (step {test_case + 1} of {len(self._play_list)}).") + return True def _create_play_label_list(self, name) -> list[str]: _labels = [] @@ -532,18 +532,19 @@ def _has_next_auto_playback_step(self) -> bool: return False def _request_auto_playback_if_needed(self, target_flags: Optional[int] = None): - _target_flags = self._flags.target_flags if target_flags is None else target_flags - if _target_flags & SDS_FLAG_MASK_START: - return - if self.opened_streams: - return - if self._has_next_auto_playback_step(): - self._flags.request_auto_playback_start() - elif self._flags.auto_playback and self._last_playback_stream_name: - if self._flags.request_auto_playback_terminate(): - _complete_msg = "Playback complete - no more steps remaining." if self._play_list else "Playback complete." - logger.info(_complete_msg) - self._request_exit_after_playback("playback complete") + with self._manager_lock: + _target_flags = self._flags.target_flags if target_flags is None else target_flags + if _target_flags & SDS_FLAG_MASK_START: + return + if self.opened_streams: + return + if self._has_next_auto_playback_step(): + self._flags.request_auto_playback_start() + elif self._flags.auto_playback and self._last_playback_stream_name: + if self._flags.request_auto_playback_terminate(): + _complete_msg = "Playback complete - no more steps remaining." if self._play_list else "Playback complete." + logger.info(_complete_msg) + self._request_exit_after_playback("playback complete") def _request_exit_after_playback(self, _reason: str): if not self._exit_after_playback: @@ -554,6 +555,10 @@ def _request_exit_after_playback(self, _reason: str): if self._loop and self._main_task: self._loop.call_soon_threadsafe(self._main_task.cancel) def _open(self, mode, name): + with self._manager_lock: + return self._open_locked(mode, name) + + def _open_locked(self, mode, name): _cmd = CMD_OPEN # prepare error response _resp_err = bytearray() @@ -590,10 +595,7 @@ def _open(self, mode, name): _step = self._play_list[self._play_step_index] _step_desc = _step.get('step', '') _desc_suffix = f": {_step_desc}" if _step_desc else "" - if self._is_single_playback_test_case_selected(): - logger.info(f"Playback step {self._play_step_index + 1}{_desc_suffix}.") - else: - logger.info(f"Playback step {self._play_step_index + 1}/{len(self._play_list)}{_desc_suffix}.") + logger.info(f"Playback step index {self._play_step_index} (step {self._play_step_index + 1} of {len(self._play_list)}){_desc_suffix}.") _set_flags = _step.get('setflags', 0) _clear_flags = _step.get('clearflags', 0) _recdir = _step.get('recdir', None) @@ -731,6 +733,10 @@ def _open(self, mode, name): return _resp def _close(self, sid): + with self._manager_lock: + return self._close_locked(sid) + + def _close_locked(self, sid): _resp = bytearray() _stream = self.opened_streams[sid] _name = _stream.name diff --git a/utilities/sdsio-server.py b/utilities/sdsio-server.py index e2b1f92..b42186d 100644 --- a/utilities/sdsio-server.py +++ b/utilities/sdsio-server.py @@ -345,16 +345,19 @@ def handle_commands(self): _set_flags = int.from_bytes(self._recv_buf[4:8], 'little') _clear_flags = int.from_bytes(self._recv_buf[8:12], 'little') _test_case = int.from_bytes(self._recv_buf[12:16], 'little') - _test_case_msg = f", test_case={_test_case}" if _test_case else "" + _test_case_msg = f", test_case={_test_case}" logger.info(f"Monitor command received: SDSIO_MON_FLAGS (set=0x{_set_flags:08X}, clear=0x{_clear_flags:08X}{_test_case_msg}).") - if _test_case: - if not self._playback_selector: - logger.warning(f"Monitor playback test case request ignored: {_test_case}.") - del self._recv_buf[:16] - continue - if not self._playback_selector(_test_case): - del self._recv_buf[:16] - continue + if _test_case < 0: + logger.error(f"Monitor playback test case request failed: {_test_case} is invalid.") + del self._recv_buf[:16] + continue + if not self._playback_selector: + logger.warning(f"Monitor playback test case request ignored: {_test_case}.") + del self._recv_buf[:16] + continue + if not self._playback_selector(_test_case): + del self._recv_buf[:16] + continue if self._flags: self._flags.apply(_set_flags, _clear_flags) elif _cmd == SDSIO_MON_SHUTDOWN: @@ -667,8 +670,8 @@ def __init__( self._read_buffers = {} # sid -> ByteStreamBuffer self._read_threads = {} # sid -> Thread self._read_stop = {} # sid -> Event - # lock to protect stream_id increment and open checks - self._manager_lock = threading.Lock() + # lock to protect playback selection and stream state transitions + self._manager_lock = threading.RLock() # timestamp of last stream read or write command self.time_last_rw = time.time() # status bar @@ -714,7 +717,8 @@ def __init__( self._loop = None self._main_task = None if playback_test_case is not None: - self.select_playback_test_case(playback_test_case) + if playback_test_case < 0 or not self.select_playback_test_case(playback_test_case): + raise ValueError(f"Invalid playback test case: {playback_test_case}") def shutdown(self): self.shutdown_requested.set() @@ -910,25 +914,24 @@ def _is_single_playback_test_case_selected(self) -> bool: return self._single_playback_test_case_selected def select_playback_test_case(self, test_case: int) -> bool: - if test_case == 0: - return True - if self.opened_streams: - logger.error("Playback test case selection failed: streams are currently open.") - return False - if not self._play_list: - logger.error("Playback test case selection failed: no play steps are configured.") - return False + with self._manager_lock: + if self.opened_streams: + logger.error("Playback test case selection failed: streams are currently open.") + return False + if not self._play_list: + logger.error("Playback test case selection failed: no play steps are configured.") + return False - if test_case < 1 or test_case > len(self._play_list): - logger.error(f"Playback test case selection failed: {test_case} is outside 1-{len(self._play_list)}.") - return False - self._play_step_index = test_case - 1 - self._play_step_limit = test_case - self._single_playback_test_case_selected = True - self._label_list.clear() - self._timestamp_boundaries.clear() - logger.info(f"Selected playback test case {test_case}/{len(self._play_list)}.") - return True + if test_case < 0 or test_case >= len(self._play_list): + logger.error(f"Playback test case selection failed: {test_case} is outside 0-{len(self._play_list) - 1}.") + return False + self._play_step_index = test_case + self._play_step_limit = test_case + 1 + self._single_playback_test_case_selected = True + self._label_list.clear() + self._timestamp_boundaries.clear() + logger.info(f"Selected playback step index {test_case} (step {test_case + 1} of {len(self._play_list)}).") + return True def _create_play_label_list(self, name) -> list[str]: _labels = [] @@ -953,18 +956,19 @@ def _has_next_auto_playback_step(self) -> bool: return False def _request_auto_playback_if_needed(self, target_flags: Optional[int] = None): - _target_flags = self._flags.target_flags if target_flags is None else target_flags - if _target_flags & SDS_FLAG_MASK_START: - return - if self.opened_streams: - return - if self._has_next_auto_playback_step(): - self._flags.request_auto_playback_start() - elif self._flags.auto_playback and self._last_playback_stream_name: - if self._flags.request_auto_playback_terminate(): - _complete_msg = "Playback complete - no more steps remaining." if self._play_list else "Playback complete." - logger.info(_complete_msg) - self._request_exit_after_playback("playback complete") + with self._manager_lock: + _target_flags = self._flags.target_flags if target_flags is None else target_flags + if _target_flags & SDS_FLAG_MASK_START: + return + if self.opened_streams: + return + if self._has_next_auto_playback_step(): + self._flags.request_auto_playback_start() + elif self._flags.auto_playback and self._last_playback_stream_name: + if self._flags.request_auto_playback_terminate(): + _complete_msg = "Playback complete - no more steps remaining." if self._play_list else "Playback complete." + logger.info(_complete_msg) + self._request_exit_after_playback("playback complete") def _request_exit_after_playback(self, _reason: str): if not self._exit_after_playback: @@ -975,6 +979,10 @@ def _request_exit_after_playback(self, _reason: str): if self._loop and self._main_task: self._loop.call_soon_threadsafe(self._main_task.cancel) def _open(self, mode, name): + with self._manager_lock: + return self._open_locked(mode, name) + + def _open_locked(self, mode, name): _cmd = CMD_OPEN # prepare error response _resp_err = bytearray() @@ -1011,10 +1019,7 @@ def _open(self, mode, name): _step = self._play_list[self._play_step_index] _step_desc = _step.get('step', '') _desc_suffix = f": {_step_desc}" if _step_desc else "" - if self._is_single_playback_test_case_selected(): - logger.info(f"Playback step {self._play_step_index + 1}{_desc_suffix}.") - else: - logger.info(f"Playback step {self._play_step_index + 1}/{len(self._play_list)}{_desc_suffix}.") + logger.info(f"Playback step index {self._play_step_index} (step {self._play_step_index + 1} of {len(self._play_list)}){_desc_suffix}.") _set_flags = _step.get('setflags', 0) _clear_flags = _step.get('clearflags', 0) _recdir = _step.get('recdir', None) @@ -1152,6 +1157,10 @@ def _open(self, mode, name): return _resp def _close(self, sid): + with self._manager_lock: + return self._close_locked(sid) + + def _close_locked(self, sid): _resp = bytearray() _stream = self.opened_streams[sid] _name = _stream.name @@ -2253,8 +2262,8 @@ def _parse_playback_selection(value): _test_case = int(_value, 0) except ValueError: raise argparse.ArgumentTypeError(f"Invalid playback test case: {value}!") - if _test_case < 1: - raise argparse.ArgumentTypeError("Playback test case must be 1 or greater!") + if _test_case < 0: + raise argparse.ArgumentTypeError("Playback test case must be 0 or greater!") return True, _test_case def ip_validator(ip_str): @@ -2339,8 +2348,8 @@ def _format_action_invocation(self, action): "\n" "examples:\n" " %(prog)s -c sdsio.yml # Recommended: all config in YAML\n" - " %(prog)s -c sdsio.yml --playback # Playback mode\n" - " %(prog)s -c sdsio.yml --playback 2 # Playback test case 2\n" + " %(prog)s -c sdsio.yml --playback \"*\" # Playback mode\n" + " %(prog)s -c sdsio.yml --playback 0 # Playback test case 0\n" " %(prog)s -c sdsio.yml --mon-port 6060 # With VS Code SDS extension monitor\n" " %(prog)s usb --workdir ./data # USB interface, explicit work dir\n" " %(prog)s socket --port 5050 # Socket interface\n" @@ -2389,8 +2398,8 @@ def _add_info_opts(p, version_text=None): def _add_general_opts(p): """Append a general-opts group to subparser p (called after server-specific groups).""" _g = p.add_argument_group("general-opts") - _g.add_argument("--playback", "-p", dest="auto_playback", nargs="?", const="all", metavar="", - help="Start SDSIO-Server in playback mode; optionally select a playback test case or '*' for all", + _g.add_argument("--playback", "-p", dest="auto_playback", nargs="?", const="all", metavar="", + help="Start SDSIO-Server in playback mode; optionally select a zero-based playback step index or '*' for all", default=argparse.SUPPRESS) _g.add_argument("--exit-after-playback", "-x", dest="exit_after_playback", action="store_true", help="Terminate when playback is completed", @@ -2497,8 +2506,8 @@ def _add_general_opts(p): _add_general_opts(_parser_usb) _general = _parser.add_argument_group("general-opts") - _general.add_argument("--playback", "-p", dest="auto_playback", nargs="?", const="all", metavar="", - help="Start SDSIO-Server in playback mode; optionally select a playback test case or '*' for all", default=None) + _general.add_argument("--playback", "-p", dest="auto_playback", nargs="?", const="all", metavar="", + help="Start SDSIO-Server in playback mode; optionally select a zero-based playback step index or '*' for all", default=None) _general.add_argument("--exit-after-playback", "-x", dest="exit_after_playback", action="store_true", help="Terminate when playback is completed", default=None) _general.add_argument("--no-progress-info", "-n", dest="no_progress_info", action="store_true", @@ -2686,8 +2695,8 @@ async def main(): if not _play_list: logger.error("Playback test case selection requires play steps in SDSIO configuration YAML.") sys.exit(1) - if _playback_test_case > len(_play_list): - logger.error(f"Playback test case {_playback_test_case} is outside 1-{len(_play_list)}.") + if _playback_test_case >= len(_play_list): + logger.error(f"Playback test case {_playback_test_case} is outside 0-{len(_play_list) - 1}.") sys.exit(1) _exit_after_playback = _args.exit_after_playback if _args.exit_after_playback else False _no_progress_info = _args.no_progress_info if _args.no_progress_info else False