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