Added derive units to core

Derive units are now available in the core and has their relevant input
values routed and outputs are looped back into the core router

Fixed the dlpak manifest which had no signal descriptors
This commit is contained in:
2026-09-09 16:09:02 +02:00
parent f0638788fc
commit c67316dd90
8 changed files with 176 additions and 23 deletions
+68 -4
View File
@@ -3,21 +3,25 @@
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from collections.abc import Callable
from datetime import datetime, timezone from datetime import datetime, timezone
import inspect
import logging import logging
from queue import Empty, Queue from queue import Empty, Queue
import threading import threading
from threading import Lock, Thread from threading import Lock, Thread
import time import time
import types
from typing import Literal from typing import Literal
from uuid import UUID from uuid import UUID, uuid4
from dynalab_core.buffer import ValueBuffer from dynalab_core.buffer import ValueBuffer
from dynalab_core.config import CoreConfig from dynalab_core.config import CoreConfig
from dynalab_core.constants import CORE_VERSION from dynalab_core.constants import CORE_VERSION
from dynalab_core.derive import DeriveUnit
from dynalab_core.dlpak import DLPak from dynalab_core.dlpak import DLPak
from dynalab_core.errors import CoreStateMismatchError from dynalab_core.errors import CoreMultipleFunctionsFoundError, CoreStateMismatchError
from dynalab_core.protocols.endpoint import ConnectorRegistry from dynalab_core.protocols.endpoint import ConnectorRegistry
from dynalab_core.protocols.common import VersionDescriptor from dynalab_core.protocols.common import VersionDescriptor
from dynalab_core.protocols.json.server import JsonServer from dynalab_core.protocols.json.server import JsonServer
@@ -46,6 +50,8 @@ class Core:
self._live_values: dict[UUID, Value] = {} self._live_values: dict[UUID, Value] = {}
self._live_values_lock = Lock() self._live_values_lock = Lock()
self._derive_units: dict[UUID, DeriveUnit] = {}
self._data_input_queue: Queue[ProtocolMessage] = Queue(524288) self._data_input_queue: Queue[ProtocolMessage] = Queue(524288)
self._input_worker_thread = Thread( self._input_worker_thread = Thread(
target=self._input_worker, name="input_worker_thread", daemon=True target=self._input_worker, name="input_worker_thread", daemon=True
@@ -104,6 +110,8 @@ class Core:
self._json_server.stop() self._json_server.stop()
self._connector_registry.stop() self._connector_registry.stop()
self._stop_event.set() self._stop_event.set()
for unit in self._derive_units.values():
unit.stop()
self._state = "stopped" self._state = "stopped"
log.info( log.info(
"Core stopped", "Core stopped",
@@ -121,9 +129,12 @@ class Core:
self._recording_buffer.normalize() self._recording_buffer.normalize()
self._processing_buffer = DLPak() self._processing_buffer = DLPak()
self._processing_buffer.set_data(self._recording_buffer) self._processing_buffer.set_data(self._recording_buffer)
signal_descriptors = self._connector_registry.get_all_signal_descriptors()
for unit in self._derive_units.values():
signal_descriptors.append(unit._return_signal)
self._processing_buffer.set_manifest( self._processing_buffer.set_manifest(
self._recording_timestamp, self._recording_timestamp, signal_descriptors
self._connector_registry.get_all_signal_descriptors(),
) )
# TODO: remove debug behavior default saving to output # TODO: remove debug behavior default saving to output
self._processing_buffer.write("./", "output") self._processing_buffer.write("./", "output")
@@ -148,6 +159,35 @@ class Core:
return {signal_id: value.get() for signal_id, value in live_values} return {signal_id: value.get() for signal_id, value in live_values}
def bind_derive_unit(
self,
function: str,
input_signals: list[SignalDescriptor],
output_signal: SignalDescriptor,
) -> UUID:
unit_id = uuid4()
namespace = {}
exec(function, namespace)
functions = [
obj for obj in namespace.values() if isinstance(obj, types.FunctionType)
]
if len(functions) != 1:
raise CoreMultipleFunctionsFoundError
unit = DeriveUnit(
unit_id, functions[0], input_signals, output_signal, self._data_input_queue
)
self._derive_units[unit_id] = unit
return unit_id
def unbind_derive_unit(self, unit_id: UUID) -> None:
self._derive_units[unit_id].stop()
def _input_worker(self) -> None: def _input_worker(self) -> None:
while not self._stop_event.is_set(): while not self._stop_event.is_set():
try: try:
@@ -177,3 +217,27 @@ class Core:
self._recording_buffer.append( self._recording_buffer.append(
message.timestamp, message.signal_id, message.value message.timestamp, message.signal_id, message.value
) )
for unit in self._derive_units.values():
input_signals = unit.get_input_signals()
if message.signal_id in [signal.id for signal in input_signals]:
args: list[ValueDescriptor] = []
for signal in input_signals:
if signal.id == message.signal_id:
args.append(message)
continue
with self._live_values_lock:
live_value = self._live_values.get(signal.id)
value_descriptor = (
live_value.to_descriptor(signal.id)
if live_value is not None
else None
)
if value_descriptor is None:
break
args.append(value_descriptor)
else:
unit.put_data(args)
+2
View File
@@ -32,6 +32,8 @@ class ValueBuffer:
return len(self._samples) return len(self._samples)
def normalize(self) -> None: def normalize(self) -> None:
if len(self) == 0:
return
with self._lock: with self._lock:
minimum = min(sample[0] for sample in self._samples) minimum = min(sample[0] for sample in self._samples)
+16 -5
View File
@@ -8,8 +8,6 @@ import inspect
from queue import Empty, Queue from queue import Empty, Queue
from threading import Thread from threading import Thread
import threading import threading
from time import sleep
from typing import Literal
from uuid import UUID from uuid import UUID
from dynalab_core.errors import ( from dynalab_core.errors import (
@@ -33,13 +31,18 @@ class DeriveUnit:
processing_function: Callable[..., ValueDescriptor], processing_function: Callable[..., ValueDescriptor],
input_signals: list[SignalDescriptor], input_signals: list[SignalDescriptor],
return_signal: SignalDescriptor, return_signal: SignalDescriptor,
return_queue: Queue[ValueDescriptor],
) -> None: ) -> None:
self._id: UUID = unit_uuid self._id: UUID = unit_uuid
self._process_function: Callable = processing_function self._process_function: Callable = processing_function
self._return_signal: SignalDescriptor = return_signal self._return_signal: SignalDescriptor = return_signal
self._input_signals: list[SignalDescriptor] = input_signals
self._stop_event: threading.Event = threading.Event() self._stop_event: threading.Event = threading.Event()
self._stopped_event: threading.Event = threading.Event()
self._input_queue: Queue[list[ValueDescriptor]] = Queue() self._input_queue: Queue[list[ValueDescriptor]] = Queue()
# TODO: Deprecate output queue
self._output_queue: Queue[ValueDescriptor] = Queue() self._output_queue: Queue[ValueDescriptor] = Queue()
self._return_queue: Queue[ValueDescriptor] = return_queue
self._num_input_args: int = 0 self._num_input_args: int = 0
self._parser_state: int = ParserState.INIT self._parser_state: int = ParserState.INIT
@@ -80,9 +83,10 @@ class DeriveUnit:
if len(input_args) != self._num_input_args: if len(input_args) != self._num_input_args:
continue continue
self._output_queue.put( value = self._process_function(*input_args, self._return_signal)
self._process_function(*input_args, self._return_signal) self._return_queue.put(value)
) self._output_queue.put(value)
self._stopped_event.set()
def put_data(self, input: list[ValueDescriptor]) -> None: def put_data(self, input: list[ValueDescriptor]) -> None:
self._input_queue.put(input) self._input_queue.put(input)
@@ -106,3 +110,10 @@ class DeriveUnit:
if len(input_args) == self._num_input_args: if len(input_args) == self._num_input_args:
return self._process_function(*input_args, self._return_signal) return self._process_function(*input_args, self._return_signal)
raise DeriveUnitArgsMismatchError raise DeriveUnitArgsMismatchError
def get_input_signals(self) -> list[SignalDescriptor]:
return self._input_signals
def stop(self) -> None:
self._stop_event.set()
self._stopped_event.wait(5.0)
+4
View File
@@ -11,6 +11,10 @@ class CoreStateMismatchError(CoreError):
"""DynaLab Core state error.""" """DynaLab Core state error."""
class CoreMultipleFunctionsFoundError(CoreError):
"""DynaLab Core multiple functions found while trying to create derive unit."""
# DLPak error declarations # DLPak error declarations
class DLPakError(Exception): class DLPakError(Exception):
"""DLPak error.""" """DLPak error."""
+26 -4
View File
@@ -5,6 +5,9 @@
from threading import Lock from threading import Lock
from time import monotonic, monotonic_ns from time import monotonic, monotonic_ns
from uuid import UUID
from dynalab_core.protocols.packets.data import ValueDescriptor
class Value: class Value:
@@ -20,11 +23,30 @@ class Value:
self._last_updated = timestamp self._last_updated = timestamp
def get(self) -> float | None: def get(self) -> float | None:
current_value = self._get_current_value()
return current_value[0] if current_value is not None else None
def to_descriptor(self, signal_id: UUID) -> ValueDescriptor | None:
current_value = self._get_current_value()
if current_value is None:
return None
value, timestamp = current_value
return ValueDescriptor(
signal_id=signal_id,
value=value,
timestamp=timestamp,
)
def _get_current_value(self) -> tuple[float, int] | None:
with self._lock:
value = self._value
last_updated = self._last_updated
now = monotonic_ns() now = monotonic_ns()
if ( if (
now > self._last_updated + self._timeout_ms * 1_000_000 now > last_updated + self._timeout_ms * 1_000_000
or self._last_updated == 0 or last_updated == 0
): ):
return None return None
with self._lock: return value, last_updated
return self._value
+30
View File
@@ -1,9 +1,13 @@
import logging import logging
from pathlib import Path
from time import sleep
from uuid import UUID, uuid4
from rich.logging import RichHandler from rich.logging import RichHandler
from dynalab_core import Core from dynalab_core import Core
from dynalab_core.config import CoreConfig from dynalab_core.config import CoreConfig
from dynalab_core.protocols.packets.handshake import SignalDescriptor
logging.basicConfig( logging.basicConfig(
level=logging.DEBUG, level=logging.DEBUG,
@@ -14,13 +18,39 @@ logging.basicConfig(
) )
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
processing_str = Path(
"/home/hector/projects/Exergie/dynalab-core/test/manual/process.py"
).read_text(encoding="utf-8")
signal_id_1: UUID = UUID("3f32cea3-d872-4c16-a0a2-54b57171aeb2")
signal_id_2: UUID = UUID("6578ac37-99d1-410c-b3f7-d049339919b2")
config = CoreConfig(port=8765) config = CoreConfig(port=8765)
log.info("Configured manual core on %s", config.bind_str()) log.info("Configured manual core on %s", config.bind_str())
dl_core = Core(config) dl_core = Core(config)
dl_core.start() dl_core.start()
dl_core.start_recording() dl_core.start_recording()
for i in range(2):
dl_core.wait(1)
values = dl_core.get_all_live_values()
log.debug(f"Core values: {values}")
unit_id = dl_core.bind_derive_unit(
processing_str,
[
SignalDescriptor(
id=signal_id_1, name="Dummy signal 1", type="number", timeout_ms=5000
),
SignalDescriptor(
id=signal_id_2, name="Dummy signal 2", type="number", timeout_ms=5000
),
],
SignalDescriptor(id=uuid4(), name="Dummy sum", type="number", timeout_ms=5000),
)
try: try:
while True: while True:
dl_core.wait(1) dl_core.wait(1)
+19 -10
View File
@@ -52,7 +52,8 @@ logging.basicConfig(
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
signal_id: UUID = uuid4() signal_id_1: UUID = UUID("3f32cea3-d872-4c16-a0a2-54b57171aeb2")
signal_id_2: UUID = UUID("6578ac37-99d1-410c-b3f7-d049339919b2")
def monotonic_ms() -> int: def monotonic_ms() -> int:
@@ -117,13 +118,18 @@ def value_sender_thread(
next_send_time += period next_send_time += period
message = ValueBatch(values=[]) message = ValueBatch(values=[])
for i in range(10): value1 = ValueDescriptor(
value = ValueDescriptor( signal_id=signal_id_1,
signal_id=signal_id, value=2.0,
value=2.0, timestamp=monotonic_ns(),
timestamp=monotonic_ns(), )
) value2 = ValueDescriptor(
message.values.append(value) signal_id=signal_id_2,
value=1.0,
timestamp=monotonic_ns(),
)
message.values.append(value1)
message.values.append(value2)
future = asyncio.run_coroutine_threadsafe( future = asyncio.run_coroutine_threadsafe(
send_message( send_message(
@@ -205,8 +211,11 @@ async def perform_handshake(
connector_version=CONNECTOR_VERSION.get_version(), connector_version=CONNECTOR_VERSION.get_version(),
signals=[ signals=[
SignalDescriptor( SignalDescriptor(
id=signal_id, name="Dummy signal", type="number", timeout_ms=5000 id=signal_id_1, name="Dummy signal 1", type="number", timeout_ms=5000
) ),
SignalDescriptor(
id=signal_id_2, name="Dummy signal 2", type="number", timeout_ms=5000
),
], ],
) )
+11
View File
@@ -0,0 +1,11 @@
from dynalab_core.protocols.packets.data import ValueDescriptor
from dynalab_core.protocols.packets.handshake import SignalDescriptor
def process(
a: ValueDescriptor, b: ValueDescriptor, r: SignalDescriptor
) -> ValueDescriptor:
val = ValueDescriptor(
signal_id=r.id, value=a.value * b.value, timestamp=a.timestamp
)
return val