Python tooling for a three-gateway LoRaWAN stack — REST/CGI clients, CS4 gRPC-web client, MQTT subscribers, Milesight sensor decoders, and a TimescaleDB sink with a live dashboard.
| layer | component | gateway |
|---|---|---|
| Gateway A | Milesight UG63-868M (OpenWrt, ChirpStack v3) | |
| Gateway B | Milesight UG65-868M (LEDE, loraserver 4.0.6) | |
| Gateway C | RAK7391 WisGate Connect (RAKPiOS, ChirpStack v4) | |
| Network server | ChirpStack v3 (UG63/UG65) + CS4 native systemd (RAK7391) | — |
| Central broker | Mosquitto on propella-server — bridges pull from UG65 + RAK7391 | :1883 |
| Persistence | TimescaleDB hypertable uplinks on propella-server port 5433 |
— |
| Dashboard | FastAPI + ceduix UI served on port 8000 | http://propella:8000 |
| Decoders | Milesight-IoT/SensorDecoders (git submodule) + Node subprocess | shared |
| path | what |
|---|---|
ug63/ |
UG63 client (REST + CGI + SSH), MQTT subscriber, CLI |
ug65/ |
UG65 client (REST read-only), MQTT subscriber |
cs4/ |
RAK7391 CS4 gRPC-web client + MQTT subscriber |
cedlora/ |
Shared API (FastAPI), TimescaleDB sink, MQTT orchestration |
ug63/sensors/ |
Decoder registry, canonical.py Node runner, _runner.js |
vendor/milesight-decoders/ |
Official Milesight JS decoders (git submodule) |
frontend/ |
dashboard.html (live charts + uplinks table), topology.html |
docs/ |
API maps, device registry, gateway SSOT docs |
migrations/ |
TimescaleDB schema migrations (.sql, numbered) |
infra/ |
chirpstack-gateway-bridge config |
scripts/ |
env-sync.sh, env-verify.sh, probe_cs4_wire.py, add_sensor.py |
.claude/ |
LLM-agent context — rules, lexicon, stack-map, plans, decisions |
git clone --recurse-submodules <repo>
cd cedlora
python3 -m venv .venv && source .venv/bin/activate
pip install -e .
cp .env.example .env # fill in credentials
cedlora tail # stream uplinks from all gateways → TimescaleDB
cedlora serve # dashboard at http://localhost:8000cp .env.example .env
# edit .env — required vars:
# DATABASE_URL postgresql://cedlora:<pw>@<host>:5433/cedlora
# CS4_HOST http://<rak7391>:8080
# CS4_API_KEY create with: ssh rak@<rak7391> sudo chirpstack --config /etc/chirpstack create-api-key --name <name>
# GW_HOST http://<ug63-ip>
# GW_PASS UG63 admin password
# LORAWAN_APP_KEY 32-char uppercase hexCLAUDE.md is the agent boot card — route table for every domain concept, stack table, veto list.
.claude/rules/ contains the domain vocabulary:
| file | what |
|---|---|
cedlora-lexicon.md |
canonical words per domain (LoRaWAN, auth, sensors, CS4, persistence, frontend) |
cedlora-stack-map.md |
word topology — 8 layers, 9 roads, boundary-word table |
cedlora-word-standards.md |
per-word SSOT + SOTA + rule + shape + verify + anti |
auto-improve.md |
append-only session learnings (corrections + calibration results) |
.claude/plans/open/ — active feature plans.
.claude/decisions/ — architecture decisions (append-only, criteria + alternatives + falsifier).
Last updated: 2026-04-29
| area | status | notes |
|---|---|---|
| UG63 REST + CGI | ✅ operational | REST + CGI + SSH all working |
| UG63 MQTT | CONNACK rc=5 — credential issue not yet resolved | |
| UG65 MQTT | ✅ operational | uplinks flowing via central broker bridge |
| CS4 gRPC-web client | ✅ operational | provision_device_otaa, list tenants/apps/devices/gateways |
| CS4 MQTT stream | ✅ operational | eu868/gateway/+/event/up via propella bridge |
| TimescaleDB sink | ✅ operational | non-blocking queue, batched writes, health endpoint |
| Sensor decoders | ✅ operational | EM400-MUD, AM107 confirmed; full Milesight catalog vendored |
| Device registry | ✅ operational | DB catalog + docs/devices/ MD mirror + drift gate |
| Dashboard | ✅ operational | series charts (ced-charts), uplinks table, crosshair sync |
| Topology page | ✅ done | LR node tree — sensor → gateways → NS → broker → DB → UI |
| Autonomous OTAA provisioning | ✅ done | code-only via cs4/client.py:provision_device_otaa |
| UG63 MQTT fix | 🔲 pending | auth rc=5 root cause unknown; plan FEAT-ug63-llm-native |
cs4/client.pyrewritten with [PROBED] field numbers from live JS bundle — full Create chain, idempotent provision_device_otaa, delete_device- Device catalog table (
migrations/001_devices.sql) +cedlora/db.pyregister/get/list/delete docs/devices/— per-device MD files (frontmatter + restore SQL);scripts/verify-devices-ssot.pyCI gate- Dashboard charts: ced-charts crosshair sync fixed — dispatches domain time value, receivers compute nearest index via binary search (was: local array index → wrong timestamp across charts)
- Topology page: LR node tree (sensor → 3 gateways → 3 NS → broker → decoder → DB → API/UI)
- API
/topologyroute +/api/healthendpoint (DB + CS4 liveness) scripts/probe_cs4_wire.py— extracts CS4 proto field numbers from the live frontend JS bundle
- UG65 MQTT operational (loraserver, propella bridge)
- Central broker on propella bridges UG65:1883 + RAK7391:1883 → propella:1883
- TimescaleDB non-blocking sink — queue, batched flush, ThreadedConnectionPool, health()
- Frame type classification + sentinel filtering
- Dashboard MVP: ced-charts area charts per sensor field + uplinks table
- env SSOT model: docs/env-runtime.md + scripts/env-sync.sh + scripts/env-verify.sh
- ChirpStack v4 native systemd install on RAKPiOS (no Docker)
- pg_trgm extension required before first migration (documented in auto-improve)
- CS4 gRPC-web client: discovered port 8080 only (not 8090), /api.{Service}/{Method} paths
- api-key Bearer auth — static JWT from
chirpstack create-api-key - Mosquitto bridge direction: propella initiates (gateway can't reach propella — AP isolation)
- UG63 REST + CGI client (AES-mangle auth, JWT-cookie)
- Milesight decoder submodule + canonical.py Node subprocess runner
cedlora tailCLI entry point- Initial pytest smoke tests
decisions.md + .claude/decisions/decisions.md — every non-trivial choice documented with criteria, alternatives considered, reversal condition, and falsifier.
Key decisions recorded:
- TimescaleDB over InfluxDB/SQLite (query patterns, existing infra, no external dependency)
- propella as deploy host (DB + broker co-located, survives WSL restarts)
- Central MQTT broker with inbound bridges (gateway AP isolation — gateways can't reach propella)
- CS4 gRPC-web via handcrafted protobuf (no .proto files in deployed CS4 native install)
ruff check . # linting
pytest tests/ # smoke tests (requires LAN access to gateways)Tests are smoke-only — they hit real gateways on the LAN. No mock DB tests by design (see auto-improve.md 2026-04-28 SOTA performance rule).