L1 — Connect & normalise
L1 reads plant and document signals and turns them into canonical JSON envelopes that L2 can store. It is three repos: connectors-edge at the plant (protocol adapters, a SQLite buffer and an MQTT uplink), connectors-cloud for intake and quality gates, and connectors-doc for documents such as utility bills. L1 never opens TimescaleDB, and the edge agent never writes to OT registers. The only write path is the separate Plant Box writer, which is direction. Field paths are proven against lab simulators, not at a named plant.
- Status
- as-built (
feat/l1-complete, 2026-09-27) · contract (schemas, MQTT topics) · direction (Plant Box fast read,stamped-writer) - Conceptual layer
- ① Plant systems and ② Context
- Repo layer
- L1
connectors-edge,connectors-cloud,connectors-doc - Source
- architecture section 3.6.1, section 3.6.2, section 7 · ADRs 006, 001, 028 · L1-L2-DATA-PLANE.md
L1 reads plant and document signals and turns them into canonical JSON that L2 can store. It never opens Timescale. The edge agent never writes OT registers; the only write path is the separate Plant BoxPlant-side computer for the fast loop (direction; D4) stamped-writer (D10, direction). Energy and waste is one outcome; utility bills are one document family on the people door.
| Label | Meaning |
|---|---|
| as-built | Code on the three connector repos’ feat/l1-complete (not a claim about main) |
| contract | Schemas and MQTT topics in this pack |
| simulator-proven | Field path proven against lab simulators (T1 11–13, B6). Not a named plant |
Workspace proof: docs/audits/l1-layer-completeness.md · docs/plans/l1-complete/{E1_RESULTS,R1_BOOT,T1_TRIALS}.md.
Repos (three doors)#
| Repo | Job | Must not |
|---|---|---|
connectors-edge | Plant gateway: protocol adapters + context-agent → tag/pack map → SQLite buffer (prune flushed ~72h) → MQTT uplink | OT write; inbound plant HTTP as SoR; L2_DATABASE_URL |
connectors-cloud | Cloud door: MQTT/HTTP intake → schema + quality gate → Postgres outbox → HTTP relay to L2 | Plant protocol poll; L2 SQL |
connectors-doc | Document ingest PWA: photos, scans, PDFs, CSV/XLSX; utility bills one family with ₹ gate → review → explicit MQTT publish | MQTT consumer; outbox writer; L2 SQL |
connectors-doc was renamed from connectors-bill. Payload names (bill_line, …/bills, discom_bill) stay.
Data flow#
How to read it.
- Edge polls/subscribes (Modbus, MQTT/Sparkplug, OPC UA, filewatch, REST, historian Postgres/MySQL/sqlite, MTConnect, BACnet/IP read-only, DLMS read-only LN Low, fake) →
RawReading→ signed mapping → MQTT understamped/v1/{org}/{plant}/…. context-agent maps IT file/REST/SQL packs. Unmapped →unmapped_tag, never a zero. - Doc extracts
bill_lineand plant docs.recompute_bill±₹1 setsextraction.validated. Missing printed total fails. Status staysreviewuntilPOST /v1/documents/{id}/publish. - Cloud validates fail-closed against contracts, refuses
bill_lineunlessextraction.validated=true(bill_unvalidated→ DLQ), SHA-256 dedupe, wrapsStampedRecordEnvelope, writesl1_outbox, relaysPOSTL2/v1/ingest/records. Invalid → DLQ. Ingest MQTT is a persistent session (MQTT_CLIENT_ID,clean_session=False, MQTT 3.1.1). Broker persistence queues QoS 1 while ingest is down.
Build now: three-door uplink into L2. Later: Plant Box fast read and stamped-writer on site (D4, ADR-034, ADR-035).
View Mermaid source
flowchart TB
%% house-style: l1-connect-flow
subgraph sources["Plant and people"]
direction LR
plant["Plant OT and IT"]
pwa["connectors-doc PWA"]
end
subgraph edgeDoor["connectors-edge"]
direction LR
edge["edge-agent and context-agent"]
end
mqtt["Mosquitto MQTT stamped/v1"]
subgraph cloudDoor["connectors-cloud"]
direction LR
cloud["ingest and outbox"]
gate{{"schema and quality gate<br/>fail-closed"}}
l2["POST L2 /v1/ingest/records"]
end
dlq(["↩ DLQ on invalid payload"])
plant --> edge
pwa --> mqtt
edge --> mqtt
mqtt --> cloud --> gate --> l2
gate -.-> dlq
classDef govc fill:#fff4d6,stroke:#c99a2e,color:#000
classDef agentc fill:#e8f0ff,stroke:#5b7bd5,color:#000
classDef loopc fill:#eef7ee,stroke:#4f9a4f,color:#000
class gate govc
class pwa agentc
class dlq loopc
There is no POST /v1/bills. HTTP ingest is /v1/measurements, /v1/production-orders, /v1/context. bill_line over HTTP is accept(..., transport="http").
Record catalog (contract)#
| Record type | Typical source | Notes |
|---|---|---|
measurement | edge | Live + historian backfill |
event | edge health / doc arrival | unmapped_tag lives here |
production_record / production_order | edge MES-lite / doc exports | |
bill_line | doc | L1 cloud refuses unless extraction.validated=true. Schema boolean is not const: true |
Context (asset_state, batches, flow, maintenance, quality, materials, shift, rates) | edge context-agent / doc sheets | ADR-019 · MQTT …/context wrapper |
Schemas: contracts/schemas/ · topics: contracts/TOPICS.md.
Field protocols and SQL#
| Path | As-built | Bound |
|---|---|---|
| MTConnect | HTTP/XML /current + /sample; sim mtconnect/demo:2.7 | Simulator-proven. No FOCAS |
| BACnet/IP | go-bacnet ReadProperty / RPM only; UDP 47808 | Simulator-proven. No WriteProperty* |
| DLMS/COSEM | In-house GET, LN, WRAPPER, Low/LLS | Simulator-proven. Not HLS |
Historian / sql_rows | sqlite + Postgres (pgx) + MySQL; session read-only + ValidateSelect | Write statements fail at DB |
Fast loop on the Plant Box (direction)#
Nothing in this section is as-built. Design: ../fast-loop/02-fast-read-and-writer.md · topics and owners: ../fast-loop/07-interfaces-and-ownership.md.
| Piece | Repo | Job | Decision |
|---|---|---|---|
| Fast read | connectors-edge | Sub-second reads (1 s heater, 60 s heat treatment) published to the plant-only broker on stamped/v1/{org}/{plant}/fast/{line}/{asset}/{signal}; carries PLC time and edge receive time; the normal uplink path is unchanged | ADR-034 |
| Time sync | connectors-edge | NTP/PTP discipline on the Plant Box; a clock jump over 2 s flags the window and stops the writer | ADR-034 |
stamped-writer | connectors-edge (separate binary and process) | Only writer to the PLC. Writes allow-listed process setpoints within signed limits, on twin heartbeat, restores operator values on loss; result on writer/write/result | ADR-035, D10 |
sync-agent | connectors-edge | Store-and-forward of twin records (records/{type}) and fast batches to L2; outbound pull of the signed plant_box_config bundle | 07 section 4 |
| Zones and conduits | Site deployment | Plant Box sits in its own zone; the writer is the single conduit to the PLC; only outbound connections to the cloud (IEC 62443 style) | 05, D4 |
Safety asymmetry. A remote change can only switch writes off. Switching writes on for a tag or shift takes a named person at the plant. The allow-list, limits and hazard rows live in the site packVersioned, owner-reviewed plant configuration including topology, signed by the production head.
Hard rules#
| Rule | Why |
|---|---|
| Read-only OT by default; edge agent never writes | Architecture section 7 and master document section 7 (control today). The only exception (direction) is stamped-writer: separate process, allow-listed tags, signed limits, staged per tag, plant-approved, heartbeat-bound, every write recorded |
No L2_DATABASE_URL in L1 | Only L2 opens Timescale for plant truth |
| Bill ₹ gate + explicit publish | OCR must not invent charges; a person publishes |
Cloud refuses unvalidated bill_line | Bad money does not become an outbox fact |
| Schema fail-closed at cloud | Bad payload → DLQ, not silent store |
Master document section 7 (control today) also binds: quality holds, maintenance authorisation, dispatch and master data stay with the plant; no invented currency precision.
Related handoffs#
Integration playbooks under ../../handoff/connectors/. Prefer this page for architecture; handoffs may still carry older build detail.
Page history: last 5 changes
- docs(technical): rewrite layers/ and L1-L2-DATA-PLANE.md to the architecture
322bf46 - docs(layers): L1 fast read, writer and sync; L2 tables; L5 relay and budgets; L6 surfaces; data-plane topics
5127af9 - docs(decisions): renumber live ADRs 001-032 in order, mark withdrawn refs ADR-W###, repoint withdrawn links to archive, note partial supersessions
36c944e - docs(l1): layer page from trial evidence
9f80101 - docs(l1): connectors-doc across architecture and handoff
a86cfcb