Source code for capture2go.pkg

# SPDX-FileCopyrightText: 2025 SensorStim Neurotechnology GmbH <support@capture2go.com>
#
# SPDX-License-Identifier: MIT

# WARNING: This file is automatically generated. Do not edit manually!

import ctypes
import enum
import math
import struct
from typing import Type, TypeVar
import zlib

import numpy as np

from .utils import qmult, quatFromGyr, decodeQuat, addHeading


T_AbstractPackage = TypeVar('T_AbstractPackage', bound='AbstractPackage')

packages: dict['SensorHeader', Type['AbstractPackage']] = {}
"""
Global package registry that maps protocol header values to package classes.

This dictionary is populated by the :py:func:`register_package` decorator. It enables lookup of the
appropriate package class for a given header value when parsing protocol messages.

Key:
    SensorHeader: The header value identifying the package type.
Value:
    Type[AbstractPackage]: The class implementing the package structure for that header.
"""


[docs] def register_package(cls: Type[T_AbstractPackage]) -> Type[T_AbstractPackage]: """ Decorator to register a package class. This function is used as a class decorator for subclasses of AbstractPackage. It ensures that each package class is registered in the global `packages` dictionary, mapping its header value to the class type. This enables automatic lookup and instantiation of package classes based on header values in the protocol. Args: cls (Type[T_AbstractPackage]): The package class to register. Must have a unique `header` attribute. Returns: Type[T_AbstractPackage]: The same class, unmodified. Raises: AssertionError: If the class has no header, or if the header or class is already registered. """ global packages assert cls.header is not None assert cls.header not in packages assert cls not in packages.values() packages[cls.header] = cls return cls
[docs] class AbstractPackage(ctypes.Structure): """ Base class for all protocol package structures. This class provides the common interface and serialization logic for all protocol packages. Subclasses should define the appropriate fields and set the `header` attribute to a unique value. """ header: 'SensorHeader | None' = None """ The protocol header value associated with this package type. This should be set to a unique value for each subclass, or None for abstract base classes. """ _pack_ = 1 _layout_ = 'ms' _fields_ = []
[docs] @classmethod def frombytes(cls, val: bytes): """ Create a package instance from a bytes object. Args: val (bytes): The raw bytes to parse into a package instance. Returns: AbstractPackage: An instance of the package class with fields populated from the bytes. """ return cls.from_buffer_copy(val)
[docs] def pack(self, header: 'SensorHeader | None' = None): """ Serialize the package to bytes, including header and CRC. Args: header (SensorHeader | None, optional): The header value to use. If None, uses self.header. Returns: bytes: The serialized package as a byte string, ready for transmission. """ if header is None: header = self.header data = struct.pack('<H', header) + bytes(self) crc = zlib.crc32(data) return struct.pack('<BIB', 2, crc, len(data) - 2) + data
[docs] def parse(self): """ Convert the package fields to a dictionary. Subclasses may override this method to return data as numpy arrays with approprate shapes and apply useful processing like converting from fixed point numbers to floats in physical units. Returns: dict: A dictionary mapping field names to their values. """ return {f: getattr(self, f) for f, _ in self._fields_} # type: ignore
def __repr__(self): return f'{self.__class__.__name__}({self.parse()})'
MAX_PAYLOAD_SIZE = 236 MAX_PKG_SIZE = 244 GYR_SCALE_FACTOR = np.deg2rad(2000.0) / 32768.0 ACC_SCALE_FACTOR = 16.0 / 32768.0 * 9.81 MAG_SCALE_FACTOR = 1.0 / 16.0 DELTA_SCALE_FACTOR = math.pi / 32768.0 GYR_BIAS_SCALE_FACTOR = np.deg2rad(2.0) / 32768.0 # IMU data error flags (member errorFlags) ERROR_FLAG_TIME_GAP = 0x01 ERROR_FLAG_GYR_CLIPPING = 0x02 ERROR_FLAG_ACC_CLIPPING = 0x04 ERROR_FLAG_MAG_CLIPPING = 0x08 ERROR_FLAG_PROCESSING_ISSUE = 0x10
[docs] @enum.unique class SensorHeader(enum.IntEnum): CMD_GET_DEVICE_INFO = 0x0070 """Requests device information.""" DATA_DEVICE_INFO = 0x0071 """Device information.""" _RESERVED01 = 0x00A0 _RESERVED02 = 0x00A1 _RESERVED03 = 0x0103 _RESERVED04 = 0x0104 _RESERVED05 = 0x0105 _RESERVED06 = 0x0106 # Sleep and deep sleep CMD_SLEEP = 0x0110 """Puts device into sleep mode.""" ACK_SLEEP = 0x0111 """Acknowledges sleep command.""" CMD_DEEP_SLEEP = 0x0112 """Puts device into deep sleep mode (transport mode).""" ACK_DEEP_SLEEP = 0x0113 """Acknowledges deep sleep command.""" # Main measurement configuration CMD_SET_MEASUREMENT_MODE = 0x0120 """Sets measurement mode.""" CMD_GET_MEASUREMENT_MODE = 0x0121 """Requests measurement mode.""" DATA_MEASUREMENT_MODE = 0x0122 """Measurement mode.""" # Burst measurement CMD_SET_MEASUREMENT_BURST_MODE = 0x0123 """Sets measurement burst mode.""" CMD_GET_MEASUREMENT_BURST_MODE = 0x0124 """Requests measurement burst mode.""" DATA_MEASUREMENT_BURST_MODE = 0x0125 """Measurement burst mode.""" _RESERVED07 = 0x0130 _RESERVED08 = 0x0131 _RESERVED09 = 0x0132 # Recording configuration CMD_SET_RECORDING_CONFIG = 0x0140 """Sets recording configuration.""" CMD_GET_RECORDING_CONFIG = 0x0141 """Requests recording configuration.""" DATA_RECORDING_CONFIG = 0x0142 """Recording configuration.""" # Start and stop measurement CMD_START_STREAMING = 0x0150 """Starts data streaming.""" ACK_START_STREAMING = 0x0151 """Acknowledges start streaming command.""" CMD_STOP_STREAMING = 0x0152 """Stops data streaming.""" ACK_STOP_STREAMING = 0x0153 """Acknowledges stop streaming command.""" CMD_START_RECORDING = 0x0154 """Starts recording data to internal storage.""" ACK_START_RECORDING = 0x0155 """Acknowledges start recording command.""" CMD_STOP_RECORDING = 0x0156 """Stops data recording.""" ACK_STOP_RECORDING = 0x0157 """Acknowledges stop recording command.""" CMD_STOP_STREAMING_AND_CLEAR_BUFFER = 0x0158 """ Stops streaming and clears the send buffer. Used after connecting to a sensor in streaming mode. Note: Clearing the buffer can cause partial packages to be sent, which must be handled when unpacking. """ ACK_STOP_STREAMING_AND_CLEAR_BUFFER = 0x0159 """Acknowledges stop streaming and clear buffer command.""" # Streaming via the real-time channel CMD_START_REAL_TIME_STREAMING = 0x0160 """Starts real-time data streaming via dedicated channel.""" CMD_GET_REAL_TIME_STREAMING_MODE = 0x0161 """Requests current real-time streaming mode.""" DATA_REAL_TIME_STREAMING_MODE = 0x0162 """Real-time streaming mode.""" CMD_STOP_REAL_TIME_STREAMING = 0x0163 """Stops real-time data streaming.""" ACK_STOP_REAL_TIME_STREAMING = 0x0164 """Acknowledges stop real-time streaming command.""" # Clock CMD_SET_ABSOLUTE_TIME = 0x0170 """Sets the absolute time on the device. Sent from host to sync sender.""" DATA_ABSOLUTE_TIME = 0x0171 """Absolute time.""" DATA_CLOCK_ROUNDTRIP = 0x0172 """Clock roundtrip package for clock drift estimation.""" # LED CMD_SET_LED_CONFIG = 0x0180 """Sets LED configuration parameters.""" CMD_GET_LED_CONFIG = 0x0181 """Requests LED configuration parameters.""" DATA_LED_CONFIG = 0x0182 """LED configuration parameters.""" CMD_SET_LED_MODE = 0x0183 """Sets LED mode.""" CMD_GET_LED_MODE = 0x0184 """Requests LED mode.""" DATA_LED_MODE = 0x0185 """LED mode.""" # Sync output CMD_SET_SYNC_OUTPUT_MODE = 0x0186 """Configures a sync output pulse.""" DATA_SYNC_OUTPUT_MODE = 0x0187 """Sync output pulse configuration.""" _RESERVED10 = 0x0190 _RESERVED11 = 0x0191 _RESERVED12 = 0x0192 _RESERVED13 = 0x0193 _RESERVED14 = 0x0194 _RESERVED15 = 0x0195 # Status CMD_GET_STATUS = 0x0200 """Requests current device status information.""" DATA_STATUS = 0x0201 """Device status.""" _RESERVED16 = 0x0210 _RESERVED17 = 0x0211 _RESERVED18 = 0x0212 _RESERVED19 = 0x0213 _RESERVED20 = 0x0214 # Sensor data DATA_FULL_PACKED_200HZ = 0x0221 """Full IMU data at 200 Hz, 8 samples, packaged at 25 Hz (200/8 Hz).""" DATA_FULL_PACKED_100HZ = 0x0222 """Full IMU data at 100 Hz, 8 samples, packaged at 12.5 Hz (100/8 Hz).""" DATA_FULL_PACKED_50HZ = 0x0223 """Full IMU data at 50 Hz, 8 samples, packaged at 6.25 Hz (50/8 Hz).""" DATA_FULL_PACKED_25HZ = 0x0224 """Full IMU data at 25 Hz, 8 samples, packaged at 3.125 Hz (25/8 Hz).""" DATA_FULL_PACKED_10HZ = 0x0225 """Full IMU data at 10 Hz, 8 samples, packaged at 1.25 Hz (10/8 Hz).""" DATA_FULL_PACKED_1HZ = 0x0226 """Full IMU data at 1 Hz, 8 samples, packaged at 0.125 Hz (1/8 Hz).""" DATA_FULL_6D_PACKED_200HZ = 0x0231 """IMU data without magnetometer at 200 Hz, 8 samples, packaged at 25 Hz (200/8 Hz).""" DATA_FULL_6D_PACKED_100HZ = 0x0232 """IMU data without magnetometer at 100 Hz, 8 samples, packaged at 12.5 Hz (100/8 Hz).""" DATA_FULL_6D_PACKED_50HZ = 0x0233 """IMU data without magnetometer at 50 Hz, 8 samples, packaged at 6.25 Hz (50/8 Hz).""" DATA_FULL_6D_PACKED_25HZ = 0x0234 """IMU data without magnetometer at 25 Hz, 8 samples, packaged at 3.125 Hz (25/8 Hz).""" DATA_FULL_6D_PACKED_10HZ = 0x0235 """IMU data without magnetometer at 10 Hz, 8 samples, packaged at 1.25 Hz (10/8 Hz).""" DATA_FULL_6D_PACKED_1HZ = 0x0236 """IMU data without magnetometer at 1 Hz, 8 samples, packaged at 0.125 Hz (1/8 Hz).""" DATA_FULL_FIXED_200HZ = 0x0241 """Full IMU data sample at 200 Hz, fixed-point format.""" DATA_FULL_FIXED_100HZ = 0x0242 """Full IMU data sample at 100 Hz, fixed-point format.""" DATA_FULL_FIXED_50HZ = 0x0243 """Full IMU data sample at 50 Hz, fixed-point format.""" DATA_FULL_FIXED_25HZ = 0x0244 """Full IMU data sample at 25 Hz, fixed-point format.""" DATA_FULL_FIXED_10HZ = 0x0245 """Full IMU data sample at 10 Hz, fixed-point format.""" DATA_FULL_FIXED_1HZ = 0x0246 """Full IMU data sample at 1 Hz, fixed-point format.""" DATA_FULL_FIXED_RT = 0x0247 """Full IMU data sample for real-time transmission, fixed-point format.""" DATA_FULL_6D_FIXED_200HZ = 0x0251 """IMU data sample without magnetometer at 200 Hz, fixed-point format.""" DATA_FULL_6D_FIXED_100HZ = 0x0252 """IMU data sample without magnetometer at 100 Hz, fixed-point format.""" DATA_FULL_6D_FIXED_50HZ = 0x0253 """IMU data sample without magnetometer at 50 Hz, fixed-point format.""" DATA_FULL_6D_FIXED_25HZ = 0x0254 """IMU data sample without magnetometer at 25 Hz, fixed-point format.""" DATA_FULL_6D_FIXED_10HZ = 0x0255 """IMU data sample without magnetometer at 10 Hz, fixed-point format.""" DATA_FULL_6D_FIXED_1HZ = 0x0256 """IMU data sample without magnetometer at 1 Hz, fixed-point format.""" DATA_FULL_FLOAT_200HZ = 0x0261 """Full IMU data sample at 200 Hz, floating-point format.""" DATA_QUAT_PACKED_200HZ = 0x0271 """Orientation data at 200 Hz, 20 samples, packaged at 10 Hz (200/20 Hz).""" DATA_QUAT_PACKED_100HZ = 0x0272 """Orientation data at 100 Hz, 20 samples, packaged at 5 Hz (100/20 Hz).""" DATA_QUAT_PACKED_50HZ = 0x0273 """Orientation data at 50 Hz, 20 samples, packaged at 2.5 Hz (50/20 Hz).""" DATA_QUAT_PACKED_25HZ = 0x0274 """Orientation data at 25 Hz, 20 samples, packaged at 1.25 Hz (25/20 Hz).""" DATA_QUAT_PACKED_10HZ = 0x0275 """Orientation data at 10 Hz, 20 samples, packaged at 0.5 Hz (10/20 Hz).""" DATA_QUAT_PACKED_1HZ = 0x0276 """Orientation data at 1 Hz, 20 samples, packaged at 0.05 Hz (1/20 Hz).""" DATA_QUAT_FIXED_200HZ = 0x0281 """Orientation data sample at 200 Hz, fixed-point format.""" DATA_QUAT_FIXED_100HZ = 0x0282 """Orientation data sample at 100 Hz, fixed-point format.""" DATA_QUAT_FIXED_50HZ = 0x0283 """Orientation data sample at 50 Hz, fixed-point format.""" DATA_QUAT_FIXED_25HZ = 0x0284 """Orientation data sample at 25 Hz, fixed-point format.""" DATA_QUAT_FIXED_10HZ = 0x0285 """Orientation data sample at 10 Hz, fixed-point format.""" DATA_QUAT_FIXED_1HZ = 0x0286 """Orientation data sample at 1 Hz, fixed-point format.""" DATA_QUAT_FIXED_RT = 0x0287 """Orientation data sample for real-time transmission, fixed-point format.""" DATA_QUAT_FLOAT_200HZ = 0x0291 """Orientation data sample at 200 Hz, floating-point format.""" DATA_QUAT_FLOAT_100HZ = 0x0292 """Orientation data sample at 100 Hz, floating-point format.""" DATA_QUAT_FLOAT_50HZ = 0x0293 """Orientation data sample at 50 Hz, floating-point format.""" DATA_QUAT_FLOAT_25HZ = 0x0294 """Orientation data sample at 25 Hz, floating-point format.""" DATA_QUAT_FLOAT_10HZ = 0x0295 """Orientation data sample at 10 Hz, floating-point format.""" DATA_QUAT_FLOAT_1HZ = 0x0296 """Orientation data sample at 1 Hz, floating-point format.""" DATA_RAW_BURST = 0x0300 """Raw sensor burst data at ~1666 Hz.""" DATA_ACCZ_BURST = 0x0301 """Accelerometer z-axis burst data at ~1666 Hz.""" _RESERVED21 = 0x0310 _RESERVED22 = 0x0311 _RESERVED23 = 0x0312 # Hardware sync input DATA_SYNC_TRIGGER = 0x0400 """Received hardware synchronization trigger event.""" CMD_FS_LIST_FILES = 0x0500 """Requests a list of all files on the sensor.""" DATA_FS_FILE_COUNT = 0x0501 """Number of files on the sensor.""" DATA_FS_FILE = 0x0502 """Information about one file on the sensors.""" CMD_FS_GET_BYTES = 0x0503 """Requests contents of a file on the sensor.""" DATA_FS_BYTES = 0x0504 """Contents a file on the sensor.""" CMD_FS_STOP_GET_BYTES = 0x0505 """Stops getting contents from a file.""" ACK_FS_STOP_GET_BYTES = 0x0506 """Acknowledges stop getting bytes command.""" CMD_FS_GET_SIZE = 0x0507 """Requests the size of a file on the sensor.""" DATA_FS_SIZE = 0x0508 """Size of a file on the sensor.""" CMD_FS_DELETE_FILE = 0x0509 """Deletes a file on the sensor.""" ACK_FS_DELETE_FILE = 0x050A """Acknowledges file deletion command.""" CMD_FS_FORMAT_FILESYSTEM = 0x050D """Formats the filesystem.""" ACK_FS_FORMAT_FILESYSTEM = 0x050E """Acknowledges filesystem format command.""" _RESERVED24 = 0x1000 _RESERVED25 = 0xFF00 # Error ERROR = 0xFFFF """General error message from device."""
# classes for commands that do not have a payload (i.e., there is no corresponding struct): @register_package class CmdGetDeviceInfo(AbstractPackage): header = SensorHeader.CMD_GET_DEVICE_INFO """SensorHeader.CMD_GET_DEVICE_INFO (0x0070)""" @register_package class CmdSleep(AbstractPackage): header = SensorHeader.CMD_SLEEP """SensorHeader.CMD_SLEEP (0x0110)""" @register_package class AckSleep(AbstractPackage): header = SensorHeader.ACK_SLEEP """SensorHeader.ACK_SLEEP (0x0111)""" @register_package class CmdDeepSleep(AbstractPackage): header = SensorHeader.CMD_DEEP_SLEEP """SensorHeader.CMD_DEEP_SLEEP (0x0112)""" @register_package class AckDeepSleep(AbstractPackage): header = SensorHeader.ACK_DEEP_SLEEP """SensorHeader.ACK_DEEP_SLEEP (0x0113)""" @register_package class CmdGetMeasurementMode(AbstractPackage): header = SensorHeader.CMD_GET_MEASUREMENT_MODE """SensorHeader.CMD_GET_MEASUREMENT_MODE (0x0121)""" @register_package class CmdGetMeasurementBurstMode(AbstractPackage): header = SensorHeader.CMD_GET_MEASUREMENT_BURST_MODE """SensorHeader.CMD_GET_MEASUREMENT_BURST_MODE (0x0124)""" @register_package class CmdGetRecordingConfig(AbstractPackage): header = SensorHeader.CMD_GET_RECORDING_CONFIG """SensorHeader.CMD_GET_RECORDING_CONFIG (0x0141)""" @register_package class CmdStartStreaming(AbstractPackage): header = SensorHeader.CMD_START_STREAMING """SensorHeader.CMD_START_STREAMING (0x0150)""" @register_package class AckStartStreaming(AbstractPackage): header = SensorHeader.ACK_START_STREAMING """SensorHeader.ACK_START_STREAMING (0x0151)""" @register_package class CmdStopStreaming(AbstractPackage): header = SensorHeader.CMD_STOP_STREAMING """SensorHeader.CMD_STOP_STREAMING (0x0152)""" @register_package class AckStopStreaming(AbstractPackage): header = SensorHeader.ACK_STOP_STREAMING """SensorHeader.ACK_STOP_STREAMING (0x0153)""" @register_package class CmdStartRecording(AbstractPackage): header = SensorHeader.CMD_START_RECORDING """SensorHeader.CMD_START_RECORDING (0x0154)""" @register_package class AckStartRecording(AbstractPackage): header = SensorHeader.ACK_START_RECORDING """SensorHeader.ACK_START_RECORDING (0x0155)""" @register_package class CmdStopRecording(AbstractPackage): header = SensorHeader.CMD_STOP_RECORDING """SensorHeader.CMD_STOP_RECORDING (0x0156)""" @register_package class AckStopRecording(AbstractPackage): header = SensorHeader.ACK_STOP_RECORDING """SensorHeader.ACK_STOP_RECORDING (0x0157)""" @register_package class CmdStopStreamingAndClearBuffer(AbstractPackage): header = SensorHeader.CMD_STOP_STREAMING_AND_CLEAR_BUFFER """SensorHeader.CMD_STOP_STREAMING_AND_CLEAR_BUFFER (0x0158)""" @register_package class AckStopStreamingAndClearBuffer(AbstractPackage): header = SensorHeader.ACK_STOP_STREAMING_AND_CLEAR_BUFFER """SensorHeader.ACK_STOP_STREAMING_AND_CLEAR_BUFFER (0x0159)""" @register_package class CmdGetRealTimeStreamingMode(AbstractPackage): header = SensorHeader.CMD_GET_REAL_TIME_STREAMING_MODE """SensorHeader.CMD_GET_REAL_TIME_STREAMING_MODE (0x0161)""" @register_package class CmdStopRealTimeStreaming(AbstractPackage): header = SensorHeader.CMD_STOP_REAL_TIME_STREAMING """SensorHeader.CMD_STOP_REAL_TIME_STREAMING (0x0163)""" @register_package class AckStopRealTimeStreaming(AbstractPackage): header = SensorHeader.ACK_STOP_REAL_TIME_STREAMING """SensorHeader.ACK_STOP_REAL_TIME_STREAMING (0x0164)""" @register_package class CmdGetLedConfig(AbstractPackage): header = SensorHeader.CMD_GET_LED_CONFIG """SensorHeader.CMD_GET_LED_CONFIG (0x0181)""" @register_package class CmdGetLedMode(AbstractPackage): header = SensorHeader.CMD_GET_LED_MODE """SensorHeader.CMD_GET_LED_MODE (0x0184)""" @register_package class CmdGetStatus(AbstractPackage): header = SensorHeader.CMD_GET_STATUS """SensorHeader.CMD_GET_STATUS (0x0200)""" @register_package class CmdFsListFiles(AbstractPackage): header = SensorHeader.CMD_FS_LIST_FILES """SensorHeader.CMD_FS_LIST_FILES (0x0500)""" @register_package class CmdFsStopGetBytes(AbstractPackage): header = SensorHeader.CMD_FS_STOP_GET_BYTES """SensorHeader.CMD_FS_STOP_GET_BYTES (0x0505)""" @register_package class AckFsStopGetBytes(AbstractPackage): header = SensorHeader.ACK_FS_STOP_GET_BYTES """SensorHeader.ACK_FS_STOP_GET_BYTES (0x0506)""" @register_package class CmdFsFormatFilesystem(AbstractPackage): header = SensorHeader.CMD_FS_FORMAT_FILESYSTEM """SensorHeader.CMD_FS_FORMAT_FILESYSTEM (0x050D)""" @register_package class AckFsFormatFilesystem(AbstractPackage): header = SensorHeader.ACK_FS_FORMAT_FILESYSTEM """SensorHeader.ACK_FS_FORMAT_FILESYSTEM (0x050E)""" # remaining content of package.hpp:
[docs] @enum.unique class ErrorCode(enum.IntEnum): NO_ERROR = 0x00 """No error occurred.""" FILE_NOT_FOUND = 0xF0 """File was not found.""" FILE_DELETION_FAILED = 0xF1 """File deletion failed.""" FILE_SYSTEM_ERROR = 0xF2 """File system error occurred.""" FILE_ALREADY_EXISTS = 0xF3 """File already exists.""" FILE_TOO_SHORT = 0xF4 """File is too short.""" FILE_NAME_INVALID = 0xF5 """File name is invalid.""" FILE_SYSTEM_FULL = 0xF6 """File system is full.""" FILE_SYSTEM_BUSY = 0xF7 """File system is busy.""" RECORDING_CONFIG_NOT_SET = 0xF9 """Recording configuration is not set.""" CALIB_PARAM_FLASH_ERROR = 0xFA """Error when flashing calibration parameters.""" WRONG_STATE = 0xFB """Device is in the wrong state.""" PKG_ERROR = 0xFC """Could not parse received package.""" UNKNOWN_COMMAND = 0xFD """Unknown command received.""" SEND_BUFFER_FULL = 0xFE """Send buffer is full.""" UNKNOWN_ERROR = 0xFF """An unknown error occurred."""
[docs] @enum.unique class SensorState(enum.IntEnum): OFF = 0 """Sensor is powered off.""" IDLE = enum.auto() """Sensor is idle.""" STREAMING = enum.auto() """Sensor is streaming data.""" RECORDING = enum.auto() """Sensor is recording data."""
[docs] @enum.unique class ConnectionState(enum.IntEnum): OFFLINE = 0 """Device is offline.""" ADVERTISING = enum.auto() """Device is advertising via BLE.""" BLE_CONNECTED = enum.auto() """Device is connected via BLE.""" USB_CONNECTED = enum.auto() """Device is connected via USB."""
[docs] @enum.unique class SamplingMode(enum.IntEnum): MODE_DISABLED = 0x00 """Disabled.""" MODE_200HZ = 0x01 """Sampling at 200 Hz.""" MODE_100HZ = 0x02 """Sampling at 100 Hz.""" MODE_50HZ = 0x03 """Sampling at 50 Hz.""" MODE_25HZ = 0x04 """Sampling at 25 Hz.""" MODE_10HZ = 0x05 """Sampling at 10 Hz.""" MODE_1HZ = 0x06 """Sampling at 1 Hz."""
[docs] @enum.unique class SyncMode(enum.IntEnum): NO_SYNC = 0x00 """Synchronization is disabled.""" SYNC_SENDER = 0x01 """Device is a sync sender.""" SYNC_RECEIVER = 0x02 """Device is a sync receiver."""
[docs] @enum.unique class ProcessExtensionMode(enum.IntEnum): NO_EXTENSION = 0x0000 """No processing extension.""" _RESERVED01 = 0x0101
[docs] @enum.unique class CalibrationDataMode(enum.IntEnum): CALIB_DATA_DISABLED = 0x00 """Calibration data is disabled.""" CALIB_DATA_FULL = 0x01 """Full calibration data is generated.""" CALIB_DATA_MAG = 0x02 """Magnetometer calibration data is generated."""
[docs] @enum.unique class RealTimeDataMode(enum.IntEnum): REAL_TIME_DATA_DISABLED = 0x00 """Real-time data is disabled.""" REAL_TIME_DATA_QUAT = 0x01 """Send ``DataQuatFixedRt`` packages via the real-time channel.""" REAL_TIME_DATA_FULL = 0x02 """Send ``DataFullFixedRt`` packages via the real-time channel."""
[docs] @register_package class DataDeviceInfo(AbstractPackage): """ Device information such as protocol version, serial, hardware and firmware details. **Fields**: * **protocolVersion**: *uint16* -- Protocol version, currently 1. Values >= 32768 (0x8000) are reserved for custom applications. * **serial**: *char[6]* -- Device serial. * **hardwareRevision**: *char[8]* -- Hardware revision string. * **firmwareRevision**: *char[8]* -- Firmware revision string. * **firmwareVersion**: *char[12]* -- Firmware version string. * **firmwareDate**: *char[11]* -- Firmware build date. 2 + 6 + 8 + 8 + 12 + 11 = 47 bytes """ header = SensorHeader.DATA_DEVICE_INFO """SensorHeader.DATA_DEVICE_INFO (0x0071)""" _fields_ = [ ('protocolVersion', ctypes.c_uint16), ('serial', ctypes.c_char * 6), ('hardwareRevision', ctypes.c_char * 8), ('firmwareRevision', ctypes.c_char * 8), ('firmwareVersion', ctypes.c_char * 12), ('firmwareDate', ctypes.c_char * 11), ] def parse(self): return {f: self.protocolVersion if f == 'protocolVersion' else getattr(self, f).decode().strip() for f, _ in self._fields_} # type: ignore
[docs] @register_package class DataMeasurementMode(AbstractPackage): """ Configures the IMU measurement mode. The measurement mode determines which sensor data packages are generated, how synchronization is configured, and if bias estimation and magnetic disturbance rejection are enabled. The ``syncId`` should be set to a random 64 bit number that is re-generated whenever the set of employed sensors change. One device should be configured as sender and the other devices as receivers with the same ``syncId``. Note: The ``processExtensionMode`` is reserved for future use. Set to ``NO_EXTENSION`` (0) for now. **Fields**: * **timestamp**: *int64* -- Timestamp (in nanoseconds) when the mode is first applied. Set to 0 in ``CmdSetMeasurementMode`` to apply immediately or use a later timestamp to delay applying the new mode. * **fullFloat200HzEnabled**: *bool* -- Enables full float packages at 200 Hz. * **fullFixedMode**: *SamplingMode, uint8* -- Sampling mode for full fixed packages. * **fullPackedMode**: *SamplingMode, uint8* -- Sampling mode for full packed packaged. * **quatFloatMode**: *SamplingMode, uint8* -- Sampling mode for orientation float packages. * **quatFixedMode**: *SamplingMode, uint8* -- Sampling mode for orientation fixed packages. * **quatPackedMode**: *SamplingMode, uint8* -- Sampling mode for orientation packed packages. * **statusMode**: *uint8* -- Interval of status packages in seconds, 0 to disable. Recommended: 1 s. * **calibDataMode**: *CalibrationDataMode, uint8* -- Calibration data mode. For internal use only; set to 0. * **processExtensionMode**: *ProcessExtensionMode, uint16* -- Process extension mode. * **syncMode**: *SyncMode, uint8* -- Synchronization mode. * **syncId**: *uint64* -- Synchronization ID. * **disableBiasEstimation**: *bool* -- If true, rest detection and online bias estimation are not performed. * **disableMagDistRejection**: *bool* -- If true, magnetic disturbance rejection is not performed. * **disableMagData**: *bool* -- If true, the full packed/fixed modes will generate the 6D variants without magnetometer data. 8 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 2 + 1 + 8 + 1 + 1 + 1 = 30 bytes """ header = SensorHeader.DATA_MEASUREMENT_MODE """SensorHeader.DATA_MEASUREMENT_MODE (0x0122)""" _fields_ = [ ('timestamp', ctypes.c_int64), ('fullFloat200HzEnabled', ctypes.c_bool), ('fullFixedMode', ctypes.c_uint8), ('fullPackedMode', ctypes.c_uint8), ('quatFloatMode', ctypes.c_uint8), ('quatFixedMode', ctypes.c_uint8), ('quatPackedMode', ctypes.c_uint8), ('statusMode', ctypes.c_uint8), ('calibDataMode', ctypes.c_uint8), ('processExtensionMode', ctypes.c_uint16), ('syncMode', ctypes.c_uint8), ('syncId', ctypes.c_uint64), ('disableBiasEstimation', ctypes.c_bool), ('disableMagDistRejection', ctypes.c_bool), ('disableMagData', ctypes.c_bool), ]
@register_package class CmdSetMeasurementMode(DataMeasurementMode): header = SensorHeader.CMD_SET_MEASUREMENT_MODE """SensorHeader.CMD_SET_MEASUREMENT_MODE (0x0120)"""
[docs] @register_package class DataMeasurementBurstMode(AbstractPackage): """ Configures the burst mode for short-term high-frequency measurements. The burst mode is meant for recording high-frequency IMU data at ~1666 Hz for a short period of time (around a second). The burst time window can be configured in advance. The sensor will store the last received DataMeasurementBurstMode and start the burst measurement as soon as the configured startTimestamp is reached. Note: Longer burst measurements might be possible, depending on the device connection and configured measurement mode, but this cannot be guaranteed. **Fields**: * **enabled**: *bool* -- Indicates if burst mode is enabled. * **startTimestamp**: *int64* -- Start timestamp (in nanoseconds) for burst mode. * **endTimestamp**: *int64* -- End timestamp (in nanoseconds) for burst mode. Set to 0 to disable, set to 0x8000000000000000 (max int64) for continuous measurement. * **endTimestampIsRelative**: *bool* -- Indicates if end timestamp is relative to start. * **accZOnly**: *bool* -- If true, only the z-axis accelerometer data is saved. 1 + 8 + 8 + 1 + 1 = 19 bytes """ header = SensorHeader.DATA_MEASUREMENT_BURST_MODE """SensorHeader.DATA_MEASUREMENT_BURST_MODE (0x0125)""" _fields_ = [ ('enabled', ctypes.c_bool), ('startTimestamp', ctypes.c_int64), ('endTimestamp', ctypes.c_int64), ('endTimestampIsRelative', ctypes.c_bool), ('accZOnly', ctypes.c_bool), ]
@register_package class CmdSetMeasurementBurstMode(DataMeasurementBurstMode): header = SensorHeader.CMD_SET_MEASUREMENT_BURST_MODE """SensorHeader.CMD_SET_MEASUREMENT_BURST_MODE (0x0123)"""
[docs] @register_package class DataRecordingConfig(AbstractPackage): """ Configures file name and automatic end for data recording to internal storage. **Fields**: * **endTimestamp**: *int64* -- End timestamp (in nanoseconds) for recording. 0 for open-end recording. * **endTimestampIsRelative**: *bool* -- If true, end timestamp is relative. * **filename**: *char[65]* -- File name for recording. Maximum file name length is 64 characters, the rest needs to be filled with zero bytes. 8 + 1 + 65 = 74 bytes """ header = SensorHeader.DATA_RECORDING_CONFIG """SensorHeader.DATA_RECORDING_CONFIG (0x0142)""" _fields_ = [ ('endTimestamp', ctypes.c_int64), ('endTimestampIsRelative', ctypes.c_bool), ('filename', ctypes.c_char * 65), ]
@register_package class CmdSetRecordingConfig(DataRecordingConfig): header = SensorHeader.CMD_SET_RECORDING_CONFIG """SensorHeader.CMD_SET_RECORDING_CONFIG (0x0140)"""
[docs] @register_package class DataRealTimeStreamingMode(AbstractPackage): """ Configures real-time data streaming. **Fields**: * **mode**: *RealTimeDataMode, uint8* -- Real-time data mode. * **rateLimit**: *uint8* -- Maximum sending frequency in Hz, set to 0 to use the default value (currently 50 Hz). 1 + 1 = 2 bytes """ header = SensorHeader.DATA_REAL_TIME_STREAMING_MODE """SensorHeader.DATA_REAL_TIME_STREAMING_MODE (0x0162)""" _fields_ = [ ('mode', ctypes.c_uint8), ('rateLimit', ctypes.c_uint8), ]
@register_package class CmdStartRealTimeStreaming(DataRealTimeStreamingMode): header = SensorHeader.CMD_START_REAL_TIME_STREAMING """SensorHeader.CMD_START_REAL_TIME_STREAMING (0x0160)"""
[docs] @register_package class DataAbsoluteTime(AbstractPackage): """ Absolute time to be applied to the sensor clock. Note: The acknowlegement to ``CmdSetAbsoluteTime`` is a ``DataAbsoluteTime`` package with the same timestamp. Otherwise, ``DataAbsoluteTime`` is not used. To get the current time on the sensor, look at the timestamps in the ``DataStatus`` or ``DataClockRoundtrip`` packages. **Fields**: * **newTimestamp**: *int64* -- New absolute timestamp (in nanoseconds). 8 bytes """ header = SensorHeader.DATA_ABSOLUTE_TIME """SensorHeader.DATA_ABSOLUTE_TIME (0x0171)""" _fields_ = [ ('newTimestamp', ctypes.c_int64), ]
@register_package class CmdSetAbsoluteTime(DataAbsoluteTime): header = SensorHeader.CMD_SET_ABSOLUTE_TIME """SensorHeader.CMD_SET_ABSOLUTE_TIME (0x0170)"""
[docs] @register_package class DataClockRoundtrip(AbstractPackage): """ Clock roundtrip package for estimation of clock drift between host and sensors. To estimate clock drift between the host and the sensors, send this package regularly to the sensor (e.g., every second). When sending, set ``hostSendTimestamp`` to the current host time and the other 3 timestamps to zero. The sensor will set ``sensorReceiveTimestamp`` directly after receiving the package and ``sensorSendTimestamp`` directly when sending the package. The host should then set ``hostReceiveTimestamp`` directly after receiving the package. (The Python SDK does this automatically.) Calculate ``(hostReceiveTimestamp + sensorReceiveTimestamp - hostSendTimestamp - sensorSendTimestamp)/2`` to estimate the transmission delay and ``(hostSendTimestamp + hostReceiveTimestamp - sensorReceiveTimestamp - sensorSendTimestamp)/2`` to estimate the host clock offset. Note that single samples will jitter significantly and outliers can occurr due to transmission issues. **Fields**: * **hostSendTimestamp**: *int64* -- Timestamp (in nanoseconds) when host sent the message. * **sensorReceiveTimestamp**: *int64* -- Timestamp (in nanoseconds) when sensor received the message. * **sensorSendTimestamp**: *int64* -- Timestamp (in nanoseconds) when sensor sent the response. * **hostReceiveTimestamp**: *int64* -- Timestamp (in nanoseconds) when host received the response. 8 + 8 + 8 + 8 = 32 bytes """ header = SensorHeader.DATA_CLOCK_ROUNDTRIP """SensorHeader.DATA_CLOCK_ROUNDTRIP (0x0172)""" _fields_ = [ ('hostSendTimestamp', ctypes.c_int64), ('sensorReceiveTimestamp', ctypes.c_int64), ('sensorSendTimestamp', ctypes.c_int64), ('hostReceiveTimestamp', ctypes.c_int64), ]
[docs] @register_package class DataLedConfig(AbstractPackage): """ Configures the device LED. **Fields**: * **brightnessPercentage**: *uint8* -- LED brightness percentage (default: 40). * **alternativeColors**: *bool* -- If true, alternative LED colors are used (blue instead of green). * **notifyColor**: *uint32* -- Notification color RGB value (default: white, 0xFFFFFF). 1 + 1 + 4 = 6 bytes """ header = SensorHeader.DATA_LED_CONFIG """SensorHeader.DATA_LED_CONFIG (0x0182)""" _fields_ = [ ('brightnessPercentage', ctypes.c_uint8), ('alternativeColors', ctypes.c_bool), ('notifyColor', ctypes.c_uint32), ]
@register_package class CmdSetLedConfig(DataLedConfig): header = SensorHeader.CMD_SET_LED_CONFIG """SensorHeader.CMD_SET_LED_CONFIG (0x0180)"""
[docs] @register_package class DataLedMode(AbstractPackage): """ Configures the LED notification light (typically white unless changed in ``DataLedConfig``). This is used by the app to flash the LED for one second at the start and end of the measurement. Note: The ``DataLedMode`` sent as a response to ``CmdSetLedMode`` will always contain the actual and absolute timestamps. Those timestamps can be used for example for synchronization of IMU data and a video that recorded the flashing LED. **Fields**: * **notifyStartTimestamp**: *int64* -- Start timestamp (in nanoseconds) for LED notification. Set to 0 for immediate start (if end timestamp is not 0), set both to 0 to disable. * **notifyEndTimestamp**: *int64* -- End timestamp (in nanoseconds) for LED notification. Set to 0 for unlimited duration (if end timestamp is not 0), set both to 0 to disable. * **endTimestampIsRelative**: *bool* -- If true, end timestamp is relative to the start. 8 + 8 + 1 = 17 bytes """ header = SensorHeader.DATA_LED_MODE """SensorHeader.DATA_LED_MODE (0x0185)""" _fields_ = [ ('notifyStartTimestamp', ctypes.c_int64), ('notifyEndTimestamp', ctypes.c_int64), ('endTimestampIsRelative', ctypes.c_bool), ]
@register_package class CmdSetLedMode(DataLedMode): header = SensorHeader.CMD_SET_LED_MODE """SensorHeader.CMD_SET_LED_MODE (0x0183)"""
[docs] @register_package class DataSyncOutputMode(AbstractPackage): """ Synchronization output mode configuration. Setting this pulls the USB-C sideband use pin (SBU) down for the configured duration. The resulting electrical signal can be used for precise time synchronization between the sensors and other systems or to automatically start or stop other measurement systems. The response to ``CmdSetSyncOutputMode`` is sent after the pulse is finished and includes precise and absolute timestamps. **Fields**: * **startTimestamp**: *int64* -- Start timestamp (in nanoseconds) for sync output. Set to 0 for immediate start (if end timestamp is not 0), set both to 0 to disable. * **endTimestamp**: *int64* -- End timestamp (in nanoseconds) for sync output. Set to 0 for unlimited duration (if end timestamp is not 0), set both to 0 to disable (max pulse duration 1 sec). * **endTimestampIsRelative**: *bool* -- If true, end timestamp is relative to the start. 8 + 8 + 1 = 17 bytes """ header = SensorHeader.DATA_SYNC_OUTPUT_MODE """SensorHeader.DATA_SYNC_OUTPUT_MODE (0x0187)""" _fields_ = [ ('startTimestamp', ctypes.c_int64), ('endTimestamp', ctypes.c_int64), ('endTimestampIsRelative', ctypes.c_bool), ]
@register_package class CmdSetSyncOutputMode(DataSyncOutputMode): header = SensorHeader.CMD_SET_SYNC_OUTPUT_MODE """SensorHeader.CMD_SET_SYNC_OUTPUT_MODE (0x0186)"""
[docs] @register_package class DataStatus(AbstractPackage): """ Device status information. **Fields**: * **timestamp**: *int64* -- Timestamp (in nanoseconds). * **sensorState**: *SensorState, uint8* -- Current sensor state. * **connectionState**: *ConnectionState, uint8* -- Current connection state. * **gyrBias**: *int16[3]* -- Gyroscope bias estimate, fixed-point, 2°/s full range. The gyroscope measurements already have this bias removed. * **synchronized**: *bool* -- If true, device is either a sync sender or it is a sync receiver and received sufficient sync information recently. * **battery**: *uint8* -- Battery level in percent. When charging, 128 is added to the percentage. * **freeStoragePercentage**: *uint8* -- Free storage percentage. 8 + 1 + 1 + 3*2 + 1 + 1 + 1 = 19 bytes """ header = SensorHeader.DATA_STATUS """SensorHeader.DATA_STATUS (0x0201)""" _fields_ = [ ('timestamp', ctypes.c_int64), ('sensorState', ctypes.c_uint8), ('connectionState', ctypes.c_uint8), ('gyrBias', ctypes.c_int16 * 3), ('synchronized', ctypes.c_bool), ('battery', ctypes.c_uint8), ('freeStoragePercentage', ctypes.c_uint8), ] def parse(self): vals = super().parse() vals['gyrBias'] = np.array(self.gyrBias, float).reshape((1, 3)) * GYR_BIAS_SCALE_FACTOR charging = bool(vals['battery'] & 0x80) vals['batteryPercentage'] = vals.pop('battery') - 0x80 if charging else vals.pop('battery') vals['isCharging'] = charging return vals
[docs] class DataFullPacked(AbstractPackage): """ Full IMU data (raw data and orientations), encoded as fixed-point numbers and with 8 samples per package. This is the main package type for recording and transmitting raw data. Note: The orientation is only provided for the first sample because the remaining values can be extrapolated from the first orientation sample and the gyroscope measurements. See the Python SDK code of the ``parse`` method for an implementation. **Fields**: * **timestamp**: *int64* -- Timestamp (in nanoseconds) of the first sample. * **gyr**: *int16[24]* -- 8 gyroscope measurements, 2000°/s full range. * **acc**: *int16[24]* -- 8 accelerometer measurements, 16*9.81 m/s² full range. * **mag**: *int16[24]* -- 8 magnetometer measurements, in µT/16. * **quat**: *uint64* -- 6D orientation for first sample, including rest+magDist flags. * **delta**: *int16* -- Heading offset for first sample. The 9D orientation can be calculated from ``quat`` and ``delta``. * **errorFlags**: *uint8* -- Error flags. 8 + 24*2 + 24*2 + 24*2 + 8 + 2 + 1 = 163 bytes 200 Hz: 32600 bytes/s 100 Hz: 16300 bytes/s 50 Hz: 8150 bytes/s 25 Hz: 4075 bytes/s 10 Hz: 1630 bytes/s 1 Hz: 163 bytes/s """ _fields_ = [ ('timestamp', ctypes.c_int64), ('gyr', ctypes.c_int16 * 24), ('acc', ctypes.c_int16 * 24), ('mag', ctypes.c_int16 * 24), ('quat', ctypes.c_uint64), ('delta', ctypes.c_int16), ('errorFlags', ctypes.c_uint8), ] def parse(self): timestamp = np.arange(self.timestamp, self.timestamp + int(8e9/self.rate) - 1, int(1e9/self.rate)) gyr = np.array(self.gyr, float).reshape((8, 3)) * GYR_SCALE_FACTOR acc = np.array(self.acc, float).reshape((8, 3)) * ACC_SCALE_FACTOR mag = np.array(self.mag, float).reshape((8, 3)) * MAG_SCALE_FACTOR delta = np.full((8, 1), self.delta * DELTA_SCALE_FACTOR, float) decoded = decodeQuat(self.quat) gyrquat = quatFromGyr(gyr, self.rate) # Axis-angle quat from the gyroscope measurements. quat = np.empty((8, 4)) quat9D = np.empty((8, 4)) quat[0] = decoded[0] quat9D[0] = addHeading(decoded[0], delta[0, 0]) for i in range(7): # Perform gyroscope prediction for the missing quaternion samples. quat[i+1] = qmult(quat[i], gyrquat[i+1]) quat9D[i+1] = addHeading(quat[i+1], delta[i+1, 0]) restDetected = np.full((8, 1), decoded[1], bool) magDistDetected = np.full((8, 1), decoded[2], bool) return { 'timestamp': timestamp.reshape(8, 1), 'gyr': gyr, 'acc': acc, 'mag': mag, 'quat': quat, 'quat9D': quat9D, 'delta': delta, 'restDetected': restDetected, 'magDistDetected': magDistDetected, 'errorFlags': np.full((8, 1), self.errorFlags, np.uint8), } def __repr__(self): vals = self.parse() s = [f'{self.__class__.__name__}(timestamp={self.timestamp:_}, data:\n', 'gyr | acc | mag | ' 'quat | delta | rest dist | errorFlags'] for i in range(8): s.append(f'\n{vals["gyr"][i, 0]: 8.4f} {vals["gyr"][i, 1]: 8.4f} {vals["gyr"][i, 2]: 8.4f} | ' f'{vals["acc"][i, 0]: 9.4f} {vals["acc"][i, 1]: 9.4f} {vals["acc"][i, 2]: 9.4f} | ' f'{vals["mag"][i, 0]: 9.4f} {vals["mag"][i, 1]: 9.4f} {vals["mag"][i, 2]: 9.4f} | ' f'{vals["quat"][i, 0]: 7.4f} {vals["quat"][i, 1]: 7.4f} {vals["quat"][i, 2]: 7.4f} ' f'{vals["quat"][i, 3]: 7.4f} | {vals["delta"][i, 0]: 7.4f} | ' f'{vals["restDetected"][i, 0]: 4} {vals["magDistDetected"][i, 0]: 4} | ' f'{vals["errorFlags"][i, 0]:d}') s.append(' )') return ''.join(s)
@register_package class DataFullPacked200Hz(DataFullPacked): header = SensorHeader.DATA_FULL_PACKED_200HZ """SensorHeader.DATA_FULL_PACKED_200HZ (0x0221)""" rate = 200 @register_package class DataFullPacked100Hz(DataFullPacked): header = SensorHeader.DATA_FULL_PACKED_100HZ """SensorHeader.DATA_FULL_PACKED_100HZ (0x0222)""" rate = 100 @register_package class DataFullPacked50Hz(DataFullPacked): header = SensorHeader.DATA_FULL_PACKED_50HZ """SensorHeader.DATA_FULL_PACKED_50HZ (0x0223)""" rate = 50 @register_package class DataFullPacked25Hz(DataFullPacked): header = SensorHeader.DATA_FULL_PACKED_25HZ """SensorHeader.DATA_FULL_PACKED_25HZ (0x0224)""" rate = 25 @register_package class DataFullPacked10Hz(DataFullPacked): header = SensorHeader.DATA_FULL_PACKED_10HZ """SensorHeader.DATA_FULL_PACKED_10HZ (0x0225)""" rate = 10 @register_package class DataFullPacked1Hz(DataFullPacked): header = SensorHeader.DATA_FULL_PACKED_1HZ """SensorHeader.DATA_FULL_PACKED_1HZ (0x0226)""" rate = 1
[docs] class DataFull6DPacked(AbstractPackage): """ Variant of ``DataFullPacked`` that does not include magnetometer data. See ``DataFullPacked`` for more details. Set ``disableMagData`` to true in the measurement mode to generate this package instead of ``DataFullPacked``. Note: Even with magnetometer data output disabled, this package still contains the full 6D and 9D orientations. **Fields**: * **timestamp**: *int64* -- Timestamp (in nanoseconds) of the first sample. * **gyr**: *int16[24]* -- 8 gyroscope measurements, 2000°/s full range. * **acc**: *int16[24]* -- 8 accelerometer measurements, 16*9.81 m/s² full range. * **quat**: *uint64* -- 6D orientation for first sample, including rest+magDist flags. * **delta**: *int16* -- Heading offset for first sample. The 9D orientation can be calculated from ``quat`` and ``delta``. * **errorFlags**: *uint8* -- Error flags. 8 + 24*2 + 24*2 + 8 + 2 + 1 = 115 bytes 200 Hz: 23000 bytes/s 100 Hz: 11500 bytes/s 50 Hz: 5750 bytes/s 25 Hz: 2875 bytes/s 10 Hz: 1150 bytes/s 1 Hz: 115 bytes/s """ _fields_ = [ ('timestamp', ctypes.c_int64), ('gyr', ctypes.c_int16 * 24), ('acc', ctypes.c_int16 * 24), ('quat', ctypes.c_uint64), ('delta', ctypes.c_int16), ('errorFlags', ctypes.c_uint8), ] def parse(self): timestamp = np.arange(self.timestamp, self.timestamp + int(8e9/self.rate) - 1, int(1e9/self.rate)) gyr = np.array(self.gyr, float).reshape((8, 3)) * GYR_SCALE_FACTOR acc = np.array(self.acc, float).reshape((8, 3)) * ACC_SCALE_FACTOR delta = np.full((8, 1), self.delta * DELTA_SCALE_FACTOR, float) decoded = decodeQuat(self.quat) gyrquat = quatFromGyr(gyr, self.rate) # Axis-angle quat from the gyroscope measurements. quat = np.empty((8, 4)) quat9D = np.empty((8, 4)) quat[0] = decoded[0] quat9D[0] = addHeading(decoded[0], delta[0, 0]) for i in range(7): # Perform gyroscope prediction for the missing quaternion samples. quat[i+1] = qmult(quat[i], gyrquat[i+1]) quat9D[i+1] = addHeading(quat[i+1], delta[i+1, 0]) restDetected = np.full((8, 1), decoded[1], bool) magDistDetected = np.full((8, 1), decoded[2], bool) return { 'timestamp': timestamp.reshape(8, 1), 'gyr': gyr, 'acc': acc, 'quat': quat, 'quat9D': quat9D, 'delta': delta, 'restDetected': restDetected, 'magDistDetected': magDistDetected, 'errorFlags': np.full((8, 1), self.errorFlags, np.uint8), } def __repr__(self): vals = self.parse() s = [f'{self.__class__.__name__}(timestamp={self.timestamp:_}, data:\n', 'gyr | acc | ' 'quat | delta | rest dist | errorFlags'] for i in range(8): s.append(f'\n{vals["gyr"][i, 0]: 8.4f} {vals["gyr"][i, 1]: 8.4f} {vals["gyr"][i, 2]: 8.4f} | ' f'{vals["acc"][i, 0]: 9.4f} {vals["acc"][i, 1]: 9.4f} {vals["acc"][i, 2]: 9.4f} | ' f'{vals["quat"][i, 0]: 7.4f} {vals["quat"][i, 1]: 7.4f} {vals["quat"][i, 2]: 7.4f} ' f'{vals["quat"][i, 3]: 7.4f} | {vals["delta"][i, 0]: 7.4f} | ' f'{vals["restDetected"][i, 0]: 4} {vals["magDistDetected"][i, 0]: 4} | ' f'{vals["errorFlags"][i, 0]:d}') s.append(' )') return ''.join(s)
@register_package class DataFull6DPacked200Hz(DataFull6DPacked): header = SensorHeader.DATA_FULL_6D_PACKED_200HZ """SensorHeader.DATA_FULL_6D_PACKED_200HZ (0x0231)""" rate = 200 @register_package class DataFull6DPacked100Hz(DataFull6DPacked): header = SensorHeader.DATA_FULL_6D_PACKED_100HZ """SensorHeader.DATA_FULL_6D_PACKED_100HZ (0x0232)""" rate = 100 @register_package class DataFull6DPacked50Hz(DataFull6DPacked): header = SensorHeader.DATA_FULL_6D_PACKED_50HZ """SensorHeader.DATA_FULL_6D_PACKED_50HZ (0x0233)""" rate = 50 @register_package class DataFull6DPacked25Hz(DataFull6DPacked): header = SensorHeader.DATA_FULL_6D_PACKED_25HZ """SensorHeader.DATA_FULL_6D_PACKED_25HZ (0x0234)""" rate = 25 @register_package class DataFull6DPacked10Hz(DataFull6DPacked): header = SensorHeader.DATA_FULL_6D_PACKED_10HZ """SensorHeader.DATA_FULL_6D_PACKED_10HZ (0x0235)""" rate = 10 @register_package class DataFull6DPacked1Hz(DataFull6DPacked): header = SensorHeader.DATA_FULL_6D_PACKED_1HZ """SensorHeader.DATA_FULL_6D_PACKED_1HZ (0x0236)""" rate = 1
[docs] class DataFullFixed(AbstractPackage): """ Single sample containing full IMU data (raw data and orientations), encoded as fixed-point numbers. This can be useful for streaming or recording at low sampling rates. In most cases, ``DataFullPacked`` is the better option. **Fields**: * **timestamp**: *int64* -- Timestamp (in nanoseconds). * **gyr**: *int16[3]* -- Gyroscope measurement, 2000°/s full range. * **acc**: *int16[3]* -- Accelerometer measurement, 16*9.81 m/s² full range. * **mag**: *int16[3]* -- Magnetometer measurement, in µT/16. * **quat**: *uint64* -- 6D orientation, including rest+magDist flags. * **delta**: *int16* -- Heading offset. The 9D orientation can be calculated from ``quat`` and ``delta``. * **errorFlags**: *uint8* -- Error flags. 8 + 3*2 + 3*2 + 3*2 + 8 + 2 + 1 = 37 bytes 200 Hz: 7400 bytes/s 100 Hz: 3700 bytes/s 50 Hz: 1850 bytes/s 25 Hz: 925 bytes/s 10 Hz: 370 bytes/s 1 Hz: 37 bytes/s """ _fields_ = [ ('timestamp', ctypes.c_int64), ('gyr', ctypes.c_int16 * 3), ('acc', ctypes.c_int16 * 3), ('mag', ctypes.c_int16 * 3), ('quat', ctypes.c_uint64), ('delta', ctypes.c_int16), ('errorFlags', ctypes.c_uint8), ] def parse(self): quat, restDetected, magDistDetected = decodeQuat(self.quat) return { 'timestamp': self.timestamp, 'gyr': np.array(self.gyr, float) * GYR_SCALE_FACTOR, 'acc': np.array(self.acc, float) * ACC_SCALE_FACTOR, 'mag': np.array(self.mag, float) * MAG_SCALE_FACTOR, 'quat': quat, 'quat9D': addHeading(quat, self.delta * DELTA_SCALE_FACTOR), 'delta': self.delta * DELTA_SCALE_FACTOR, 'restDetected': restDetected, 'magDistDetected': magDistDetected, 'errorFlags': self.errorFlags, } def __repr__(self): vals = self.parse() return f'{self.__class__.__name__}(timestamp={self.timestamp:_}, ' \ f'gyr=[{vals["gyr"][0]: 8.4f} {vals["gyr"][1]: 8.4f} {vals["gyr"][2]: 8.4f}], ' \ f'acc=[{vals["acc"][0]: 9.4f} {vals["acc"][1]: 9.4f} {vals["acc"][2]: 9.4f}], ' \ f'mag=[{vals["mag"][0]: 9.4f} {vals["mag"][1]: 9.4f} {vals["mag"][2]: 9.4f}], ' \ f'quat=[{vals["quat"][0]: 7.4f} {vals["quat"][1]: 7.4f} {vals["quat"][2]: 7.4f} ' \ f'{vals["quat"][3]: 7.4f}], delta={vals["delta"]: 6.4f}, ' \ f'rest={vals["restDetected"]:1}, dist={vals["magDistDetected"]:1}, flags={vals["errorFlags"]:d})'
@register_package class DataFullFixed200Hz(DataFullFixed): header = SensorHeader.DATA_FULL_FIXED_200HZ """SensorHeader.DATA_FULL_FIXED_200HZ (0x0241)""" rate = 200 @register_package class DataFullFixed100Hz(DataFullFixed): header = SensorHeader.DATA_FULL_FIXED_100HZ """SensorHeader.DATA_FULL_FIXED_100HZ (0x0242)""" rate = 100 @register_package class DataFullFixed50Hz(DataFullFixed): header = SensorHeader.DATA_FULL_FIXED_50HZ """SensorHeader.DATA_FULL_FIXED_50HZ (0x0243)""" rate = 50 @register_package class DataFullFixed25Hz(DataFullFixed): header = SensorHeader.DATA_FULL_FIXED_25HZ """SensorHeader.DATA_FULL_FIXED_25HZ (0x0244)""" rate = 25 @register_package class DataFullFixed10Hz(DataFullFixed): header = SensorHeader.DATA_FULL_FIXED_10HZ """SensorHeader.DATA_FULL_FIXED_10HZ (0x0245)""" rate = 10 @register_package class DataFullFixed1Hz(DataFullFixed): header = SensorHeader.DATA_FULL_FIXED_1HZ """SensorHeader.DATA_FULL_FIXED_1HZ (0x0246)""" rate = 1 @register_package class DataFullFixedRt(DataFullFixed): header = SensorHeader.DATA_FULL_FIXED_RT """SensorHeader.DATA_FULL_FIXED_RT (0x0247)"""
[docs] class DataFull6DFixed(AbstractPackage): """ Variant of ``DataFullFixed`` that does not include magnetometer data. See ``DataFullFixed`` for more details. Set ``disableMagData`` to true in the measurement mode to generate this package instead of ``DataFullFixed``. Note: Even with magnetometer data output disabled, this package still contains the full 6D and 9D orientations. **Fields**: * **timestamp**: *int64* -- Timestamp (in nanoseconds). * **gyr**: *int16[3]* -- Gyroscope measurement, 2000°/s full range. * **acc**: *int16[3]* -- Accelerometer measurement, 16*9.81 m/s² full range. * **quat**: *uint64* -- 6D orientation, including rest+magDist flags. * **delta**: *int16* -- Heading offset. The 9D orientation can be calculated from ``quat`` and ``delta``. * **errorFlags**: *uint8* -- Error flags. 8 + 3*2 + 3*2 + 8 + 2 + 1 = 31 bytes 200 Hz: 6200 bytes/s 100 Hz: 3100 bytes/s 50 Hz: 1550 bytes/s 25 Hz: 775 bytes/s 10 Hz: 310 bytes/s 1 Hz: 31 bytes/s """ _fields_ = [ ('timestamp', ctypes.c_int64), ('gyr', ctypes.c_int16 * 3), ('acc', ctypes.c_int16 * 3), ('quat', ctypes.c_uint64), ('delta', ctypes.c_int16), ('errorFlags', ctypes.c_uint8), ] def parse(self): quat, restDetected, magDistDetected = decodeQuat(self.quat) return { 'timestamp': self.timestamp, 'gyr': np.array(self.gyr, float) * GYR_SCALE_FACTOR, 'acc': np.array(self.acc, float) * ACC_SCALE_FACTOR, 'quat': quat, 'quat9D': addHeading(quat, self.delta * DELTA_SCALE_FACTOR), 'delta': self.delta * DELTA_SCALE_FACTOR, 'restDetected': restDetected, 'magDistDetected': magDistDetected, 'errorFlags': self.errorFlags, } def __repr__(self): vals = self.parse() return f'{self.__class__.__name__}(timestamp={self.timestamp:_}, ' \ f'gyr=[{vals["gyr"][0]: 8.4f} {vals["gyr"][1]: 8.4f} {vals["gyr"][2]: 8.4f}], ' \ f'acc=[{vals["acc"][0]: 9.4f} {vals["acc"][1]: 9.4f} {vals["acc"][2]: 9.4f}], ' \ f'quat=[{vals["quat"][0]: 7.4f} {vals["quat"][1]: 7.4f} {vals["quat"][2]: 7.4f} ' \ f'{vals["quat"][3]: 7.4f}], delta={vals["delta"]: 6.4f}, ' \ f'rest={vals["restDetected"]:1}, dist={vals["magDistDetected"]:1}, flags={vals["errorFlags"]:d})'
@register_package class DataFull6DFixed200Hz(DataFull6DFixed): header = SensorHeader.DATA_FULL_6D_FIXED_200HZ """SensorHeader.DATA_FULL_6D_FIXED_200HZ (0x0251)""" rate = 200 @register_package class DataFull6DFixed100Hz(DataFull6DFixed): header = SensorHeader.DATA_FULL_6D_FIXED_100HZ """SensorHeader.DATA_FULL_6D_FIXED_100HZ (0x0252)""" rate = 100 @register_package class DataFull6DFixed50Hz(DataFull6DFixed): header = SensorHeader.DATA_FULL_6D_FIXED_50HZ """SensorHeader.DATA_FULL_6D_FIXED_50HZ (0x0253)""" rate = 50 @register_package class DataFull6DFixed25Hz(DataFull6DFixed): header = SensorHeader.DATA_FULL_6D_FIXED_25HZ """SensorHeader.DATA_FULL_6D_FIXED_25HZ (0x0254)""" rate = 25 @register_package class DataFull6DFixed10Hz(DataFull6DFixed): header = SensorHeader.DATA_FULL_6D_FIXED_10HZ """SensorHeader.DATA_FULL_6D_FIXED_10HZ (0x0255)""" rate = 10 @register_package class DataFull6DFixed1Hz(DataFull6DFixed): header = SensorHeader.DATA_FULL_6D_FIXED_1HZ """SensorHeader.DATA_FULL_6D_FIXED_1HZ (0x0256)""" rate = 1
[docs] @register_package class DataFullFloat200Hz(AbstractPackage): """ Single sample containing full IMU data (raw data and orientations) at 200 Hz, encoded as 32-bit floating point numbers. Unlike the other data packages, this package is not packed and included padding bytes. Note: In most applications, the more efficient ``DataFullPacked`` or ``DataFullFixed`` packages should be prefered. **Fields**: * **timestamp**: *int64* -- Timestamp (in nanoseconds). * **gyr**: *float[3]* -- Gyroscope measurement in rad/s. * **acc**: *float[3]* -- Accelerometer measurement in m/s². * **mag**: *float[3]* -- Magnetometer measurement in µT. * **quat**: *float[4]* -- 6D orientation quaternion (w, x, y, z). * **delta**: *float* -- Heading offset in rad. The 9D orientation can be calculated from ``quat`` and ``delta``. * **restDetected**: *bool* -- Indicates if sensor is at rest. * **magDistDetected**: *bool* -- Indicates if magnetic disturbance is detected. * **errorFlags**: *uint8* -- Error flags. 8 + 3*4 + 3*4 + 3*4 + 4*4 + 4 + 1 + 1 + 1 = 67 bytes (+ padding) """ header = SensorHeader.DATA_FULL_FLOAT_200HZ """SensorHeader.DATA_FULL_FLOAT_200HZ (0x0261)""" _pack_ = 0 _fields_ = [ ('timestamp', ctypes.c_int64), ('gyr', ctypes.c_float * 3), ('acc', ctypes.c_float * 3), ('mag', ctypes.c_float * 3), ('quat', ctypes.c_float * 4), ('delta', ctypes.c_float), ('restDetected', ctypes.c_bool), ('magDistDetected', ctypes.c_bool), ('errorFlags', ctypes.c_uint8), ] def parse(self): return { 'timestamp': self.timestamp, 'gyr': np.array(self.gyr, float), 'acc': np.array(self.acc, float), 'mag': np.array(self.mag, float), 'quat': np.array(self.quat, float), 'quat9D': addHeading(np.array(self.quat, float), self.delta), 'delta': self.delta, 'restDetected': self.restDetected, 'magDistDetected': self.magDistDetected, 'errorFlags': self.errorFlags, } def __repr__(self): vals = self.parse() return f'{self.__class__.__name__}(timestamp={self.timestamp:_}, ' \ f'gyr=[{vals["gyr"][0]: 8.4f} {vals["gyr"][1]: 8.4f} {vals["gyr"][2]: 8.4f}], ' \ f'acc=[{vals["acc"][0]: 9.4f} {vals["acc"][1]: 9.4f} {vals["acc"][2]: 9.4f}], ' \ f'mag=[{vals["mag"][0]: 9.4f} {vals["mag"][1]: 9.4f} {vals["mag"][2]: 9.4f}], ' \ f'quat=[{vals["quat"][0]: 7.4f} {vals["quat"][1]: 7.4f} {vals["quat"][2]: 7.4f} ' \ f'{vals["quat"][3]: 7.4f}], delta={vals["delta"]: 6.4f}, ' \ f'rest={vals["restDetected"]:1}, dist={vals["magDistDetected"]:1}, flags={vals["errorFlags"]:d})'
[docs] class DataQuatPacked(AbstractPackage): """ Orientation data, encoded as fixed-point numbers and with 20 samples per package. This is the main package type for recording and transmitting orientation data. **Fields**: * **timestamp**: *int64* -- Timestamp (in nanoseconds) of the first sample. * **quat**: *uint64[20]* -- 6D orientation samples, including rest+magDist flags. * **delta**: *int16[20]* -- Heading offset. The 9D orientation can be calculated from ``quat`` and ``delta``. * **errorFlags**: *uint8[20]* -- Error flags. 8 + 20*8 + 20*2 + 20 = 228 bytes 200 Hz: 45600 bytes/s 100 Hz: 22800 bytes/s 50 Hz: 11400 bytes/s 25 Hz: 5700 bytes/s 10 Hz: 2280 bytes/s 1 Hz: 228 bytes/s """ _fields_ = [ ('timestamp', ctypes.c_int64), ('quat', ctypes.c_uint64 * 20), ('delta', ctypes.c_int16 * 20), ('errorFlags', ctypes.c_uint8 * 20), ] def parse(self): timestamp = np.arange(self.timestamp, self.timestamp+int(20e9/self.rate)-1, int(1e9/self.rate)) decoded = [decodeQuat(q) for q in self.quat] quat = np.array([d[0] for d in decoded]) restDetected = np.array([d[1] for d in decoded]) magDistDetected = np.array([d[2] for d in decoded]) delta = np.array(self.delta, float) * DELTA_SCALE_FACTOR quat9D = np.array([addHeading(q, d) for q, d in zip(quat, delta)]) return { 'timestamp': timestamp.reshape(20, 1), 'quat': quat, 'quat9D': quat9D, 'delta': delta.reshape(20, 1), 'restDetected': restDetected.reshape(20, 1), 'magDistDetected': magDistDetected.reshape(20, 1), 'errorFlags': np.array(self.errorFlags, np.uint8).reshape(20, 1), } def __repr__(self): vals = self.parse() data = np.column_stack([vals[k] for k in ['quat', 'delta', 'restDetected', 'magDistDetected', 'errorFlags']]) s = f'{self.__class__.__name__}(timestamp={self.timestamp}, data:\n' \ f' quat_w quat_x quat_y quat_z delta rest magDist errorFlags' \ f'\n{np.array2string(data, precision=8, sign=" ", max_line_width=1000)})' return s
@register_package class DataQuatPacked200Hz(DataQuatPacked): header = SensorHeader.DATA_QUAT_PACKED_200HZ """SensorHeader.DATA_QUAT_PACKED_200HZ (0x0271)""" rate = 200 @register_package class DataQuatPacked100Hz(DataQuatPacked): header = SensorHeader.DATA_QUAT_PACKED_100HZ """SensorHeader.DATA_QUAT_PACKED_100HZ (0x0272)""" rate = 100 @register_package class DataQuatPacked50Hz(DataQuatPacked): header = SensorHeader.DATA_QUAT_PACKED_50HZ """SensorHeader.DATA_QUAT_PACKED_50HZ (0x0273)""" rate = 50 @register_package class DataQuatPacked25Hz(DataQuatPacked): header = SensorHeader.DATA_QUAT_PACKED_25HZ """SensorHeader.DATA_QUAT_PACKED_25HZ (0x0274)""" rate = 25 @register_package class DataQuatPacked10Hz(DataQuatPacked): header = SensorHeader.DATA_QUAT_PACKED_10HZ """SensorHeader.DATA_QUAT_PACKED_10HZ (0x0275)""" rate = 10 @register_package class DataQuatPacked1Hz(DataQuatPacked): header = SensorHeader.DATA_QUAT_PACKED_1HZ """SensorHeader.DATA_QUAT_PACKED_1HZ (0x0276)""" rate = 1
[docs] class DataQuatFixed(AbstractPackage): """ Single orientation sample, encoded as fixed-point numbers. **Fields**: * **timestamp**: *int64* -- Timestamp (in nanoseconds). * **quat**: *uint64* -- 6D orientation, including rest+magDist flags. * **delta**: *int16* -- Heading offset. The 9D orientation can be calculated from ``quat`` and ``delta``. * **errorFlags**: *uint8* -- Error flags. 8 + 8 + 2 + 1 = 19 bytes 200 Hz: 3800 bytes/s 100 Hz: 1900 bytes/s 50 Hz: 950 bytes/s 25 Hz: 475 bytes/s 10 Hz: 190 bytes/s 1 Hz: 19 bytes/s """ _fields_ = [ ('timestamp', ctypes.c_int64), ('quat', ctypes.c_uint64), ('delta', ctypes.c_int16), ('errorFlags', ctypes.c_uint8), ] def parse(self): quat, restDetected, magDistDetected = decodeQuat(self.quat) delta = self.delta * DELTA_SCALE_FACTOR return { 'timestamp': self.timestamp, 'quat': quat, 'quat9D': addHeading(quat, delta), 'delta': delta, 'restDetected': restDetected, 'magDistDetected': magDistDetected, 'errorFlags': self.errorFlags, }
@register_package class DataQuatFixed200Hz(DataQuatFixed): header = SensorHeader.DATA_QUAT_FIXED_200HZ """SensorHeader.DATA_QUAT_FIXED_200HZ (0x0281)""" rate = 200 @register_package class DataQuatFixed100Hz(DataQuatFixed): header = SensorHeader.DATA_QUAT_FIXED_100HZ """SensorHeader.DATA_QUAT_FIXED_100HZ (0x0282)""" rate = 100 @register_package class DataQuatFixed50Hz(DataQuatFixed): header = SensorHeader.DATA_QUAT_FIXED_50HZ """SensorHeader.DATA_QUAT_FIXED_50HZ (0x0283)""" rate = 50 @register_package class DataQuatFixed25Hz(DataQuatFixed): header = SensorHeader.DATA_QUAT_FIXED_25HZ """SensorHeader.DATA_QUAT_FIXED_25HZ (0x0284)""" rate = 25 @register_package class DataQuatFixed10Hz(DataQuatFixed): header = SensorHeader.DATA_QUAT_FIXED_10HZ """SensorHeader.DATA_QUAT_FIXED_10HZ (0x0285)""" rate = 10 @register_package class DataQuatFixed1Hz(DataQuatFixed): header = SensorHeader.DATA_QUAT_FIXED_1HZ """SensorHeader.DATA_QUAT_FIXED_1HZ (0x0286)""" rate = 1 @register_package class DataQuatFixedRt(DataQuatFixed): header = SensorHeader.DATA_QUAT_FIXED_RT """SensorHeader.DATA_QUAT_FIXED_RT (0x0287)"""
[docs] class DataQuatFloat(AbstractPackage): """ Single orientation sample, encoded as 32-bit floating point numbers. Note: In most applications, the more efficient ``QuatFullPacked`` or ``QuatFullFixed`` packages should be prefered. **Fields**: * **timestamp**: *int64* -- Timestamp (in nanoseconds). * **quat**: *float[4]* -- 6D orientation quaternion (w, x, y, z). * **delta**: *float* -- Heading offset in rad. The 9D orientation can be calculated from ``quat`` and ``delta``. * **restDetected**: *bool* -- Indicates if sensor is at rest. * **magDistDetected**: *bool* -- Indicates if magnetic disturbance is detected. * **errorFlags**: *uint8* -- Error flags. 8 + 4*4 + 4 + 1 + 1 + 1 = 31 bytes 200 Hz: 6200 bytes/s 100 Hz: 3100 bytes/s 50 Hz: 1550 bytes/s 25 Hz: 775 bytes/s 10 Hz: 310 bytes/s 1 Hz: 31 bytes/s """ _fields_ = [ ('timestamp', ctypes.c_int64), ('quat', ctypes.c_float * 4), ('delta', ctypes.c_float), ('restDetected', ctypes.c_bool), ('magDistDetected', ctypes.c_bool), ('errorFlags', ctypes.c_uint8), ] def parse(self): return { 'timestamp': self.timestamp, 'quat': np.array(self.quat, float), 'quat9D': addHeading(np.array(self.quat, float), self.delta), 'delta': self.delta, 'restDetected': self.restDetected, 'magDistDetected': self.magDistDetected, 'errorFlags': self.errorFlags, }
@register_package class DataQuatFloat200Hz(DataQuatFloat): header = SensorHeader.DATA_QUAT_FLOAT_200HZ """SensorHeader.DATA_QUAT_FLOAT_200HZ (0x0291)""" rate = 200 @register_package class DataQuatFloat100Hz(DataQuatFloat): header = SensorHeader.DATA_QUAT_FLOAT_100HZ """SensorHeader.DATA_QUAT_FLOAT_100HZ (0x0292)""" rate = 100 @register_package class DataQuatFloat50Hz(DataQuatFloat): header = SensorHeader.DATA_QUAT_FLOAT_50HZ """SensorHeader.DATA_QUAT_FLOAT_50HZ (0x0293)""" rate = 50 @register_package class DataQuatFloat25Hz(DataQuatFloat): header = SensorHeader.DATA_QUAT_FLOAT_25HZ """SensorHeader.DATA_QUAT_FLOAT_25HZ (0x0294)""" rate = 25 @register_package class DataQuatFloat10Hz(DataQuatFloat): header = SensorHeader.DATA_QUAT_FLOAT_10HZ """SensorHeader.DATA_QUAT_FLOAT_10HZ (0x0295)""" rate = 10 @register_package class DataQuatFloat1Hz(DataQuatFloat): header = SensorHeader.DATA_QUAT_FLOAT_1HZ """SensorHeader.DATA_QUAT_FLOAT_1HZ (0x0296)""" rate = 1
[docs] @register_package class DataRawBurst(AbstractPackage): """ Raw data at ~1666 Hz of short high-frequency burst measurements. Each package contains 16 samples, i.e., packages are generated at ~104 Hz. **Fields**: * **timestamp**: *int64* -- Timestamp (in nanoseconds) of the first sample. * **gyr**: *int16[48]* -- 16 gyroscope measurements. * **acc**: *int16[48]* -- 16 accelerometer measurements. * **mag**: *int16[3]* -- Magnetometer measurement for first sample. * **errorFlags**: *uint8* -- Error flags. 8 + 48*2 + 48*2 + 3*2 + 1 = 207 bytes """ header = SensorHeader.DATA_RAW_BURST """SensorHeader.DATA_RAW_BURST (0x0300)""" _fields_ = [ ('timestamp', ctypes.c_int64), ('gyr', ctypes.c_int16 * 48), ('acc', ctypes.c_int16 * 48), ('mag', ctypes.c_int16 * 3), ('errorFlags', ctypes.c_uint8), ] def parse(self): timestamp = np.full((16, 1), np.nan, int) timestamp[0] = self.timestamp mag = np.array(self.mag, float).reshape(1, 3) * MAG_SCALE_FACTOR mag = np.repeat(mag, 16, axis=0) return { 'timestamp': timestamp, 'gyr': np.array(self.gyr, float).reshape(16, 3) * GYR_SCALE_FACTOR, 'acc': np.array(self.acc, float).reshape(16, 3) * ACC_SCALE_FACTOR, 'mag': mag, 'errorFlags': np.full((16, 1), self.errorFlags, np.uint8), } def __repr__(self): vals = self.parse() s = [f'{self.__class__.__name__}(timestamp={self.timestamp:_}, mag={vals["mag"][0]}, ' f'errorFlags={self.errorFlags}, data:\n', 'gyr | acc'] for i in range(16): s.append(f'\n{vals["gyr"][i, 0]: 8.4f} {vals["gyr"][i, 1]: 8.4f} {vals["gyr"][i, 2]: 8.4f} | ' f'{vals["acc"][i, 0]: 9.4f} {vals["acc"][i, 1]: 9.4f} {vals["acc"][i, 2]: 9.4f}') s.append(' )') return ''.join(s)
[docs] @register_package class DataAccZBurst(AbstractPackage): """ Z-component of the accelerometer measurement at ~1666 Hz of short high-frequency burst measurements. Compared to the full burst mode, choosing this package type reduces the data rate but still allows for measurement of high-frequency vibrations orthogonal to the device surface. Each package contains 64 samples, i.e., packages are generated at ~26 Hz. **Fields**: * **timestamp**: *int64* -- Timestamp (in nanoseconds) of the first sample. * **accZ**: *int16[64]* -- 64 z-axis accelerometer measurements. * **errorFlags**: *uint8* -- Error flags. 8 + 64*2 + 1 = 137 bytes """ header = SensorHeader.DATA_ACCZ_BURST """SensorHeader.DATA_ACCZ_BURST (0x0301)""" _fields_ = [ ('timestamp', ctypes.c_int64), ('accZ', ctypes.c_int16 * 64), ('errorFlags', ctypes.c_uint8), ] def parse(self): timestamp = np.full((64, 1), np.nan, int) timestamp[0] = self.timestamp return { 'timestamp': timestamp, 'accZ': np.array(self.accZ, float).reshape(64, 1) * ACC_SCALE_FACTOR, 'errorFlags': np.full((64, 1), self.errorFlags, np.uint8), } def __repr__(self): vals = self.parse() return f'{self.__class__.__name__}(t={self.timestamp:_}, ' \ f'accZ in m/s²: {vals["accZ"].flatten()}, errorFlags={self.errorFlags})'
[docs] @register_package class DataSyncTrigger(AbstractPackage): """ Hardware synchronization event. When a rising or falling edge of the USB-C sideband use pin (SBU) is detected, a ``DataSyncTrigger`` package with the current sensor timestamp is generated. Those packages can be used for precise time synchronization between the sensors and other systems. **Fields**: * **timestamp**: *int64* -- Timestamp (in nanoseconds). * **value**: *uint8* -- 0 for falling edge, 1 for rising edge. 8 + 1 = 9 bytes """ header = SensorHeader.DATA_SYNC_TRIGGER """SensorHeader.DATA_SYNC_TRIGGER (0x0400)""" _fields_ = [ ('timestamp', ctypes.c_int64), ('value', ctypes.c_uint8), ]
[docs] @register_package class DataFsFileCount(AbstractPackage): """ Number of files on the sensor. **Fields**: * **fileCount**: *uint16* -- Number of files. 2 bytes """ header = SensorHeader.DATA_FS_FILE_COUNT """SensorHeader.DATA_FS_FILE_COUNT (0x0501)""" _fields_ = [ ('fileCount', ctypes.c_uint16), ] def __repr__(self): s = f'{self.__class__.__name__}(fileCount={self.fileCount})' return s
[docs] @register_package class DataFsFile(AbstractPackage): """ Information about a file on the sensor. **Fields**: * **index**: *uint16* -- Index. * **filename**: *char[65]* -- File name. * **size**: *uint32* -- File size in bytes. 2 + 65 + 4 = 71 bytes """ header = SensorHeader.DATA_FS_FILE """SensorHeader.DATA_FS_FILE (0x0502)""" _fields_ = [ ('index', ctypes.c_uint16), ('filename', ctypes.c_char * 65), ('size', ctypes.c_uint32), ] def __repr__(self): s = f'{self.__class__.__name__}(index={self.index},' \ f' filename={self.filename}, size={self.size})' return s
[docs] @register_package class CmdFsGetBytes(AbstractPackage): """ Command to get bytes from a file on the sensor. **Fields**: * **filename**: *char[65]* -- File name. * **startPos**: *uint32* -- Start position in the file. * **endPos**: *uint32* -- End position in the file. Set to 0 to transfer the whole file. 65 + 4 + 4 = 73 bytes """ header = SensorHeader.CMD_FS_GET_BYTES """SensorHeader.CMD_FS_GET_BYTES (0x0503)""" _fields_ = [ ('filename', ctypes.c_char * 65), ('startPos', ctypes.c_uint32), ('endPos', ctypes.c_uint32), ]
[docs] @register_package class DataFsBytes(AbstractPackage): """ Chunk of bytes from a file on the sensor. **Fields**: * **offset**: *uint32* -- Offset in the file. * **payload**: *uint8[232]* -- Payload data (``MAX_PAYLOAD_SIZE`` - 4). 4 + 232 = 236 bytes """ header = SensorHeader.DATA_FS_BYTES """SensorHeader.DATA_FS_BYTES (0x0504)""" variable_size = True payload = bytes() _fields_ = [ ('offset', ctypes.c_uint32), ] @classmethod def frombytes(cls, val): pkg = cls.from_buffer_copy(val[0:ctypes.sizeof(ctypes.c_int32)]) pkg.payload = val[ctypes.sizeof(ctypes.c_int32):] return pkg def parse(self): vals = super().parse() vals['payload'] = self.payload return vals def __repr__(self): return f'{self.__class__.__name__}({self.offset}, {len(self.payload)} bytes: {self.payload.hex()})'
[docs] class DataFsFilename(AbstractPackage): """ This package contains a file name for filesystem operations, used by various commands. **Fields**: * **filename**: *char[65]* -- File name. 65 bytes """ _fields_ = [ ('filename', ctypes.c_char * 65), ]
@register_package class CmdFsGetSize(DataFsFilename): header = SensorHeader.CMD_FS_GET_SIZE """SensorHeader.CMD_FS_GET_SIZE (0x0507)""" @register_package class CmdFsDeleteFile(DataFsFilename): header = SensorHeader.CMD_FS_DELETE_FILE """SensorHeader.CMD_FS_DELETE_FILE (0x0509)""" @register_package class AckFsDeleteFile(DataFsFilename): header = SensorHeader.ACK_FS_DELETE_FILE """SensorHeader.ACK_FS_DELETE_FILE (0x050A)"""
[docs] @register_package class DataFsSize(AbstractPackage): """ Size of a file on the sensor. **Fields**: * **filename**: *char[65]* -- File name. * **fileSize**: *uint32* -- File size in bytes. 65 + 4 = 69 bytes """ header = SensorHeader.DATA_FS_SIZE """SensorHeader.DATA_FS_SIZE (0x0508)""" _fields_ = [ ('filename', ctypes.c_char * 65), ('fileSize', ctypes.c_uint32), ]
[docs] @register_package class SensorError(AbstractPackage): """ Represents an error that occurred on the sensor. **Fields**: * **errorCode**: *ErrorCode, uint8* -- Error code. * **command**: *SensorHeader, uint16* -- If applicable: the command that caused the error, otherwise: ``ERROR``. 1 + 2 = 3 bytes """ header = SensorHeader.ERROR """SensorHeader.ERROR (0xFFFF)""" _fields_ = [ ('errorCode', ctypes.c_uint8), ('command', ctypes.c_uint16), ] def __repr__(self): name = self.__class__.__name__ return f'{name}(errorCode={ErrorCode(self.errorCode)!r}, command: {SensorHeader(self.command)!r})'
[docs] class SensorSerialPackage(AbstractPackage): """ Package frame including header, payload size, and checksum. **Fields**: * **startByte**: *uint8* -- Fixed 0x02 byte. * **crc32**: *uint32* -- Checksum calculated over ``header`` and ``payload``. * **payloadSize**: *uint8* -- Size of ``payload``, between 0 and 236. * **header**: *SensorHeader, uint16* -- Value that identifies the command or data package. * **payload**: *uint8[236]* -- Variable-length payload, between 0 and 236 bytes. 1 + 4 + 1 + 2 + 236 = 244 bytes """ _fields_ = [ ('startByte', ctypes.c_uint8), ('crc32', ctypes.c_uint32), ('payloadSize', ctypes.c_uint8), ('header', ctypes.c_uint16), # + variable length payload, up until MAX_PAYLOAD_SIZE bytes ]