Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion documentation/sdsio.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
15 changes: 11 additions & 4 deletions documentation/theory.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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,
Expand Down
15 changes: 12 additions & 3 deletions documentation/utilities.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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 <step_index|*> 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 <path> Directory for SDS files (overrides *.sdsio.yml setting; default: current directory)
Expand Down Expand Up @@ -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
Expand Down
86 changes: 66 additions & 20 deletions template/Board/Corstone-300/vsi/python/sdsio.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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:
Expand All @@ -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()
Expand Down Expand Up @@ -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:
Expand All @@ -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:
Expand All @@ -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()
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down
Loading
Loading