Datalogging#
Usage of the SDK watchables as described in Accessing variables is done through polling and the update rate is limited by the bandwidth of the communication link between the server and the device (Serial, TCP, CAN, etc). Therefore, using Python to monitor a value may not reliably capture a rapid event in the firmware, as the interval between two polling events could span several milliseconds.
Datalogging (or Embedded Graphs) addresses this issue by instructing the device to record a specific set of watchables in a circular buffer until a certain event occurs. This effectively turns the embedded device into a scope.
The condition that stop the circular acquisition is called the Trigger condition and may be of many type. Once the trigger condition is met, the acquisition finishes (either immediately or after a specified amount of time) and the data is returned to the server to be saved into a database. A client can then download and display that data under the form of a graph.
Configuring the datalogger#
The first step before configuring the datalogger is knowing what the device is capable of in terms of datalogging. We need information such as :
Is datalogging even supported?
What are the sampling rates?
What is the size of the buffer?
How many signal can I log?
etc
This information can be obtained using the ScrutinyClient.get_datalogging_capabilities
method,
which returns a DataloggingCapabilities
object.
- ScrutinyClient.get_datalogging_capabilities()[source]#
Gets the device capabilities in terms of datalogging. This information includes the available sampling rates, the datalogging buffer size, the data encoding format and the maximum number of signals.
- Raises:
OperationFailure – If the request to the server fails
- Returns:
The datalogging capabilities
- Return type:
- class scrutiny.sdk.datalogging.DataloggingCapabilities[source]#
(Immutable struct) Tells what the device is able to achieve in terms of datalogging
- buffer_size: int#
Size of the datalogging buffer
- encoding: DataloggingEncoding#
The encoding of data
- max_nb_signal: int#
Maximum number of signal per acquisition (including time if measured)
- sampling_rates: List[SamplingRate]#
List of available sampling rates
Configuring and arming the datalogger is possible with the ScrutinyClient.start_datalog
method
which takes a sdk.DataloggingConfig
as sole argument.
- ScrutinyClient.start_datalog(config)[source]#
Requires the device to make a datalogging acquisition based on the given configuration
- Parameters:
config (DataloggingConfig) – The datalogging configuration including sampling rate, signals to log, trigger condition and operands, etc.
- Raises:
OperationFailure – If the request to the server fails
ValueError – Bad parameter value
TypeError – Given parameter not of the expected type
- Returns:
A DataloggingRequest handle that can provide the status of the acquisition process and used to fetch the data.
- Return type:
- class scrutiny.sdk.datalogging.DataloggingConfig[source]#
A datalogging acquisition configuration. Contains all the configurable parameters for an acquisition
- __init__(sampling_rate, decimation=1, timeout=0.0, name='')[source]#
Creates an instance of
DataloggingConfig
- Parameters:
sampling_rate (int | SamplingRate) – The acquisition sampling rate. Can be the sampling rate ID in the device or an instance of
SamplingRate
gotten from theDataloggingCapabilities
returned byScrutinyClient.get_datalogging_capabilities
decimation (int) – The decimation factor that reduces the effective sampling rate
timeout (float) – Timeout to the acquisition. After the datalogger is armed, it will forcefully trigger after this amount of time. 0 means no timeout
name (str) – Name of the configuration. Save into the database for reference
- Raises:
TypeError – Given parameter not of the expected type
ValueError – Given parameter has an invalid value
- add_axis(name)[source]#
Adds a Y axis to the acquisition.
- Parameters:
name (str) – The name of the axis, for display purpose.
- Returns:
An
AxisDefinition
object that can be assigned to a signal when callingadd_signal()
- Return type:
- add_signal(signal, axis, name=None)[source]#
Adds a signal to the acquisition
- Parameters:
signal (WatchableHandle | str) – The signal to add. Can either be a path to a var/rpv/alias (string) or a
WatchableHandle
given byScrutinyClient.watch()
axis (AxisDefinition | int) – The Y axis to assigned this signal to. Can either be the index (int) or the
AxisDefinition
object given byadd_axis()
name (str | None) – A display name for the signal
- Raises:
IndexError – Invalid axis index
ValueError – Bad parameter value
TypeError – Given parameter not of the expected type
- Return type:
None
- configure_trigger(condition, operands=None, position=0.5, hold_time=0)[source]#
Configure the required conditions to fire the trigger event
- Parameters:
condition (TriggerCondition) – The type of condition used for triggering the acquisition.
operands (List[WatchableHandle | float | str] | None) – List of operands. Each operands can be a constant number (float), the path to a variable/rpv/alias (str) or a
WatchableHandle
given byScrutinyClient.watch()
. The number of operands depends on the trigger conditionposition (float) – Position of the trigger event in the datalogging buffer. Value from 0 to 1, where 0 is leftmost, 0.5 middle and 1 rightmost.
hold_time (float) – Time in seconds that the trigger condition must evaluate to true before firing the trigger event.
- Raises:
ValueError – Bad parameter value
TypeError – Given parameter not of the expected type
- Return type:
None
- configure_xaxis(axis_type, signal=None, name=None)[source]#
Configures the X-Axis
- Parameters:
axis_type (XAxisType) – Type of X-Axis.
signal (str | WatchableHandle | None) – The signal to be used for the X-Axis if its type is set to
Signal
. Ignored if the X-Axis type is notSignal
. Can be the path to a watchable or an instance of aWatchableHandle
given byScrutinyClient.watch()
name (str | None) – A display name for the X-Axis
- Raises:
ValueError – Bad parameter value
TypeError – Given parameter not of the expected type
- Return type:
None
- class scrutiny.sdk.datalogging.TriggerCondition[source]#
(Enum) The type of trigger condition to use.
- AlwaysTrue = 'true'#
Always true. Triggers immediately after being armed
- ChangeMoreThan = 'cmt'#
X=(Operand1[n]-Operand1[n-1]); |X| > |Operand2| && sign(X) == sign(Operand2)
- Equal = 'eq'#
Operand1 == Operand2
- GreaterOrEqualThan = 'get'#
Operand1 >= Operand2
- GreaterThan = 'gt'#
Operand1 > Operand2
- IsWithin = 'within'#
|Operand1 - Operand2| < |Operand3|
- LessOrEqualThan = 'let'#
Operand1 <= Operand2
- LessThan = 'lt'#
Operand1 < Operand2
- NotEqual = 'neq'#
Operand1 != Operand2
- class scrutiny.sdk.datalogging.XAxisType[source]#
(Enum) Represent a type of X-Axis that a user can select
- IdealTime = 'ideal_time'#
Time deduced from the sampling frequency. Does not require space in the datalogging buffer. Only available for fixed frequency loops
- Indexed = 'index'#
No signal will be captured for the X-Axis and the returned X-Axis data will be the index of the samples
- MeasuredTime = 'measured_time'#
Time measured by the device. Requires space for a 32 bits value in the datalogging buffer
- Signal = 'signal'#
X-Axis is an arbitrary signal, not time.
- class scrutiny.sdk.datalogging.AxisDefinition[source]#
(Immutable struct) Represent an axis
- axis_id: int#
A unique ID used to identify the axis
- name: str#
The name of the axis. Used for display
- class scrutiny.sdk.datalogging.SamplingRate[source]#
(Immutable struct) Represent a sampling rate supported by the device
- identifier: int#
The unique identifier of the sampling rate. Matches the embedded device index in the loop array set in the configuration
- name: str#
Name for display
- class scrutiny.sdk.datalogging.DataloggingRequest[source]#
Handle to a request for a datalogging acquisition. Gets updated by the client and reflects the actual status of the acquisition
- wait_for_completion(timeout=None)[source]#
Wait for the acquisition to be triggered and extracted by the server. Once this is done, the
acquisition_reference_id
will not beNone
anymore and its value will point to the database entry storing the data.- Params timeout:
Maximum wait time in seconds. Waits forever if
None
- Raises:
TimeoutException – If the acquisition does not complete in less than the specified timeout value
OperationFailure – If an error happened that prevented the acquisition to successfully complete
- Parameters:
timeout (float | None) –
- Return type:
None
- fetch_acquisition(timeout=None)[source]#
Download and returns an acquisition data from the server. The acquisition must be complete
- Params timeout:
Timeout to get a response by the server in seconds. Uses the default timeout value if
None
- Raises:
TimeoutException – If the server does not respond in time
OperationFailure – If the acquisition is not complete or if an error happens while fetching the data
- Returns:
The
DataloggingAcquisition
object containing the acquired data- Parameters:
timeout (float | None) –
- Return type:
- wait_and_fetch(timeout=None, fetch_timeout=None)[source]#
Do successive calls to
wait_for_completion()
&fetch_acquisition()
and return the acquisition- Params timeout:
Timeout given to
wait_for_completion()
- Params fetch_timeout:
Timeout given to
fetch_acquisition()
- Raises:
TimeoutException – If any of the timeout is violated
OperationFailure – If a problem occur while waiting/fetching
- Returns:
The
DataloggingAcquisition
object containing the acquired data- Parameters:
timeout (float | None) –
fetch_timeout (float | None) –
- Return type:
- property acquisition_reference_id: str | None#
The unique ID used to fetch the acquisition data from the server. Value is set only if request is completed and succeeded.
None
otherwise
Example#
with client.connect(hostname, port):
var1 = client.watch('/a/b/var1')
var2 = client.watch('/a/b/var2')
config = sdk.datalogging.DataloggingConfig(sampling_rate=0, decimation=1, timeout=0, name="MyGraph")
config.configure_trigger(sdk.datalogging.TriggerCondition.GreaterThan, [var1, 3.14159], position=0.75, hold_time=0)
config.configure_xaxis(sdk.datalogging.XAxisType.MeasuredTime)
axis1 = config.add_axis('Axis 1')
axis2 = config.add_axis('Axis 2')
config.add_signal(var1, axis1, name="MyVar1")
config.add_signal(var2, axis1, name="MyVar2")
config.add_signal('/a/b/alias_rpv1000', axis2, name="MyAliasRPV1000")
request = client.start_datalog(config)
timeout = 60
print(f"Embedded datalogger armed. Waiting for MyVar1 >= 3.14159...")
try:
request.wait_for_completion(timeout) # Wait for the trigger condition to be fulfilled
except sdk.exceptions.TimeoutException:
print(f'Timed out while waiting')
if request.completed: # Will be False if timed out
if request.is_success:
acquisition = request.fetch_acquisition()
filename = "my_acquisition.csv"
acquisition.to_csv(filename)
print(f"Acquisition [{acquisition.reference_id}] saved to CSV format in {filename}")
else:
print(f"The datalogging acquisition failed. Reason: {request.failure_reason}")
Reading an acquisition after completion#
A DataloggingAcquisition
represent what the datalogger has captured.
It contains multiple data series, some acquired by the device and some generated by the server (like the ideal time data series crafted from the sampling frequency).
The content of a data series is converted to double-precision floating point values so they can be more easily plotted or manipulated,
Note
Double precision (64 bits) floating point values have a mantissa of 52 bits, therefore, only 64bits integers greater than 2^52 may lose precision during that conversion. The choice of auto-converting the values seemed to offer more advantages than disadvantage in the majority of real use cases.
Data series are tied to an axis. There can be a single X-Axis and multiple Y-Axis.
A DataloggingAcquisition
can be obtained by calling either
DataloggingRequest.fetch_acquisition()
orDataloggingRequest.wait_and_fetch()
ScrutinyClient.read_datalogging_acquisition
to read a past acquisition stored in the database.
Once a DataloggingAcquisition
is obtained,
the DataloggingAcquisition.to_csv()
can be used to export the data
- class scrutiny.core.datalogging.DataloggingAcquisition[source]#
Represent an acquisition of multiple signals
- reference_id: str#
ID used to reference the acquisition in the storage
- firmware_id: str#
Firmware ID of the device on which the acquisition has been taken
- acq_time: datetime#
Time at which the acquisition has been taken
- xdata: DataSeries#
The series of data that represent the X-Axis
- name: str | None#
A display name associated with the acquisition for easier management
- ydata: List[DataSeriesWithAxis]#
List of data series acquired
- trigger_index: int | None#
Sample index of the trigger
- firmware_name: str | None#
The firmware name taken from the metadata of the SFD loaded when the acquisition was made.
None
if it is not available
- to_csv(filename)[source]#
Export a
DataloggingAcquisition
content to a csv file- Parameters:
filename (str) – The file to write to
- Return type:
None
- class scrutiny.core.datalogging.DataSeriesWithAxis[source]#
(Immutable struct) Dataseries tied to an axis definition
- series: DataSeries#
The dataseries containing the acquisition data
- axis: AxisDefinition#
The Y-Axis to which the dataseries is bound to
- class scrutiny.core.datalogging.DataSeries[source]#
A data series is a series of measurement represented by a series of 64 bits floating point value
- name: str#
The name of the data series. Used for display
- logged_element: str#
The server element that was the source of the data. Path to a variable, alias or RPV (Runtime Published Value)
- data: List[float]#
The data stored as a list of 64 bits float
Fetching an acquisition from the database#
The server maintain a local sqlite database of all the acquisition captured. In most use case relevant for this SDK, a user will want to download an acquisition that just got triggered, but it is also possible to browse the database and download past acquisitions (which is also possible through the CLI)
Each acquisition has a unique ID called the reference_id
.
On can list the acquisition available by calling ScrutinyClient.list_stored_datalogging_acquisitions
and once the reference_id
is known, it can be passed to
ScrutinyClient.read_datalogging_acquisition()
- ScrutinyClient.list_stored_datalogging_acquisitions(timeout=None)[source]#
Gets the list of datalogging acquisition stored in the server database
- Parameters:
timeout (float | None) – The request timeout value. The default client timeout will be used if set to
None
Defaults toNone
- Raises:
OperationFailure – If fetching the list fails
- Returns:
A list of database entries, each one representing an acquisition in the database with reference_id as its unique identifier
- Return type:
List[DataloggingStorageEntry]
- ScrutinyClient.read_datalogging_acquisition(reference_id, timeout=None)[source]#
Reads a datalogging acquisition from the server storage identified by its reference ID
- Parameters:
reference_id (str) – The acquisition unique ID
timeout (float | None) – The request timeout value. The default client timeout will be used if set to
None
Defaults toNone
- Raises:
OperationFailure – If fetching the acquisition fails
- Returns:
An object containing the acquisition, including the data, the axes, the trigger index, the graph name, etc
- Return type:
- class scrutiny.sdk.datalogging.DataloggingStorageEntry[source]#
(Immutable struct) Represent an entry in datalogging storage
- reference_id: str#
Database ID used to uniquely identified this acquisition
- firmware_id: str#
Firmware ID of the device that took the acquisition
- name: str#
Name of the acquisition. For display purpose
- timestamp: datetime#
Date/Time at which the acquisition was captured
- firmware_metadata: SFDMetadata | None#
The metadata of the firmware used by the device if available
Example#
with client.connect(hostname, port):
entries = client.list_stored_datalogging_acquisitions()
print(f"The server has {len(entries)} acquisition stored")
for entry in entries:
dt = entry.timestamp.strftime(r"%Y-%m-%d %H:%M:%S")
print(f"[{entry.reference_id}] {entry.name} taken on {dt}")
assert (len(entries) > 0, "Cannot fetch first datalogging acquisition")
acquisition = client.read_datalogging_acquisition(entries[0].reference_id) # Read the first one
print(f"Acquisition {acquisition.name} [{acquisition.reference_id}] downloaded and has {len(acquisition.ydata)} signals on the Y-Axis. ")
acquisition.to_csv("myfile.csv")