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 62f358a..9b62327 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,21 @@ 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 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 | +|******|**********|************|**********| ``` +Valid `TestCase` values are: + +- `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. + **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..255f407 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 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. @@ -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 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) @@ -277,12 +278,20 @@ 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 +python sdsio-server.py -c sdsio.yml --playback "*" ``` +Start SDSIO-Server automatically starting only the first configured `play:` step: + +```bash +python sdsio-server.py -c sdsio.yml --playback 0 +``` + +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. ```bash diff --git a/template/Board/Corstone-300/vsi/python/sdsio.py b/template/Board/Corstone-300/vsi/python/sdsio.py index a84c5ba..2508e51 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, @@ -245,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 @@ -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,9 @@ def __init__( except RuntimeError: self._loop = None self._main_task = None + if playback_test_case is not None: + 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() @@ -473,9 +479,40 @@ 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: + 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 < 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 = [] - 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,24 +526,25 @@ 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 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: @@ -517,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() @@ -549,11 +591,11 @@ 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}.") + 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) @@ -691,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 a84c5ba..2508e51 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, @@ -245,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 @@ -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,9 @@ def __init__( except RuntimeError: self._loop = None self._main_task = None + if playback_test_case is not None: + 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() @@ -473,9 +479,40 @@ 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: + 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 < 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 = [] - 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,24 +526,25 @@ 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 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: @@ -517,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() @@ -549,11 +591,11 @@ 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}.") + 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) @@ -691,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 1d40829..b42186d 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,20 @@ 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}" + 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 < 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: @@ -633,6 +647,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, @@ -655,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 @@ -670,6 +685,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 +697,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 +716,9 @@ def __init__( except RuntimeError: self._loop = None self._main_task = None + if playback_test_case is not None: + 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() @@ -883,9 +903,40 @@ 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: + 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 < 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 = [] - 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,24 +950,25 @@ 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 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: @@ -927,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() @@ -959,11 +1015,11 @@ 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}.") + 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) @@ -1101,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 @@ -2192,6 +2252,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 < 0: + raise argparse.ArgumentTypeError("Playback test case must be 0 or greater!") + return True, _test_case + def ip_validator(ip_str): try: ipaddress.ip_address(ip_str) @@ -2274,7 +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 \"*\" # 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" @@ -2323,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", 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 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", @@ -2431,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", 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 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", @@ -2602,11 +2677,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 +2685,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 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 + _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: