Files
dynalab-core/docs/API.md
T
h3cx 19b356a94d Moved derive units to registry architectur
DeriveUnits are now owned by a DeriveRegistry which has its own routing
thread to handle routing values to the different DeriveUnits
2026-09-11 18:50:32 +02:00

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. |