Skip to content

Commit 882a6cf

Browse files
committed
refactor(python): decouple measurement typing from proto/string adapters
1 parent 16d3909 commit 882a6cf

File tree

19 files changed

+770
-0
lines changed

19 files changed

+770
-0
lines changed

lang/python/samples/CMakeLists.txt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,12 @@ set(python_samples_measurement
22
measurement/low_level_rw.py
33
measurement/low_level_rw_protobuf.py
44
measurement/measurement_read.py
5+
measurement/binary_write.py
6+
measurement/binary_read.py
7+
measurement/person_write.py
8+
measurement/person_read.py
9+
measurement/string_write.py
10+
measurement/string_read.py
511
)
612

713
set(python_samples_core
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# Measurement Samples (serializer-based)
2+
3+
## Binary
4+
5+
```bash
6+
python binary_write.py
7+
python binary_read.py
8+
```
9+
10+
## Protobuf
11+
12+
```bash
13+
python person_write.py
14+
python person_read.py
15+
```
16+
17+
## String
18+
19+
```bash
20+
python string_write.py
21+
python string_read.py
22+
```
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# ========================= eCAL LICENSE =================================
2+
# Copyright (C) 2016 - 2025 Continental Corporation
3+
# Licensed under the Apache License, Version 2.0
4+
# ========================= eCAL LICENSE =================================
5+
6+
import argparse
7+
8+
from ecal.measurement2.measurement import MeasurementReader
9+
10+
11+
def main() -> None:
12+
parser = argparse.ArgumentParser(description="Read binary entries from an eCAL measurement")
13+
parser.add_argument("--input", default="binary_measurement", help="Measurement path")
14+
args = parser.parse_args()
15+
16+
with MeasurementReader(args.input) as reader:
17+
channel = reader.get_channel("blob")
18+
print(f"channel metadata: {channel.metadata}")
19+
for entry in channel:
20+
print(
21+
f"rcv={entry.rcv_timestamp} snd={entry.snd_timestamp} size={len(entry.msg)} "
22+
f"payload={entry.msg.hex()}"
23+
)
24+
25+
26+
if __name__ == "__main__":
27+
main()
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# ========================= eCAL LICENSE =================================
2+
# Copyright (C) 2016 - 2025 Continental Corporation
3+
# Licensed under the Apache License, Version 2.0
4+
# ========================= eCAL LICENSE =================================
5+
6+
import argparse
7+
8+
from ecal.measurement2.measurement import DataTypeInformation, MeasurementWriter
9+
10+
11+
def main() -> None:
12+
parser = argparse.ArgumentParser(description="Write binary entries into an eCAL measurement")
13+
parser.add_argument("--output-dir", default="binary_measurement", help="Measurement output directory")
14+
parser.add_argument("--file-name", default="measurement", help="Measurement base file name")
15+
args = parser.parse_args()
16+
17+
with MeasurementWriter(args.output_dir, args.file_name, 500) as writer:
18+
blob_type = DataTypeInformation(name="blob.v1", encoding="binary")
19+
channel = writer.create_channel(channel_name="blob", data_type=blob_type)
20+
for index in range(5):
21+
payload = bytes([index, index + 1, index + 2, 255 - index])
22+
timestamp = 1_000_000 + index * 100_000
23+
channel.write_entry(payload, rcv_timestamp=timestamp, snd_timestamp=timestamp)
24+
print(f"wrote index={index} payload={payload.hex()}")
25+
26+
27+
if __name__ == "__main__":
28+
main()
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# ========================= eCAL LICENSE =================================
2+
# Copyright (C) 2016 - 2025 Continental Corporation
3+
# Licensed under the Apache License, Version 2.0
4+
# ========================= eCAL LICENSE =================================
5+
6+
import argparse
7+
import os
8+
import sys
9+
10+
import ecal.msg.proto.measurement as proto_measurement
11+
from ecal.msg.common.measurement import MeasurementReader
12+
13+
sys.path.insert(1, os.path.join(sys.path[0], '../core/pubsub/_protobuf'))
14+
import person_pb2
15+
16+
17+
def main() -> None:
18+
parser = argparse.ArgumentParser(description="Read protobuf person messages from an eCAL measurement")
19+
parser.add_argument("--input", default="person_measurement", help="Measurement path")
20+
args = parser.parse_args()
21+
22+
with MeasurementReader(args.input) as reader:
23+
channel = proto_measurement.open_channel(reader, "person", person_pb2.Person)
24+
print(f"channel metadata: {channel.metadata}")
25+
for entry in channel:
26+
print(
27+
f"rcv={entry.receive_timestamp} snd={entry.send_timestamp} "
28+
f"person_id={entry.message.id} name={entry.message.name}"
29+
)
30+
31+
32+
if __name__ == "__main__":
33+
main()
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# ========================= eCAL LICENSE =================================
2+
# Copyright (C) 2016 - 2025 Continental Corporation
3+
# Licensed under the Apache License, Version 2.0
4+
# ========================= eCAL LICENSE =================================
5+
6+
import argparse
7+
import os
8+
import sys
9+
10+
import ecal.msg.proto.measurement as proto_measurement
11+
from ecal.msg.common.measurement import MeasurementWriter
12+
13+
sys.path.insert(1, os.path.join(sys.path[0], '../core/pubsub/_protobuf'))
14+
import person_pb2
15+
16+
17+
def main() -> None:
18+
parser = argparse.ArgumentParser(description="Write protobuf person messages into an eCAL measurement")
19+
parser.add_argument("--output-dir", default="person_measurement", help="Measurement output directory")
20+
parser.add_argument("--file-name", default="measurement", help="Measurement base file name")
21+
args = parser.parse_args()
22+
23+
with MeasurementWriter(args.output_dir, args.file_name, 500) as writer:
24+
channel = proto_measurement.create_channel(writer, "person", person_pb2.Person)
25+
26+
for index, name in enumerate(["Alice", "Bob", "Charlie"]):
27+
person = person_pb2.Person()
28+
person.id = index
29+
person.name = name
30+
person.email = f"{name.lower()}@example.com"
31+
person.stype = person_pb2.Person.FEMALE if index % 2 == 0 else person_pb2.Person.MALE
32+
timestamp = 1_000_000 + index * 100_000
33+
channel.write(person, rcv_timestamp=timestamp, snd_timestamp=timestamp)
34+
print(f"wrote person id={person.id} name={person.name}")
35+
36+
37+
if __name__ == "__main__":
38+
main()
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# ========================= eCAL LICENSE =================================
2+
# Copyright (C) 2016 - 2025 Continental Corporation
3+
# Licensed under the Apache License, Version 2.0
4+
# ========================= eCAL LICENSE =================================
5+
6+
import argparse
7+
8+
import ecal.msg.string.measurement as string_measurement
9+
from ecal.msg.common.measurement import MeasurementReader
10+
11+
12+
def main() -> None:
13+
parser = argparse.ArgumentParser(description="Read string entries from an eCAL measurement")
14+
parser.add_argument("--input", default="string_measurement", help="Measurement path")
15+
args = parser.parse_args()
16+
17+
with MeasurementReader(args.input) as reader:
18+
channel = string_measurement.open_channel(reader, "hello")
19+
print(f"channel metadata: {channel.metadata}")
20+
for entry in channel:
21+
print(f"rcv={entry.receive_timestamp} snd={entry.send_timestamp} text={entry.message}")
22+
23+
24+
if __name__ == "__main__":
25+
main()
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# ========================= eCAL LICENSE =================================
2+
# Copyright (C) 2016 - 2025 Continental Corporation
3+
# Licensed under the Apache License, Version 2.0
4+
# ========================= eCAL LICENSE =================================
5+
6+
import argparse
7+
8+
import ecal.msg.string.measurement as string_measurement
9+
from ecal.msg.common.measurement import MeasurementWriter
10+
11+
12+
def main() -> None:
13+
parser = argparse.ArgumentParser(description="Write string entries into an eCAL measurement")
14+
parser.add_argument("--output-dir", default="string_measurement", help="Measurement output directory")
15+
parser.add_argument("--file-name", default="measurement", help="Measurement base file name")
16+
args = parser.parse_args()
17+
18+
with MeasurementWriter(args.output_dir, args.file_name, 500) as writer:
19+
channel = string_measurement.create_channel(writer, "hello")
20+
for index, text in enumerate(["hello", "from", "ecal", "measurement"]):
21+
timestamp = 1_000_000 + index * 100_000
22+
channel.write(text, rcv_timestamp=timestamp, snd_timestamp=timestamp)
23+
print(f"wrote text={text}")
24+
25+
26+
if __name__ == "__main__":
27+
main()

lang/python/src/ecalhdf5/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ set(python_files
3030
ecal/measurement/measurement.py
3131
ecal/measurement/writer.py
3232
ecal/measurement/__init__.py
33+
ecal/measurement2/measurement.py
34+
ecal/measurement2/__init__.py
3335
)
3436

3537
target_sources(${PROJECT_NAME}
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
# Measurement API Reimplementation Plan (Python)
2+
3+
## Goals
4+
5+
1. Reimplement the Python measurement interface on top of the stable low-level `hdf5.py` wrapper (without changing `hdf5.py`).
6+
2. Support both **reading** and **writing** measurements with serializer-driven behavior.
7+
3. Use the serializer abstractions from `ecal.msg` (`common`, `proto`, `string`) instead of hardcoded protobuf-only behavior.
8+
4. Add clear samples for:
9+
- binary measurement read/write,
10+
- protobuf measurement read/write,
11+
- string measurement read/write,
12+
following the naming and structure style used by existing Python communication samples.
13+
14+
---
15+
16+
## Phase 1: Baseline and API contract definition
17+
18+
1. Inventory current measurement APIs:
19+
- `measurement.py` (reader abstractions),
20+
- `writer.py` (writer abstractions),
21+
- current measurement samples.
22+
2. Define a target public API that unifies reading and writing:
23+
- explicit `MeasurementReader` and `MeasurementWriter` entry points,
24+
- channel-level typed access based on serializers,
25+
- raw binary access for all channels.
26+
3. Preserve compatibility where practical:
27+
- keep legacy class/function names as thin wrappers (deprecated) if needed,
28+
- avoid changing data layout or low-level HDF5 behavior.
29+
30+
Deliverable: short API sketch in docstrings / module comments before implementation.
31+
32+
---
33+
34+
## Phase 2: Serializer-driven type model for measurement
35+
36+
1. Introduce a measurement-local datatype info object compatible with `ecal.msg.common.serializer.DataTypeInfo` protocol:
37+
- fields: `name`, `encoding`, `descriptor`.
38+
2. Implement robust conversion helpers:
39+
- parse channel type from HDF5 (`"encoding:type"`, fallback to empty encoding),
40+
- compose channel type string when writing,
41+
- map HDF5 channel metadata <-> serializer datatype info.
42+
3. Provide a reusable serializer selection strategy:
43+
- caller passes a serializer instance explicitly for typed channels,
44+
- optional convenience constructors for protobuf / string.
45+
46+
Deliverable: internal helpers that isolate metadata parsing and serializer matching logic.
47+
48+
---
49+
50+
## Phase 3: Reading reimplementation
51+
52+
1. Build a new binary-first reader core on top of `hdf5.Meas`:
53+
- channel metadata object,
54+
- channel entry object with `rcv_timestamp`, `snd_timestamp`, `payload`.
55+
2. Add typed read adapters:
56+
- `read_binary(...)` returns raw bytes,
57+
- `read_typed(serializer=...)` uses `accepts_data_with_type(...)` + `deserialize(...)`.
58+
3. Support protobuf and string through msg serializers:
59+
- protobuf: `ecal.msg.proto.serializer.Serializer` / `DynamicSerializer`,
60+
- string: `ecal.msg.string.serializer.Serializer`.
61+
4. Define explicit behavior for type mismatches and deserialization failures:
62+
- raise informative exceptions with channel + datatype context,
63+
- optionally provide iteration mode that reports and skips malformed entries.
64+
5. Provide dynamic-channel support:
65+
- expose a `DynamicChannelReader` path that selects deserialization based on channel metadata,
66+
- for protobuf channels, use `ecal.msg.proto.serializer.DynamicSerializer` to return dynamically typed protobuf objects.
67+
68+
Deliverable: measurement reading path that no longer hardcodes protobuf logic in `measurement.py`.
69+
70+
---
71+
72+
## Phase 4: Writing reimplementation
73+
74+
1. Build writer channels that always store metadata from serializer datatype info:
75+
- `set_channel_type(encoding:type)` and `set_channel_description(descriptor)`.
76+
2. Implement generic typed writer:
77+
- accepts serializer + message object,
78+
- serializes to bytes and writes timestamps.
79+
3. Implement explicit binary writer:
80+
- accepts bytes payload directly,
81+
- optional metadata override (`encoding`, `type`, `descriptor`) for non-empty typed binary streams.
82+
4. Add convenience factories:
83+
- protobuf writer channel from protobuf type,
84+
- string writer channel,
85+
- raw binary writer channel.
86+
5. Ensure writer can create all three required sample formats (binary, protobuf, string).
87+
88+
Deliverable: `writer.py` no longer protobuf-only in `create_channel`; supports binary and string cleanly.
89+
90+
---
91+
92+
## Phase 5: Backward compatibility and migration
93+
94+
1. Keep old symbols where feasible and redirect to new internals.
95+
2. Mark legacy-only APIs as deprecated in docstrings.
96+
3. Document migration examples:
97+
- old protobuf writer usage -> new serializer-based usage.
98+
99+
Deliverable: existing users are not broken abruptly while new APIs are available.
100+
101+
---
102+
103+
## Phase 6: Samples (new high-level measurement examples)
104+
105+
Create six samples under `lang/python/samples/measurement/` using consistent naming:
106+
107+
1. `binary_write.py`
108+
2. `binary_read.py`
109+
3. `person_write.py` (protobuf)
110+
4. `person_read.py` (protobuf)
111+
5. `string_write.py`
112+
6. `string_read.py`
113+
114+
Sample conventions:
115+
116+
- mirror style of `samples/core/pubsub/*_send.py` and `*_receive.py` naming,
117+
- minimal dependencies and predictable output,
118+
- configurable path to output measurement directory,
119+
- comments that explain datatype metadata and serializer usage.
120+
121+
Also add/update sample README with run order and expected output.
122+
123+
Deliverable: discoverable, task-oriented examples for all requested formats.
124+
125+
---
126+
127+
## Phase 7: Test plan
128+
129+
1. Add or extend Python tests for measurement API:
130+
- roundtrip binary,
131+
- roundtrip protobuf,
132+
- roundtrip string,
133+
- serializer mismatch behavior,
134+
- metadata preservation (`encoding`, `name/type`, `descriptor`).
135+
2. Keep tests deterministic (fixed timestamps / payloads).
136+
3. Run targeted test subset and broader suite as available.
137+
138+
Deliverable: automated confidence for read/write and serializer integration.
139+
140+
---
141+
142+
## Phase 8: Rollout checklist
143+
144+
1. Verify imports and package paths for both `ecalhdf5` and `msg` modules.
145+
2. Ensure no modifications to `hdf5.py`.
146+
3. Validate sample scripts execute in sequence:
147+
- write -> read for each format.
148+
4. Update changelog / release notes if required by repository practice.
149+
150+
Deliverable: complete feature with docs, samples, and tests ready for review.
151+
152+
---
153+
154+
## Suggested implementation order
155+
156+
1. Metadata helper + datatype info bridge.
157+
2. New reader binary core and typed adapter.
158+
3. New writer generic channel + binary/proto/string convenience.
159+
4. Compatibility wrappers.
160+
5. New samples + README updates.
161+
6. Automated tests and final polish.

0 commit comments

Comments
 (0)