DeriveUnits are now owned by a DeriveRegistry which has its own routing thread to handle routing values to the different DeriveUnits
258 lines
8.4 KiB
Markdown
258 lines
8.4 KiB
Markdown
# DynaLab Core API
|
|
|
|
`Core` is the Python interface for running a DynaLab Core instance, inspecting
|
|
connected signals and their current values, recording incoming data, and
|
|
creating derived signals. Connectors provide data to a running core using the
|
|
[DynaLab protocol](Protocol.md).
|
|
|
|
> [!WARNING]
|
|
> DynaLab Core is in alpha. Its public API may change between releases.
|
|
|
|
## Quick Start
|
|
|
|
Create a configuration, construct the core, then start it. Always stop the core
|
|
when the application exits so its server and worker threads are shut down.
|
|
|
|
```python
|
|
from dynalab_core import Core
|
|
from dynalab_core.config import CoreConfig
|
|
|
|
core = Core(CoreConfig(host="127.0.0.1", port=58763))
|
|
|
|
try:
|
|
core.start()
|
|
core.wait() # Blocks until stop() is called or a timeout expires.
|
|
finally:
|
|
core.stop()
|
|
```
|
|
|
|
`Core` does not currently implement the context-manager protocol. Use
|
|
`try`/`finally` when the core may be started.
|
|
|
|
## Configuration
|
|
|
|
`CoreConfig` is defined in `dynalab_core.config`.
|
|
|
|
```python
|
|
from dynalab_core.config import CoreConfig
|
|
|
|
config = CoreConfig(
|
|
host="127.0.0.1", # Default
|
|
port=58763, # Default; must be in the range 0 through 65535
|
|
)
|
|
```
|
|
|
|
| Field | Type | Default | Description |
|
|
| --- | --- | --- | --- |
|
|
| `host` | `str` | `"127.0.0.1"` | Address on which the core server listens. |
|
|
| `port` | `int` | `58763` | TCP port on which the core server listens. |
|
|
|
|
`CoreConfig.bind_str() -> str` returns the configured address in
|
|
`"host:port"` form.
|
|
|
|
## Lifecycle
|
|
|
|
### `Core(config: CoreConfig)`
|
|
|
|
Creates a core and starts its internal input worker. The network server is not
|
|
started until `start()` is called.
|
|
|
|
### `start() -> None`
|
|
|
|
Starts the configured server and begins accepting connector connections. This
|
|
method returns after the server start sequence completes.
|
|
|
|
Call `start()` once per `Core` instance. Calling it in any state other than the
|
|
initial state raises `CoreStateMismatchError`. Server startup failures, such as
|
|
an address already in use, are propagated by the underlying server.
|
|
|
|
### `wait(timeout: float | None = None) -> None`
|
|
|
|
Blocks until `stop()` is called or until `timeout` seconds have elapsed. Omit
|
|
`timeout` to wait indefinitely. This is useful for keeping a command-line
|
|
application alive without a busy loop.
|
|
|
|
### `stop() -> None`
|
|
|
|
Stops the server, disconnects connectors, signals the input worker to stop, and
|
|
stops all bound derive units. It also releases any call currently blocked in
|
|
`wait()`.
|
|
|
|
Call this during application shutdown, including when startup or runtime work
|
|
raises an exception. A stopped core cannot be started again.
|
|
|
|
## Signals
|
|
|
|
Signals are registered by connectors during their protocol handshake. A signal
|
|
is described by `SignalDescriptor` from
|
|
`dynalab_core.protocols.packets.handshake`.
|
|
|
|
```python
|
|
from uuid import UUID
|
|
|
|
from dynalab_core.protocols.packets.handshake import SignalDescriptor
|
|
|
|
signal = SignalDescriptor(
|
|
id=UUID("2d60a378-f37c-4c37-867b-c68f5116598b"),
|
|
name="Engine RPM",
|
|
type="number",
|
|
min_value=0.0,
|
|
max_value=6000.0,
|
|
unit="rpm",
|
|
timeout_ms=2_000,
|
|
)
|
|
```
|
|
|
|
| Field | Type | Default | Description |
|
|
| --- | --- | --- | --- |
|
|
| `id` | `UUID` | Required | Stable, unique signal identifier. |
|
|
| `name` | `str` | Required | Human-readable signal name. |
|
|
| `type` | `"number"` or `"binary"` | Required | Signal value category. Values are represented as `float` by the current data API. |
|
|
| `min_value` | `float \| None` | `None` | Optional lower bound. |
|
|
| `max_value` | `float \| None` | `None` | Optional upper bound. |
|
|
| `unit` | `str \| None` | `None` | Optional display unit. |
|
|
| `timeout_ms` | `int` | `2000` | How long a received value remains live. |
|
|
| `origin` | `"source"` or `"derived"` | `"source"` | Whether a connector or a derive unit produces the signal. |
|
|
|
|
### `get_signal_descriptor(signal_id: UUID) -> SignalDescriptor | None`
|
|
|
|
Returns the descriptor for `signal_id`, including a derived-signal output, or
|
|
`None` when no matching signal is registered. Use a UUID, not its string
|
|
representation.
|
|
|
|
### `get_all_signal_descriptors() -> list[SignalDescriptor]`
|
|
|
|
Returns descriptors for every signal currently registered by connected
|
|
connectors and bound derive units. The list is empty when no connectors have
|
|
completed a handshake and no derive units are bound.
|
|
|
|
Connector membership is dynamic: a disconnected connector's signals are no
|
|
longer returned.
|
|
|
|
## Live Values
|
|
|
|
After a connector sends a value, Core retains its latest value until the
|
|
signal's `timeout_ms` has elapsed. A stale, unknown, or not-yet-received value
|
|
is represented by `None`.
|
|
|
|
### `get_live_value(signal_id: UUID) -> float | None`
|
|
|
|
Returns the latest non-stale value for one signal.
|
|
|
|
```python
|
|
rpm = core.get_live_value(signal_id)
|
|
if rpm is not None:
|
|
print(f"Engine speed: {rpm:.0f} rpm")
|
|
```
|
|
|
|
### `get_all_live_values() -> dict[UUID, float]`
|
|
|
|
Returns a snapshot mapping signal IDs to their latest non-stale values. Signals
|
|
without a current value are omitted.
|
|
|
|
```python
|
|
for signal_id, value in core.get_all_live_values().items():
|
|
print(signal_id, value)
|
|
```
|
|
|
|
Incoming data timestamps use the monotonic-nanosecond timebase. See
|
|
[Protocol timestamps](Protocol.md#timestamps) when implementing a connector.
|
|
|
|
## Recording
|
|
|
|
Recording captures incoming connector values in an in-memory buffer.
|
|
|
|
### `start_recording() -> None`
|
|
|
|
Clears the current recording buffer, records a new UTC start timestamp, and
|
|
begins capturing subsequent values.
|
|
|
|
> [!IMPORTANT]
|
|
> Starting a recording discards any values captured by a previous recording.
|
|
|
|
### `stop_recording() -> None`
|
|
|
|
Stops capture, normalizes the recorded data, and creates a processing buffer
|
|
containing the completed recording and its signal manifest.
|
|
|
|
> [!IMPORTANT]
|
|
> Stopping a recording replaces the previous processing buffer. Persist or
|
|
> process it before stopping another recording.
|
|
|
|
There is currently no public `Core` method for retrieving or writing the
|
|
completed processing buffer. The `.dlpak` writer is implemented by the
|
|
internal `DLPak` type; applications relying on it must use private state and
|
|
therefore should expect that integration to change.
|
|
|
|
## Derived Signals
|
|
|
|
A derive unit calculates a new value whenever one of its input signals receives
|
|
an update and all inputs have non-stale live values. Define the calculation as a
|
|
Python source string and bind it with `bind_derive_unit()`.
|
|
|
|
### `bind_derive_unit(function, input_signals, output_signal) -> UUID`
|
|
|
|
```python
|
|
from uuid import uuid4
|
|
|
|
from dynalab_core.protocols.packets.handshake import SignalDescriptor
|
|
|
|
processing_function = """
|
|
from dynalab_core.protocols.packets.data import ValueDescriptor
|
|
from dynalab_core.protocols.packets.handshake import SignalDescriptor
|
|
|
|
def add(left: ValueDescriptor, right: ValueDescriptor, output: SignalDescriptor) -> ValueDescriptor:
|
|
return ValueDescriptor(
|
|
signal_id=output.id,
|
|
value=left.value + right.value,
|
|
timestamp=left.timestamp,
|
|
)
|
|
"""
|
|
|
|
output_signal = SignalDescriptor(
|
|
id=uuid4(),
|
|
name="Total",
|
|
type="number",
|
|
origin="derived",
|
|
)
|
|
|
|
unit_id = core.bind_derive_unit(
|
|
processing_function,
|
|
input_signals=[left_signal, right_signal],
|
|
output_signal=output_signal,
|
|
)
|
|
```
|
|
|
|
`function` must define exactly one Python function. Its parameters must be:
|
|
|
|
1. Zero or more `ValueDescriptor` parameters, in the same order as
|
|
`input_signals`.
|
|
2. One final `SignalDescriptor` parameter for `output_signal`.
|
|
3. A return annotation of `ValueDescriptor`.
|
|
|
|
The parameter annotations must use the imported `ValueDescriptor` and
|
|
`SignalDescriptor` classes shown above. The source string is executed with
|
|
`exec()`, so only bind code from trusted sources.
|
|
|
|
The returned UUID identifies the bound unit. Binding source that defines more
|
|
or fewer than one function raises `DeriveUnitMultipleFunctionsFoundError`; an
|
|
invalid function signature raises `DeriveUnitInvalidSignatureError`.
|
|
|
|
### `unbind_derive_unit(unit_id: UUID) -> None`
|
|
|
|
Stops and removes the derive unit identified by `unit_id`. Passing an unknown
|
|
or already unbound ID has no effect.
|
|
|
|
The returned ID is no longer valid after unbinding. Create and bind a new unit
|
|
to resume derived-value processing.
|
|
|
|
## Exceptions
|
|
|
|
Core-specific exceptions are defined in `dynalab_core.errors`.
|
|
|
|
| Exception | Raised when |
|
|
| --- | --- |
|
|
| `CoreStateMismatchError` | `start()` is called after the core has already started or stopped. |
|
|
| `DeriveUnitMultipleFunctionsFoundError` | A derive-unit source string does not define exactly one function. |
|
|
| `DeriveUnitInvalidSignatureError` | A derive function's annotations or parameter order are invalid. |
|