Skip to content

Node federation service, CS API MQTT Part 3 and proto/flatbuffers formats - #100

Closed
tipatterson-dev wants to merge 57 commits into
masterfrom
fed-service-pr-cleanup
Closed

tipatterson-dev wants to merge 57 commits into
masterfrom
fed-service-pr-cleanup

Conversation

@tipatterson-dev

Copy link
Copy Markdown

Brings in the node federation service and the CS API pub/sub pieces it relies on. Cleaned-up version of fed-service, brought current with master.

Depends on Botts-Innovative-Research/osh-core#45 (custom obs/command formats). The proto and flatbuffers modules don't compile without it.

Changes

  • sensorhub-service-federation (new): mirrors systems, datastreams and control streams from remote nodes into a commander node over the CS API + MQTT, and routes commands from the commander to the remotes. Includes the loop guard (no self-federation, no re-forwarding already-forwarded observations).
  • sensorhub-service-consys-mqtt: CS API Part 3 draft updates. Adds CloudEvents resource events, wildcard subscriptions with a topic validator, and :data/<format> topics. The format token is passed to the API as f=. Also drops an unused dependency on the video service, and an orphaned test for batching code that was never committed.
  • sensorhub-service-mqtt-hivemq: wildcard subscriptions enabled in the default broker config (needed by the above).
  • sensorhub-service-consys-proto / -flatbuffers (new): application/swe+proto and application/swe+flatbuffers encodings for observations, commands and their schemas, through core's CustomObsFormat / CustomCommandFormat.

The NATS binding (sensorhub-service-consys-nats) is split out of this PR to be tested separately.

Testing

  • Module tests pass: consys-mqtt, consys-proto, consys-flatbuffers, federation. Whole repo compiles against osh-core#45.
  • Live, two local nodes with osh-core#45: observations mirrored to the commander in real time; MQTT topics in every format, including swe-proto; commands sent to the commander over HTTP and MQTT ran on the remote counter sim driver.
  • Known gap, not a regression: command status isn't relayed back to the commander (Federation: command status is not relayed back to the commander #99).

CI note

CI builds against the published osh-core package, so the proto/flatbuffers modules will fail there until a core build with osh-core#45 is published.

tipatterson-dev and others added 30 commits July 17, 2026 00:18
- add cloud event types
- add a resource event publisher
  - Replace permission checks in ConSysApiMqttConnector with validator
  - Add unit tests for ConSysTopicValidator
- fix a parsing bug with # wildcards
- simplify test readability for validator
Recognize an optional format token appended to a Resource Data Topic
(e.g. .../observations:data/swe-json) per the OGC CS API Part 3 content
negotiation rules, and apply it as the request/response format.
ConSysTopicValidator gains DATA_SUFFIX, FORMAT_SUBTOPICS (json, swe-json,
swe-binary, swe-csv, swe-proto, om-json, sml-json), isDataTopic,
parseDataTopicFormat and stripDataSuffix; ConSysApiMqttConnector consumes
them for both publish (request content type) and subscribe (response format),
replacing the bare :data suffix handling.
Add the sensorhub-service-consys-proto module skeleton: the Connected
Systems and SWE Common .proto definitions (including the swe_options field
annotations carrying SWE metadata), the Gradle build wiring the protobuf
plugin, and the IModuleProvider service registration. No Java yet —
the codec implementation follows in the next commit.
Implement the swe+proto codec end to end. Outbound: SWE→proto schema
generation (ProtoSchemaWriter, GeneratedSchemaCache, DataStreamSchemaCache)
and observation/command encoding (ProtoObsEncoder, ProtoFormat bindings).
Inbound: observation/command decode for ingestion (ProtoRecordDecoder,
ObsBindingProto, CommandBindingProto) and client schema ingestion that
rebuilds a SWE Common record from a wire FileDescriptorSet
(ProtoSchemaReader, DataStream/CommandStream schema bindings). Includes the
module service classes (Activator, ConSysApiProtoService, config, descriptor)
and the test suite.
…print cache

GeneratedSchemaCache now rebuilds the FileDescriptor/FileDescriptorSet on every
get() instead of memoizing per stream behind a structural fingerprint. The
hashing (fingerprint()/mix(), the entries map, Entry.fingerprint) is removed;
the class name and get(BigId, DataComponent) signature are kept (streamId
unused) so the cache can be reinstated without touching ProtoFormat or the
bindings.

The prior fingerprint-cache implementation is preserved on branch
parked/schema-fingerprint-cache.

- TestGeneratedSchemaCache: assert per-call rebuild + deterministic output
  instead of build-count memoization.
- ObsBindingProto / CommandBindingProto / ProtoFormat: refresh now-stale
  "memoized/fingerprinted" comments.
Move bindings/codecs/schema machinery out of the flat proto package into
subpackages by concern: schema/ (writer, reader, both caches), codec/
(encoder, decoder), and datastreams/ observations/ controlstreams/ commands/
for the resource bindings. Service/format/infra classes (ProtoFormat,
ConSysApiProtoService, *Config, *Descriptor, Activator) stay at the root so
the deployment-config FQN, the IModuleProvider SPI entry, and the OSGi
Bundle-Activator keep resolving.

Add the now-required cross-package imports and make the four binding name
constants (OBS_PACKAGE/OBS_MESSAGE, CMD_PACKAGE/CMD_MESSAGE) public so
ProtoFormat reads them across the boundary. Tests mirror the layout.
No behavior change; full module suite green.
Increment 1 of DataArray support. Fixed-size arrays of scalars and of
records/vectors now round-trip on the same-node encode->decode path:

- ProtoObsEncoder: DataArray -> repeated field; K = getComponentCount()
  elements walked from the flat DataBlock (contiguous for fixed arrays).
- ProtoRecordDecoder: repeated field -> flat block, with a wire-length vs
  schema-size check.
- ProtoArrays.hasNonFlatLayout: shared guard so a fixed array hiding a
  DataChoice or variable-size sub-array (DataBlockList-backed, not flat-
  addressable) throws loudly in both stages instead of mis-reading atoms.
- Extracted scalarValue()/setAtom() so the array path reuses the scalar
  type switch (no fifth copy).

Not yet: variable-size arrays, Matrix, foreign ingest (ProtoSchemaReader
still rejects repeated fields) - follow-up increments.

Tests: TestProtoArrayRoundTrip. Full module suite green (37).
Increment 2. Variable-size arrays now round-trip; the element count travels
as the repeated field's wire length (no separate size field needed).

- ProtoObsEncoder.encode/encodeCommand: bind struct.setData(data) so a
  variable array's getComponentCount() reflects the observation's data; drop
  the variable-size rejection.
- ProtoRecordDecoder: the choice pre-pass becomes prepass(), which also
  updateSize()s each variable array from msg.getRepeatedFieldCount() before
  createDataBlock() so the flat block is allocated correctly; drop the
  variable-size rejection.
- Non-flat (DataBlockList) elements — a fixed array hiding a DataChoice or a
  variable-size sub-array — still throw via ProtoArrays.hasNonFlatLayout.

Tests: variable-size round-trip (trailing scalar catches drift) + a direct
unit test of the non-flat guard. Full module suite green.
Increment 3. A DataArray whose element is itself a DataArray (a Matrix row;
proto has no repeated-of-repeated) now round-trips via a generated wrapper
message holding the inner array as its single repeated field.

- ProtoSchemaWriter: route a DataArray element to the nested-message path;
  buildMessage already wraps a non-structured component as a one-field
  message, so the wrapper falls out for free (recurses for N-D).
- ProtoObsEncoder: treat a DataArray element as nested (encodeMessage wraps
  the inner array as the wrapper's field 1).
- ProtoRecordDecoder: new branch decodes each wrapper's field-1 inner array.

Fixed-size matrices only (their flat DataBlock is contiguous); variable-size
inner arrays remain non-flat and are rejected by ProtoArrays.hasNonFlatLayout.

Test: 2x3 array-of-array round-trip with a trailing scalar. Suite green.
Increment 4. ProtoSchemaReader (the foreign-schema ingest path) no longer
rejects repeated fields — it rebuilds a variable-size DataArray:

- buildArray(): element from the field's scalar/record type, or — for a
  Matrix-row wrapper message (one repeated field) — a nested DataArray
  (recursive, so N-D matrices ingest too).
- The array is implicit variable-size (an elementCount carrying a Count whose
  value is unset → isVariableSize()), so ProtoRecordDecoder.prepass() sizes it
  from the repeated field's wire length. A foreign descriptor carries no
  count, so ingested arrays are necessarily variable-size.

Test: encode with the original struct, decode with the reader-rebuilt one
(the foreign-ingest property). Suite green.
Increment 5. ProtoFormat.isCompatible no longer returns true unconditionally
(its standing TODO): it returns canEncode(recordStructure), so a datastream
whose structure the codec can't handle won't list swe+proto in its formats
and then 500 on encode.

canEncode rejects:
- structures with a non-flat array (a DataArray whose element hides a
  DataChoice or variable-size sub-array — DataBlockList-backed; the writer
  emits these but the flat-index codec can't walk them), via
  ProtoArrays.hasNonFlatLayout (made public);
- anything the schema build throws on (Geometry, unmapped scalar types,
  invalid descriptor) — caught by attempting write()+resolve().

Test: TestProtoFormat. Suite green.
…rows)

Proves a capability the increment-3 message understated ("fixed matrices
only"). The codec's only constraint is that every array ELEMENT has a fixed
atom count, so a variable-size array is fine as the OUTER matrix dimension:
array[var n] of (array[3] double) — a variable count of fixed-width rows —
round-trips on a flat block. Only a variable INNER array (ragged rows) or an
array-of-choice is DataBlockList-backed and rejected.
Increment 6. A nested DataArray with BOTH dimensions variable (M x N, sized by
count components, rectangular / "square per message") now round-trips — M and N
may differ per message. Verified empirically that SWE lays these out as a flat
DataBlockMixed (NOT a DataBlockList), so the flat-index codec handles them; the
earlier guard rejected them on a wrong premise.

- ProtoArrays: hasNonFlatLayout -> elementHasChoice. Rectangular arrays (fixed
  or variable, nested) stay flat and are no longer flagged; only a DataChoice
  in an array element is still rejected (the pre-pass doesn't apply per-element
  selections inside an array yet).
- ProtoRecordDecoder.prepass: size the (shared) inner array dimension from row
  0's wrapper message, so createDataBlock allocates the full M x N flat block.
  A ragged row then mismatches the per-row wireCount check and throws loudly.
- ProtoFormat: hasNonFlatArray -> hasArrayWithChoiceElement.

Encoder already handled it via setData + the nested-array path. Tests: M,N
varying per message (2x3, 3x2, 1x5) on one schema; updated guard unit test.
Adoption fell through to a name-only match whenever either schema
signature was null, and ran before the schema-null guard, so a stream
whose schema fetch failed could bind onto an unrelated commander
datastream. Match on outputName/inputName (what the CS API actually keys
streams on) and require both signatures present and equal.
The dedup lookup listed the commander system's datastreams (one request
plus a schema fetch each) on every iteration of the mirror loop, making
startup quadratic in stream count. Build the outputName/inputName index
once per commander system and reuse it.
HashMap accepts a null key, so every system without a UID collapsed onto
one entry and all their streams were mirrored into a single commander
system. Skip them on both the index and lookup sides.
"procedureLink@link"/"deploymentLink@link" match no wire property, so the
remote procedure and deployment links were POSTed to the commander
unstripped. Use procedure@link/deployment@link, also clear system@link,
and drop the "id":"default" placeholder in favour of removing the key.
tipatterson-dev and others added 19 commits August 12, 2026 15:46
…collisions

allow setting root topic, minor bug fixes
…lizers

The module had no tests at all. Wire JUnit in the same shape as
consys-nats and cover APIHelper's root normalization, which until now was
only checked by a throwaway class compiled against the build output.
…rvations

Identifies an observation fleet-wide by (systemUid, outputName,
phenomenonTime) so an echo is recognized regardless of which node's
datastream id it comes back under. Reads the time from CS API JSON, and
from the leading 8-byte double of a SWE binary frame so the video relay
can be deduped too. Fails open throughout. Not yet wired in.
The observation pump holds only a Datastream, but identifying an
observation across nodes needs the system UID — datastream ids are
per-node. Set it wherever the owning system is known.
Listing the commander among the nodes is legal and useful — a commander
carrying its own sensors then appears in the federated set like any other
node. Relaying its data back is not: the observation is already stored
where it would be sent, so the copy is republished and returns.

Mirror such nodes as before, but skip their observation subscriptions,
their pumps, and their command forwarding. Node identity compares the
resolved address so the same host spelled two ways still matches.
Consult ObsEcho on both the JSON and binary relay paths, so a
re-delivered observation is relayed at most once — covering re-mirroring
after a restart, an MQTT reconnect replaying an overlap, and a cycle of
brokers relaying to each other. Fingerprints are marked before the insert
and rolled back if it fails, so a dropped insert stays retryable.
# Conflicts:
#	services/sensorhub-service-federation/src/main/java/org/sensorhub/impl/service/federation/MirroringMixin.java
# Conflicts:
#	services/sensorhub-service-federation/src/main/java/org/sensorhub/impl/service/federation/BrokerContext.java
#	services/sensorhub-service-federation/src/main/java/org/sensorhub/impl/service/federation/OSHDataBroker.java
@tipatterson-dev
tipatterson-dev marked this pull request as ready for review October 13, 2026 20:02
@tipatterson-dev

Copy link
Copy Markdown
Author

I tested with fakeweather, ffmpeg, and the counter sim for commands. It all worked fine for me.

@rmw-workbotts

Copy link
Copy Markdown

added backfill on mirror to query remote history. Fixed dropped obs for post on startup sensors.

@rmw-workbotts rmw-workbotts left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Commits are stable for federation

@drewbotts

Copy link
Copy Markdown

This should be going to OpenSensorHub - Master rather than Botts Innovative Research - Master, correct?

@tipatterson-dev

Copy link
Copy Markdown
Author

Closing to direct merge into osh's addons repo

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants