fieldmesh

Provenance of the code in sdk/#

Everything here originates in campus_twin (~/dev/campus_twin), the sibling buildings/BAS platform by the same author. It arrives by copy and relicense, not by import: fieldmesh owns these files now and is free to diverge, which is what the constellation rule asks for — no repo stops building because a sibling moved.

The license arrangement#

campus_twin is GPL-3.0 as a whole. The transport spine was dual-licensed in its original home before anything was copied, each file carrying:

# SPDX-License-Identifier: Apache-2.0 OR GPL-3.0-or-later

fieldmesh exercises the Apache-2.0 option, so files here carry SPDX-License-Identifier: Apache-2.0 alone.

Relicensing at the source rather than at the destination is deliberate. Stamping a GPL-3.0 copy as Apache-2.0 on arrival would have worked, but it builds a one-way valve: the permissive copy could flow back to campus_twin, while later GPL-only improvements there could never come forward. Dual-licensing upstream keeps both directions open permanently. Full reasoning in campus_twin/LICENSING.md.

The author holds sole copyright on every campus_twin commit and there is no CLA, so no third party's agreement was needed.

The repository's licensing was decided on 2026-10-07 (open-questions.md Q2, LICENSING.md): sdk/ is Apache-2.0, so files ported here take the Apache-2.0 option of their upstream dual licence and say so in their SPDX line.

What moved#

Here From Change
spool.py open-bas/connectors/_ingest.py Adapted. Ingest path and token environment variable parameterised (TWIN_TOKEN → FIELDMESH_TOKEN, /api/ingest now a default rather than a constant); docstring retargeted at spec §5.2 and §5.4. Logic unchanged.
modbus_rtu.py open-bas/connectors/modbus_rtu.py Verbatim logic — CRC-16/MODBUS, frame building, reply parsing, RtuSource, RtuResponder. Docstring retargeted: the equipment examples are farm equipment, and the read-only rationale is reversed in meaning (see below).
test_spool.py — New. Written here; no counterpart upstream.
csv_import.py open-bas/connectors/csv_replay.py Retargeted, and two defaults reversed. Writes Observations on declared raw datastreams instead of POSTing campus-twin/v1 batches. Upstream read every timestamp as UTC; here a timezone must be declared, because a spreadsheet's 14:00 is local and the UTC assumption shifts every reading invisibly — and the autumn hour that occurs twice and the spring hour that never occurs are reported, not guessed. Upstream applied a scale and offset on import; here that is refused, because it would store a value the file never contained with no record of the change (§3.2). Adds required uncertainty_s (§2), idempotent re-import, and per-row provenance with the file's SHA-256.
mqtt_bridge.py open-bas/connectors/mqtt_bridge.py Profile parsers kept; everything they feed rewritten. Bindings point at raw datastreams and remain the allowlist, with unbound devices reported by id. Scale, offset and invert removed for the same §3.2 reason; ON/OFF becomes a boolean, not 1.0. Fixes an upstream bug: Tasmota's Time has no offset, and upstream's fromisoformat(...).timestamp() read it in the host machine's zone, so one message got different timestamps on different machines — here a naive stamp needs the binding's timezone or is stamped on arrival and counted. Link health (RSSI, SNR, battery, LQI) is bindable like any point (§4.3) rather than auto-emitted. The ESPresense profile is removed: it exists to see people's phones. No paho-mqtt: the live path is mosquitto_sub -F '%j' on stdin, so no MQTT library is needed.
mqtt_emulator.py open-bas/connectors/mqtt_emulator.py Same shape; devices moved from a plant room to a farm (hoop-house ESPHome node, Tasmota pump plug, Shelly well-pump relay, Zigbee tank-leak and soil sensors, a Home Assistant rain gauge, a ChirpStack field probe using a common LoRaWAN soil probe's published decoder field names). Presence-tracking messages removed.
test_csv_import.py, test_mqtt_bridge.py — New. The upstream tests were never relicensed and assert campus-twin/v1 shapes, so these were written here.

The one semantic change worth knowing about#

modbus_rtu.py has no write function codes, in both repos, for opposite reasons. Upstream it is permanent: "the twin reads equipment, it never commands it." Here it is temporary: layer zero does command pumps and valves (§6), but the first release ships observations only, and writes arrive later underneath the command contract — desired state rather than toggle, expiry, bounded idempotency, node-side limits. A bare Modbus write with none of that is the unsafe path §6 exists to prevent, so the absence is load-bearing until §6 is implemented.

This file will diverge rather than track. That is expected.

What has not moved, and what it is waiting on#

These are wanted, and in several cases nearly ready, but each depends on a decision or an artifact that does not exist yet. Copying them now would mean rewriting them.

Upstream file Wanted for Blocked on
mesh_emulator.py Virtual nodes for the simulated farm Superseded rather than blocked. Q5 was decided (CBOR packed arrays), and sim/node.py with sim/link.py already emit that wire format from simulated nodes, which is what this would have done. Its profile table was buildings-domain throughout.
meshtastic_bridge.py The gateway's first link driver Q6, which link ships first, and real hardware (build step 7). Q5, the registry (§1) and batching (§5.2) — the old blockers — all exist now
modbus_collector.py, modbus_emulator.py The wired driver and its emulator The driver manifest (§9.1)
test_metric_registry.py The gate that fails a producer emitting an unregistered name — the enforcement §3.4 needs Not ported; reimplemented natively as vocab.check, which State.validate runs on every datastream. Upstream's gate was a test over metric names; this one checks the term is registered and the unit is one of that term's, offline, with vocab.py --resolve re-fetching the registry when a connection exists

What remains upstream is waiting on Q6 and hardware for the Meshtastic driver, the driver manifest (§9.1) for the Modbus collector pair, and nothing at all for the vocabulary gate, which is simply unported.

Dependencies#

Everything in sdk/ is the standard library except wire.py, which needs cbor2 (MIT) for the CBOR wire format decided in Q5. Earlier versions of this file said stdlib only; the first CI run, on a machine without the Ubuntu package that had been hiding it, showed otherwise. The two files ported first were stdlib-only, which is why they moved first. Upstream's optional extras (paho-mqtt for the live MQTT path, bacpypes3 for BACnet) are not required by anything in sdk/.

No file here depends on the meshtastic PyPI package, which is GPL-3.0 and would have been an obstacle to an Apache-2.0 SDK regardless of who wrote the calling code.