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:
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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."""
|
||||||
|
|||||||
@@ -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
|
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
Reference in New Issue
Block a user