Refactor package for unified data and device API - #5
Conversation
Add threaded serial port
update wait-for-events example
Add documentation to the project
|
Hi @bruno-f-cruz, Regarding what we spoke last time on the meeting, here his the explanation on how things are working right now. Everything is based on Single-message parsingSingle-message parsing occurs in 4 steps:
_UNSIGNED_CHARS = {1: "B", 2: "H", 4: "I", 8: "Q"}
def struct_char(self) -> str:
if self.is_float():
return "f"
c = _UNSIGNED_CHARS[self.type_size()]
return c.lower() if self.is_signed() else c
Bulk dump loading (harp-data)Everything also occurs in 4 steps:
def numpy_scalar_dtype(payload_type):
itemsize = int(payload_type.type_size())
if payload_type.is_float():
return np.dtype("<f4")
kind = "i" if payload_type.is_signed() else "u"
return np.dtype(f"<{kind}{itemsize}")
NotesIt is important to note, that Complex registers also go through this. The Both paths derive from the same three Sure, we have two parsing paths but the cost of maintaining this seems small since we have two small functions that base themselves on the same source. It is not like we have two parallel lookup tables or something more complex to maintain here. The big advantage is that we keep |
|
Can you point me to Step 4 for both code paths? This is the thing that I am struggling with being "low cost". I am probably missing something very obvious, but at some point you gotta have a signature: Let
I still don't see how these are 0-cost. But I may be missing a common data representation between the python native types and np.dtypes. For instance, if T is a datastruct, how do you vectorize its parsing? --- edit This is the code fork I was trying to avoid maintaining. I think your argument is that it is small enough that it won't be an issue (?). Still, you are pushing complexity into this pyharp/src/packages/harp-protocol/src/harp/protocol/_payload.py Lines 349 to 356 in d7ed4b7 So I think there are a few issues here:
Anyway, for the sake of moving this forward, I think we should just make the decision of what we are willing to support with the current team/effort and just go with it. As @glopesdev mentioned, we can hopefully always go back and remake it if necessary in a backwards compatible way with the public DSL. |
|
Hi @bruno-f-cruz, I continue to think that having However, and since it seems that there is a more pressing necessity to have it released, keep your version, but I will kindly ask you to consider in the near future to try to remove the For the devices themselves, including the generic one, I would ask you to consider adding convenience methods that would simply wrap your read and write methods, as an alternative access method. These are easily generated. For example for the Behavior, but would work similarly to any device: These methods, in my opinion, will be extremely helpful for newcomers to the library and it eases the interaction with the device, where the full API scope of the available registers is explicit. I believe that they will also assist in the documentation. Please let me know how I can further assist in the next steps. Thank you for all the time dedicated to this. It is really appreciated. |
|
@MicBoucinha Glad we're aligned on moving forward with the current approach for the release. Thanks also for all the feedback you provided. I feel we now have both a stronger implementation and benchmarks we can use to improve it going forward.
Do you have a concrete case we could look at where the numpy dependency would be problematic? It would help to bound the exact constraints by grounding it in a real example, since the tradeoffs are hard to get right without one. The risk otherwise is that we end up designing for an idealized general case, and then find the real constraints do not fit our solution anyway. As for the convenience methods, I think we should move that discussion to the generators repo over at harp-tech/generators#119, since it isn't critical to this PR, which is only about the core architecture. I would suggest the next steps as:
After that we should be able to extend the pyharp interface generation to all device packages. What do you think? |
glopesdev
left a comment
There was a problem hiding this comment.
A pass over the branch focused on documentation, metadata, and leftover infrastructure. None of it touches the core protocol or the register and payload DSL, which look solid; the comments group into a few themes.
- Organization references and attribution, now that the repo lives under harp-tech. The
fchampalimaudURLs acrosspyproject.toml,mkdocs.yml, andREADME.mdmove toharp-tech, theauthorsfields use the org identity, and the copyright notices switch to an inclusive, undatedharp-tech and Contributorswhile keeping the historical lines. - Documentation. Several
mkdocstringsreferences inapi/protocol.mdandapi/serial.mdno longer resolve against the refactored packages,harp.deviceandharp.datahave no API page yet, an example and its markdown still use the pre-renameread_dataframe, and the theme logo ispyharp-branded while the favicon already uses the standard harp logo. - Leftover infrastructure. The docs build still aggregates device docs by cloning
harp.devices, which can come out until the per-device structure is settled, and theharp-benchmarkspackage together with thescripts/benchmark and exploration files sit alongside the shippable packages, so they would be clearer in a dedicated internal location. - One item is a flag rather than a change. The distribution is now
harpwithharp-*components, while the repo, docs, and logo still brand aspyharp, so the project name probably wants a single deliberate decision rather than piecemeal edits here.
Happy to iterate on any of these.
Co-authored-by: glopesdev <g.lopes@neurogears.org>
|
@glopesdev, just wanted to let you know that MkDocs is no longer maintained (it hasn't had a commit in 9 months; I'll try to find a blog post about it). As an alternative, the team behind the Material for MkDocs theme started developing Zensical. They are trying to reuse part of the MkDocs ecosystem, so migrating from MkDocs to Zensical is not difficult. It might be something to consider. |
All six workspace packages build with the setuptools backend and take their version from setuptools-scm, each pointing at the repository root so one tag versions them in lockstep. The setuptools floor is 77, the first release supporting the PEP 639 license expression that harp and harp-protocol already declare. No version file is written, so runtime lookups go through importlib.metadata. The release job no longer sets versions in the tree, commits back to master, or force-moves the release tag. It passes the release tag to setuptools-scm through SETUPTOOLS_SCM_PRETEND_VERSION, fails if any distribution falls back to 0.0.0, and publishes through PyPI trusted publishing in a pypi environment rather than a token secret. The docs job deploys from the tag and fetches full history for git-authors attribution. The workflow is renamed to harp.yml after the distribution it builds. pyright replaces ty in the dev group and in CI, in standard mode, with three ty suppressions in harp-protocol translated to pyright and three more dropped.
Follows the default branch rename. Pushes to the default branch trigger the workflow again; without this the branch filter matches nothing.
Version packages from git tags and publish with trusted publishing
glopesdev
left a comment
There was a problem hiding this comment.
Approving. I went back through the full branch against every point raised in review, and it's all addressed.
- Organization references and attribution are consistent under harp-tech, with the additive LICENSE keeping the historical credits alongside
harp-tech and Contributors. - The device-docs aggregation is gone, the API pages for
harp.protocol,harp.serial,harp.device, andharp.dataall resolve, and the README reads cleanly for the four-package layout. - Versioning is lockstep via setuptools-scm across every package, type checking moved to pyright, and the release pipeline publishes to PyPI through trusted publishing, with the version pinned from the release tag and a guard against an unversioned build.
- CI is green across the full matrix, all three operating systems and Python 3.11 through 3.13.
Really nice outcome, and thanks for working through all of it.
Two non-blocking reminders for the first release: tag at 0.5.0 or higher, since harp-protocol and harp-serial are already published at 0.4.0 and every package now releases at the tag version; and configure PyPI trusted publishing for each project beforehand, including pending publishers for the not-yet-created harp-device and harp-data, so uv publish can authenticate.
Refactor package for unified data and device API
Objective
Establish a clean, typed Python API for the Harp protocol that:
Field,BitFlag,GroupMask) that code generators for downstream device packages can emit directly fromdevice.yml(the codegen is NOT part of this PR and likely will not belong in this repository)harp-protocol), device/transport (harp-device+harp-serial), and tabular data (harp-data)Packages
The repo is a
uvworkspace; every member lives undersrc/packages/*with a uniformsrc/-layout, and the top-level project is namedharp. There are four packages:Separation of concerns
Each package owns exactly one concern, depends only on the layers below it, and isolates its heavy third-party dependency so consumers pay only for what they import:
harp-protocolField/BitFlag/GroupMask), and the pandas-freeColumn/to_columns()tabular viewnumpyonlyharp-deviceDevicebase (framing, request/reply, reader thread,read/write), theITransportprotocol, core register definitions,REGISTER_MAPharp-protocolharp-serialSerialTransport+ theopen_serial_devicefactoryharp-device,harp-protocolpyserialharp-dataread_dataframe/to_dataframeColumnsharp-protocolpandasDependency edges:
harp-device→harp-protocolharp-serial→harp-device,harp-protocolharp-data→harp-protocolA downstream device package (e.g.
harp-device-behavior) depends only onharp-protocol; it importsharp-deviceonly if it also wants the live transport,harp-serialonly if it wants serial, andharp-dataonly if it wants DataFrames. The heavy third-party dependencies (pandas,pyserial) live only in the leaf packages that need them, soharp-protocolandharp-devicestay lightweight.Major features/architecture
1. Register DSL (
harp.protocol)RegisterBase[U]is the schema object for a Harp register. Every register class carriesaddress,payload_type, andpayload_classasClassVars (pluslengthfor array registers) and exposes three uniform class-methods:parse(value)HarpMessage(or rawbytes/bytearray/memoryview) → typedUparse_bulk(source, *, parse_timestamp=True)(data, timestamps, message_types, payload)(numpy, zero-copy strided views)format(value?, ...)Readif no value,Writeotherwise)The same register class works regardless of whether the source is a serial port reply, an in-memory buffer, or a
.binfile on disk. No subclassing or reconfiguration is required.Concrete register base-classes for every Harp scalar type are provided (
RegisterU8,RegisterU16, …,RegisterFloat) as well as array variants (RegisterU16Array, …). These use a metaclass so that one-off anonymous registers can be created inline:2. Payload DSL — anonymous (scalar / array) payloads
For scalar and array registers (no internal structure),
PayloadU8,PayloadU16, …,PayloadFloatArrayare provided.RegisterBase.parseunwraps these directly to a numpy scalar or ndarray — i.e. no.valueaccessor needed.NOTE I went with numpy types rather than native python types since these preserve bitdepth and add additional typing information. We should discuss. In general, while the type hinting may break, native python types will still get implicitly cast by numpy.
3. Payload DSL — structured payloads
StructPayload+Field/BitFlag/GroupMaskdescriptors declare the layout of a structured register payload. Under@dataclass_transformthe class gets a fully type-checked keyword-only__init__for free.FieldEach member declares its byte position with
offset=(in base-element units).offsetdefaults to0, which suits a single-member payload; a payload with several members must give each an explicitoffset=or the layout overlap-check rejects it.BitFlag+GroupMask— bit-packed payloadsDescriptor types for registers whose payload packs multiple boolean flags or enum values into a single byte (the
OperationControlpattern common across all Harp devices). The right-shift for aGroupMaskis derived automatically from the mask's trailing-zero count — there is no separateshift=argument.The descriptors are shape-polymorphic: a 0-D (single) parse returns a Python scalar; a 1-D (bulk) parse returns a numpy array. The descriptor code is identical in both cases.
4. Device & transport layer (
harp-device+harp-serial)The device side uses composition over inheritance, split into two independent axes so they multiply without a diamond:
__whoami__, device logic → theDeviceaxisITransportaxisDevice(inharp.device) owns all protocol logic — framing, the request/reply correlation, the reader thread, andread/write— and drives anITransport.ITransportis atyping.Protocol(structural — any object withopen/write/read/closequalifies; failures surface asTransportError). A device must be opened (with/.open()); opening starts the reader thread and validates__whoami__(a class attribute;0x0skips the check).harp-serialprovidesSerialTransport(conforms structurally toITransport) and anopen_serial_devicefactory that — like the builtinopen— returns an already-connected device:A device definition is a pure subclass with no transport concern — exactly what a generator emits:
Because the transport is just an
ITransport, new ones drop in with one implementation + a factory and zero changes to device classes:zeromq— talk to a device (or broker) over a ZMQ socket:open_zmq_device(MyDevice, endpoint=...).http— drive a device behind an HTTP/REST bridge.harp.devicealso exposes aREGISTER_MAP: dict[int, type[RegisterBase[Any]]]of the core registers, which a downstream (generated) package spreads into its own map:5. Data layer (
harp-data)Tabular/DataFrame concerns live in
harp-data. The payload layer (harp-protocol) exposes a pandas-free, numpy-only tabular view viato_columns(), returninglist[Column]:harp-dataturns those columns into a DataFrame — buildingpd.Categorical.from_codesfor enum columns, which keeps the integer codes and copies nothing:This keeps the heavy/optional
pandasdependency out of the protocol and device layers, and givesharp-dataroom to grow into a home for further analysis utilities.Also, this opens the door to future implementations (e.g: arrow), using the same protocol exposed interface.
6. Code-generation affordances
A downstream package for a specific device only needs to emit register/payload class declarations — the entire declaration is mechanical and driven directly by
device.yml:The generator emits one
Field/BitFlag/GroupMaskper payload member, oneRegisterBasesubclass per register, and (for a device package) aDevicesubclass with__whoami__plus a spreadREGISTER_MAP. No per-device framework extension is required; the output is pure application code that composes the DSL, and the constructor is type-hinted for free.