Implemented initial DLPak design

Implemented DLPak design with zip storage, DLPak consists of two files,
a manifest.json which provides details on the signals, time of recording
and currently blank comment field and data.csv which stores all the
signals as csv with the nanosecond timestamp and indexed against the
signal id
This commit is contained in:
2026-08-06 15:29:02 +01:00
parent ffcea13c0b
commit 1cd6bb695c
4 changed files with 113 additions and 17 deletions
+13 -1
View File
@@ -2,6 +2,8 @@
# Copyright (C) 2026 Association Exergie <association.exergie@gmail.com>
# SPDX-License-Identifier: GPL-3.0-or-later
from datetime import datetime, timezone
import logging
from queue import Empty, Queue
import threading
@@ -13,6 +15,7 @@ from uuid import UUID
from dynalab_core.buffer import ValueBuffer
from dynalab_core.config import CoreConfig
from dynalab_core.constants import CORE_VERSION
from dynalab_core.dlpak import DLPak
from dynalab_core.errors import CoreStateMismatchError
from dynalab_core.protocols.endpoint import ConnectorRegistry
from dynalab_core.protocols.common import VersionDescriptor
@@ -34,6 +37,8 @@ class Core:
)
self._recording = threading.Event()
self._recording_buffer = ValueBuffer()
self._recording_timestamp: datetime = datetime.now(timezone.utc)
self._processing_buffer: DLPak | None = None
self._core_version: VersionDescriptor = CORE_VERSION
self._stop_event: threading.Event = threading.Event()
self._core_config: CoreConfig = config
@@ -106,11 +111,18 @@ class Core:
def start_recording(self) -> None:
self._recording_buffer.clear()
self._recording_timestamp = datetime.now(timezone.utc)
self._recording.set()
def stop_recording(self) -> None:
self._recording.clear()
self._recording_buffer.export_csv("output.csv")
self._processing_buffer = DLPak()
self._processing_buffer.set_data(self._recording_buffer)
self._processing_buffer.set_manifest(
self._recording_timestamp,
self._connector_registry.get_all_signal_descriptors(),
)
self._processing_buffer.write("./", "output")
def get_signal_descriptor(self, signal_id: UUID) -> SignalDescriptor | None:
return self._connector_registry.get_signal_descriptor(signal_id)
+11 -3
View File
@@ -4,6 +4,7 @@
from collections import defaultdict
import csv
import io
from pathlib import Path
from threading import Lock
from uuid import UUID
@@ -22,11 +23,15 @@ class ValueBuffer:
with self._lock:
self._samples.clear()
def get_signal_ids(self) -> list[UUID]:
with self._lock:
return list({signal_id for _, signal_id, _ in self._samples})
def __len__(self) -> int:
with self._lock:
return len(self._samples)
def export_csv(self, path: str | Path) -> None:
def export_csv(self) -> str:
with self._lock:
samples = list(self._samples)
@@ -39,8 +44,9 @@ class ValueBuffer:
ordered_signal_ids = sorted(signal_ids, key=str)
with Path(path).open(mode="w", newline="", encoding="utf-8") as file:
writer = csv.writer(file)
output = io.StringIO(newline="")
writer = csv.writer(output)
writer.writerow(
["timestamp_ns", *(str(signal_id) for signal_id in ordered_signal_ids)]
@@ -58,3 +64,5 @@ class ValueBuffer:
),
]
)
return output.getvalue()
+67
View File
@@ -0,0 +1,67 @@
# 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 datetime import datetime, timezone
from pathlib import Path
from typing import Literal
from zipfile import ZIP_DEFLATED, ZipFile
from pydantic import BaseModel, Field
from dynalab_core.buffer import ValueBuffer
from dynalab_core.errors import DLPakNoDataError
from dynalab_core.protocols.packets.handshake import SignalDescriptor
class RecordManifest(BaseModel):
timestamp: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
signals: list[SignalDescriptor] = Field(default_factory=list)
comments: str = ""
class DLPak:
def __init__(self) -> None:
self._dir: Path | None = None
self._filename: str | None = None
self._savetype: Literal["zip", "folder"] = "zip"
self._data: ValueBuffer | None = None
self._manifest: RecordManifest | None = None
def set_data(self, data: ValueBuffer) -> None:
self._data = data
def set_manifest(
self, timestamp: datetime, all_signals: list[SignalDescriptor]
) -> None:
if self._data is None:
raise DLPakNoDataError
data_signal_ids = self._data.get_signal_ids()
manifest_signals: list[SignalDescriptor] = []
for signal in all_signals:
if signal.id in data_signal_ids:
manifest_signals.append(signal)
self._manifest = RecordManifest(
timestamp=timestamp, signals=manifest_signals, comments=""
)
def write(self, dir: str | Path, filename: str) -> None:
output_dir = Path(dir)
if output_dir.is_file():
output_dir = output_dir.parent
file_path = Path(filename)
if file_path.suffix.lower() != ".dlpak":
file_path = file_path.with_suffix(".dlpak")
output_path = output_dir / file_path
with ZipFile(output_path, mode="w", compression=ZIP_DEFLATED) as archive:
archive.writestr("manifest.json", self._manifest.model_dump_json(indent=2))
archive.writestr("data.csv", self._data.export_csv())
+9
View File
@@ -9,3 +9,12 @@ class CoreError(Exception):
class CoreStateMismatchError(CoreError):
"""DynaLab Core state error."""
# DLPak error declarations
class DLPakError(Exception):
"""DLPak error."""
class DLPakNoDataError(DLPakError):
"""DLPak no data error."""