Reverses the "entry goes in with the work" rule from two commits ago. That was wrong on the evidence: of the docs(changelog) commits in history, 3 of 4 in seismo-relay and 2 of 4 in Terra-View were made directly on dev. The rule was generalized from one unrepresentative commit rather than from the pattern. It also caused the exact problem it was supposed to avoid. With four worktrees in flight, every branch edits the same few lines at the top of CHANGELOG.md; feat/ach-rescue-on-connect and feat/sensor-check-h5 collide on that file and nothing else. Writing the entry once, on dev, after the merge removes the whole conflict class. The second benefit is accuracy: an entry written after the merge describes what actually landed, including anything that changed during conflict resolution. The sensor-check branch is a live example — its Unreleased preamble describes a release that no longer looks like that. The failure mode of writing it later is forgetting, so the merge is explicitly not finished until Unreleased is updated — same sitting, reconstructed from the branch commit messages. Unchanged: no preamble under Unreleased, the mandatory operational consequence, and cutting the version on dev when ready to ship to main. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qcu9ByJfuKBQxmrWb8rSrN
137 lines
6.4 KiB
Markdown
137 lines
6.4 KiB
Markdown
# CLAUDE.md — SLMM (Sound Level Meter Manager)
|
|
|
|
Device module for **Rion NL-43 / NL-53** sound level meters. Translates
|
|
Terra-View's REST calls into the meter's ASCII command protocol over TCP, and
|
|
pulls stored data over FTP. Current version: **v0.4.0**.
|
|
|
|
Stack-level context (which repo owns what, version pairing) lives in
|
|
`../terra-view/docs/tmi-stack.md`, which is also loaded as `~/CLAUDE.md`.
|
|
|
|
**Terra-View never talks to a meter directly** — it always goes through here.
|
|
SLMM owns the protocol; Terra-View owns the UI and the measurement records.
|
|
|
|
---
|
|
|
|
## Where things stand (updated 2026-08-29)
|
|
|
|
v0.4.0 (2026-06-22) is the current release. The three things that define the
|
|
current design:
|
|
|
|
- **Fan-out live monitor.** The NL-43 has **one** TCP control connection, and
|
|
every dashboard used to fight for it. Now a single poller reads the device and
|
|
all subscribers share one cached feed: `WS /api/nl43/{unit_id}/monitor`
|
|
delivers an instant first frame from cache, then live updates. Poll rate
|
|
adapts to demand, unreachable devices back off, and the background poller
|
|
skips units already covered by an active monitor so nothing double-polls.
|
|
- **Alert engine.** Per-device threshold rules (metric + threshold + cooldown)
|
|
with full CRUD, an onset/clear state machine, and acknowledgement. Enabled
|
|
rules **pin the monitor on**, so they evaluate 24/7 with no UI client
|
|
connected. Editing or deleting a rule resets its state and closes any open
|
|
event.
|
|
- **History for chart backfill.** A downsampled trail persists to `nl43_readings`
|
|
and is served from `GET /api/nl43/{unit_id}/history`, so live charts can fill
|
|
in recent history on load. L1/L10 percentiles are surfaced in status and the
|
|
live feed.
|
|
|
|
`CHANGELOG.md` is the authority for anything older; prefer it over prose here.
|
|
|
|
---
|
|
|
|
## Changelog & release convention
|
|
|
|
Adopted 2026-09-18, to match seismo-relay and Terra-View. SLMM had no written
|
|
practice before this — earlier releases used ad-hoc commits like
|
|
`chore: version bump`. This is the standard going forward.
|
|
|
|
**Feature branches do NOT touch `CHANGELOG.md`. Write the entry on `dev`, as
|
|
part of finishing the merge, under `## [Unreleased]`. Cut the version on `dev` in a
|
|
dedicated release commit when you are ready to ship to `main`.**
|
|
|
|
- **The changelog is written on `dev`, never on a feature branch.** With
|
|
several branches in flight they all edit the same few lines at the top of
|
|
the file and conflict every time. Writing it once, after the merge, also
|
|
lets it describe what actually *landed* — including anything that changed
|
|
during conflict resolution.
|
|
- ⚠ **The merge is not finished until `## [Unreleased]` is updated.** Same sitting,
|
|
not "later" — that is the one failure mode of writing it after the fact.
|
|
Reconstruct from the branch's own commit messages:
|
|
`git log --oneline dev..<branch>` before you merge, or
|
|
`git log --oneline <merge-base>..<branch>` after.
|
|
- **No preamble under `## [Unreleased]`** — just the `### Added` / `### Changed`
|
|
/ `### Fixed` lists. The themed opening paragraph gets written at release
|
|
time, when the whole release is visible and can be named honestly. A theme
|
|
written when the first item landed is stale by the third.
|
|
- ⚠ **State the operational consequence**, **including when it is "none."**
|
|
Silence is ambiguous; "none" is information. For this repo that means:
|
|
- **DB migration** — whether a `migrate_*.py` script is needed, which one,
|
|
and whether it is safe to re-run. These live at the repo root and are
|
|
easy to forget at deploy time.
|
|
- **Meter-connection impact** — anything touching the NL-43/NL-53 TCP path,
|
|
polling cadence, or the monitor fan-out. The meter has a **single TCP
|
|
slot**, so a change that bounces the connection is an operational event,
|
|
not just a code change.
|
|
- **Releases are cut on judgement, not on a schedule or a merge.** `Unreleased`
|
|
is the staging area for whatever is going into the next release; when enough
|
|
has accumulated to be worth shipping, it gets a number and a date. Nothing
|
|
about a merge to `dev` triggers a release.
|
|
- **Cutting a release** is its own `chore(release): vX.Y.Z` commit on `dev`,
|
|
renaming `## [Unreleased]` → `## [X.Y.Z] - YYYY-MM-DD` and bumping the
|
|
version in `app/main.py` (the FastAPI `version=` argument).
|
|
- **`main` carries only released versions.** No `## [Unreleased]` section
|
|
there; it lands via the `dev` → `main` PR.
|
|
|
|
---
|
|
|
|
## Layout
|
|
|
|
```
|
|
app/
|
|
main.py FastAPI app + health checks
|
|
routers.py all REST endpoints
|
|
services.py NL43Client — TCP + FTP protocol
|
|
monitor.py the fan-out live feed
|
|
background_poller.py scheduled polling
|
|
alerts.py rule evaluation + event state machine
|
|
models.py NL43Config, NL43Status, nl43_readings
|
|
database.py SQLAlchemy / SQLite (data/slmm.db)
|
|
device_logger.py
|
|
docs/ manuals/ templates/ nl52/ SLM-stress-test/
|
|
```
|
|
|
|
Key docs: `docs/API.md` (endpoint reference with curl examples),
|
|
`docs/COMMUNICATION_GUIDE.md` and `docs/nl43_Command_ref.md` (protocol).
|
|
|
|
---
|
|
|
|
## API surface
|
|
|
|
```
|
|
GET/PUT /api/nl43/{unit}/config
|
|
GET /api/nl43/{unit}/status cached snapshot
|
|
GET /api/nl43/{unit}/live fresh read
|
|
POST /api/nl43/{unit}/{start|stop|pause|resume|reset|store}
|
|
GET /api/nl43/{unit}/{battery|clock|results|settings}
|
|
WS /api/nl43/{unit}/monitor fan-out live feed
|
|
POST /api/nl43/{unit}/monitor/{start|stop}
|
|
GET /api/nl43/_monitor/status
|
|
GET /api/nl43/{unit}/history backfill trail
|
|
.../alerts/rules .../alerts/events .../events/{id}/ack
|
|
POST/GET /api/nl43/{unit}/ftp/{enable|disable|status|files|download}
|
|
GET /api/nl43/{unit}/overwrite-check BEFORE changing a store name
|
|
```
|
|
|
|
---
|
|
|
|
## Gotchas
|
|
|
|
- **One TCP connection per meter.** This is the constraint the whole monitor
|
|
design exists to work around. Never add a code path that opens its own
|
|
connection alongside the monitor.
|
|
- **1-second minimum between commands.** The NL-43 protocol requires it; SLMM
|
|
enforces it automatically. Do not "optimise" it away.
|
|
- **Check `overwrite-check` before changing a store name** — otherwise you can
|
|
destroy data on the meter's SD card.
|
|
- **FTP is active mode** — the device connects *back* to the server on port 21.
|
|
- **`network_mode: host`** in compose, for direct network access to meters.
|
|
- Environment: `PORT` (8100), `CORS_ORIGINS`, `TIMEZONE_OFFSET`, `TIMEZONE_NAME`.
|