Added ValueBuffer implementation and simple recording

ValueBuffer is now the default buffer object for storing data for
recording and processing purposes, a simple recording implementation has
been done, core.py example starts recording as soon as the core has
started then on exit it saves the output as a csv file through the
ValueBuffer export_csv method
This commit is contained in:
2026-08-06 12:57:59 +01:00
parent 1690032f5b
commit ffcea13c0b
5 changed files with 80 additions and 20 deletions
+4
View File
@@ -8,3 +8,7 @@ wheels/
# Virtual environments # Virtual environments
.venv .venv
# Output files
*.csv
*.dlpak
+8 -2
View File
@@ -6,11 +6,11 @@ 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
from time import monotonic, monotonic_ns, sleep
from typing import Literal from typing import Literal
from uuid import UUID from uuid import UUID
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.errors import CoreStateMismatchError from dynalab_core.errors import CoreStateMismatchError
@@ -33,10 +33,10 @@ class Core:
"unintid" "unintid"
) )
self._recording = threading.Event() self._recording = threading.Event()
self._recording_buffer = ValueBuffer()
self._core_version: VersionDescriptor = CORE_VERSION self._core_version: VersionDescriptor = CORE_VERSION
self._stop_event: threading.Event = threading.Event() self._stop_event: threading.Event = threading.Event()
self._core_config: CoreConfig = config self._core_config: CoreConfig = config
# TODO: Implement live values dict
self._live_values: dict[UUID, Value] = {} self._live_values: dict[UUID, Value] = {}
self._live_values_lock = Lock() self._live_values_lock = Lock()
@@ -105,10 +105,12 @@ class Core:
) )
def start_recording(self) -> None: def start_recording(self) -> None:
self._recording_buffer.clear()
self._recording.set() self._recording.set()
def stop_recording(self) -> None: def stop_recording(self) -> None:
self._recording.clear() self._recording.clear()
self._recording_buffer.export_csv("output.csv")
def get_signal_descriptor(self, signal_id: UUID) -> SignalDescriptor | None: def get_signal_descriptor(self, signal_id: UUID) -> SignalDescriptor | None:
return self._connector_registry.get_signal_descriptor(signal_id) return self._connector_registry.get_signal_descriptor(signal_id)
@@ -155,3 +157,7 @@ class Core:
self._live_values[message.signal_id] = live_value self._live_values[message.signal_id] = live_value
live_value.update(message.value, message.timestamp) live_value.update(message.value, message.timestamp)
if self._recording.is_set():
self._recording_buffer.append(
message.timestamp, message.signal_id, message.value
)
+60
View File
@@ -0,0 +1,60 @@
# Copyright (C) 2026 Hector van der Aa <hector@h3cx.dev>
# Copyright (C) 2026 Association Exergie <association.exergie@gmail.com>
# SPDX-License-Identifier: GPL-3.0-or-later
from collections import defaultdict
import csv
from pathlib import Path
from threading import Lock
from uuid import UUID
class ValueBuffer:
def __init__(self) -> None:
self._samples: list[tuple[int, UUID, float]] = []
self._lock = Lock()
def append(self, timestamp: int, uuid: UUID, value: float) -> None:
with self._lock:
self._samples.append((timestamp, uuid, value))
def clear(self) -> None:
with self._lock:
self._samples.clear()
def __len__(self) -> int:
with self._lock:
return len(self._samples)
def export_csv(self, path: str | Path) -> None:
with self._lock:
samples = list(self._samples)
rows: dict[int, dict[UUID, float]] = defaultdict(dict)
signal_ids: set[UUID] = set()
for timestamp_ns, signal_id, value in samples:
rows[timestamp_ns][signal_id] = value
signal_ids.add(signal_id)
ordered_signal_ids = sorted(signal_ids, key=str)
with Path(path).open(mode="w", newline="", encoding="utf-8") as file:
writer = csv.writer(file)
writer.writerow(
["timestamp_ns", *(str(signal_id) for signal_id in ordered_signal_ids)]
)
for timestamp_ns in sorted(rows):
timestamp_values = rows[timestamp_ns]
writer.writerow(
[
timestamp_ns,
*(
timestamp_values.get(signal_id, "")
for signal_id in ordered_signal_ids
),
]
)
+2
View File
@@ -19,6 +19,7 @@ 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()
try: try:
while True: while True:
@@ -27,4 +28,5 @@ try:
log.debug(f"Core values: {values}") log.debug(f"Core values: {values}")
except KeyboardInterrupt: except KeyboardInterrupt:
log.info("Received keyboard interrupt") log.info("Received keyboard interrupt")
dl_core.stop_recording()
dl_core.stop() dl_core.stop()
+6 -18
View File
@@ -116,24 +116,12 @@ def value_sender_thread(
next_send_time += period next_send_time += period
value = ValueDescriptor( message = ValueBatch(values=[])
signal_id=signal_id, value=2.0, timestamp=monotonic_ns() for i in range(10):
) value = ValueDescriptor(
signal_id=signal_id, value=2.0, timestamp=monotonic_ns()
message = ValueBatch( )
values=[ message.values.append(value)
value,
value,
value,
value,
value,
value,
value,
value,
value,
value,
]
)
future = asyncio.run_coroutine_threadsafe( future = asyncio.run_coroutine_threadsafe(
send_message( send_message(