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.