194 lines
14 KiB
Markdown
194 lines
14 KiB
Markdown
# Run the MQTT 5 backend and simulator
|
||
|
||
Sprints 3–4 use the dedicated deployed broker. All commands below are standard Linux commands, run on your development computer. The existing production project is not involved.
|
||
|
||
## Run with Docker Compose
|
||
|
||
`Dockerfile` builds both Go programs into one image. The root `compose.yaml` starts the observer and browser viewer; its optional `simulator` profile starts the Go test device. This connects outbound to the already deployed remote broker. The broker itself remains a separate Compose deployment under `deploy/`.
|
||
|
||
The Compose setup uses Linux host networking so the Go viewer can keep its enforced `127.0.0.1:8080` bind. It does not publish the viewer on an external interface. Run this on a Linux Docker Engine host. The container runs as your host UID/GID so it can read the private, read-only credential bind mounts without changing their permissions.
|
||
|
||
Copy `.env.docker.example` to `.env` and set the paths for the CA and private JSON handoffs on this computer. `.env` is ignored by Git; it contains paths only, not credential contents. For the current setup, the example paths point to `$HOME/mqtt-test-credentials`. Set `LOCAL_UID` and `LOCAL_GID` to `id -u` and `id -g` if they differ from the example.
|
||
|
||
Start the observer and wait for its `subscriptions_ready` log:
|
||
|
||
```bash
|
||
docker compose up --build -d observer
|
||
docker compose logs -f observer
|
||
```
|
||
|
||
Then open <http://127.0.0.1:8080>. In another terminal, start the simulator profile:
|
||
|
||
```bash
|
||
docker compose --profile simulator up -d simulator
|
||
```
|
||
|
||
Follow both event logs with `docker compose logs -f observer simulator`. Stop the simulator and observer with `docker compose --profile simulator down`. For a harmless ping, run the one-shot command container:
|
||
|
||
```bash
|
||
docker compose run --rm --no-deps \
|
||
-e TEST_MQTT5_WEB__ENABLED=false \
|
||
-e TEST_MQTT5_TOPICS__OBSERVE_DEVICES= \
|
||
-e TEST_MQTT5_TOPICS__OBSERVE_ALL=false \
|
||
-e TEST_MQTT5_RECEIPTS__ENABLED=false observer \
|
||
--broker tls://109.122.252.129:8884 --ca /run/test-mqtt/ca.crt \
|
||
--credentials /run/test-mqtt/backend.json \
|
||
--client-id rm5-docker-command-01 command ping
|
||
```
|
||
|
||
The command container inherits the CA and backend credential mounts from the observer service. The `-e` override disables the viewer for this short-lived `command` mode.
|
||
|
||
## Build
|
||
|
||
```bash
|
||
cd /home/ryan/Media/Code/Code/robot-market/test-mqtt-5
|
||
go build -buildvcs=false -o bin/test-mqtt5 ./cmd/test-mqtt5
|
||
go build -buildvcs=false -o bin/device-sim ./cmd/device-sim
|
||
```
|
||
|
||
Client credential handoffs are private JSON files copied from the server. `--credentials` reads only username/password; it never logs them. Select the broker URL and CA explicitly. Flags precede subcommands. Every running process must use a distinct client ID: sharing one disconnects the previous client. Simulator and command modes have separate defaults; command mode generates a new client ID unless configuration overrides it.
|
||
|
||
## TLS: open three terminals
|
||
|
||
Run the simulator in the first terminal:
|
||
|
||
```bash
|
||
./bin/device-sim \
|
||
--broker tls://109.122.252.129:8884 \
|
||
--ca "$HOME/mqtt-test-credentials/ca.crt" \
|
||
--credentials "$HOME/mqtt-test-credentials/sim-001.json" \
|
||
--client-id rm5-lab-simulator-01
|
||
```
|
||
|
||
Run the observer in the second:
|
||
|
||
```bash
|
||
./bin/test-mqtt5 \
|
||
--broker tls://109.122.252.129:8884 \
|
||
--ca "$HOME/mqtt-test-credentials/ca.crt" \
|
||
--credentials "$HOME/mqtt-test-credentials/test-backend.json" \
|
||
--client-id rm5-lab-observer-01 observe
|
||
```
|
||
|
||
Send harmless commands in the third:
|
||
|
||
```bash
|
||
./bin/test-mqtt5 \
|
||
--broker tls://109.122.252.129:8884 \
|
||
--ca "$HOME/mqtt-test-credentials/ca.crt" \
|
||
--credentials "$HOME/mqtt-test-credentials/test-backend.json" command ping
|
||
|
||
./bin/test-mqtt5 \
|
||
--broker tls://109.122.252.129:8884 \
|
||
--ca "$HOME/mqtt-test-credentials/ca.crt" \
|
||
--credentials "$HOME/mqtt-test-credentials/test-backend.json" command get_version
|
||
```
|
||
|
||
You should see `connection_up`, `subscriptions_ready`, simulator online status and periodic telemetry. Follow a command's request ID through `publish_attempt`, `transport_ack`, `command_accepted`, and `command_completed`. A completed ping contains `pong:true`; version returns `test-sim-1.0.0`. Transport ACK confirms broker acceptance, not execution. Fast application ACKs can be logged before the transport publish call returns.
|
||
|
||
The observer sees other clients' command/ACK traffic. It cannot measure their own-request latency: it labels ACKs without an active own request as `late_ack`, with outcome `no active own request; latency not measured`. Command mode measures its own latency and prints final counters.
|
||
|
||
Press Ctrl+C to stop the observer or simulator. The simulator attempts retained offline status before disconnecting. Its configured Will also reports offline on an ungraceful connection loss, with `will_configured_at` rather than an invented disconnect timestamp. A Will is an availability indication, not proof of why a device stopped.
|
||
|
||
## Browser viewer
|
||
|
||
The viewer is optional, starts only with the long-running `observe` process, and listens on `127.0.0.1:8080`. Create an ignored local config file:
|
||
|
||
```bash
|
||
cat > viewer.local.toml <<'EOF'
|
||
[web]
|
||
enabled = true
|
||
EOF
|
||
```
|
||
|
||
Add `--config viewer.local.toml` to the observer command in the TLS section, before `observe`, then open <http://127.0.0.1:8080>. Keep the observer running. Start the simulator in its own terminal to generate telemetry, and use a third terminal for `command ping` or `command get_version`. The page refreshes event counters and streams sanitized events live. Its device JSON-contract row means the payload matched the Go contract; it does not identify hardware or infer the device MQTT version. It offers direction, topic, device and request filters, pause/resume, and clear. The viewer has no publish endpoint; commands remain CLI-only.
|
||
|
||
The timeline keeps at most 1,000 displayed events; the server snapshot is limited by configured event history and SSE replay to 500 events. A gap notice means the page must resynchronize after events were evicted or its bounded queue overflowed. Compatibility evidence distinguishes the Go observer's MQTT 5 session from device telemetry and native MQTT 5 request/correlation support; the latter is not inferred from a payload.
|
||
|
||
## Plaintext testing
|
||
|
||
Use the same commands with `--broker mqtt://109.122.252.129:1884 --allow-plaintext` instead of the TLS URL/CA flags. The opt-in is required in each process, and credentials/ACLs still apply. Plaintext carries credentials and payloads without encryption. TLS certificate verification is never disabled.
|
||
|
||
For example:
|
||
|
||
```bash
|
||
./bin/device-sim --broker mqtt://109.122.252.129:1884 --allow-plaintext \
|
||
--credentials "$HOME/mqtt-test-credentials/sim-001.json"
|
||
|
||
./bin/test-mqtt5 --broker mqtt://109.122.252.129:1884 --allow-plaintext \
|
||
--credentials "$HOME/mqtt-test-credentials/test-backend.json" command ping
|
||
```
|
||
|
||
Run only one simulator for sim-001 at a time; the dedicated device identity and topics represent one simulated device.
|
||
|
||
## JSON logs and configuration
|
||
|
||
Create an ignored `logging.local.toml` to inspect structured JSON events:
|
||
|
||
```toml
|
||
[logging]
|
||
format = "json"
|
||
level = "info"
|
||
```
|
||
|
||
Add `--config logging.local.toml` before the subcommand. Native correlation bytes appear base64-encoded in JSON. Payload previews contain only validated contract fields and are bounded; malformed inputs are omitted and free-form ACK error messages are redacted. MQTT user properties and arbitrary response topics are not logged. Log levels warn/error suppress normal info events; use info for input/output inspection.
|
||
|
||
Defaults < TOML < environment < explicitly supplied flags. The private handoff supplied with `--credentials` acts as an explicit username/password override. TOML/client-ID settings can override role defaults; use `--client-id` to prevent collisions when sharing a full config file. `check-config` remains offline even with a credential handoff.
|
||
|
||
Runtime requires `mqtt.clean_start=true` for the initial process connection. Default session expiry is 300 seconds; reconnects inside that process use Clean Start=false. When the broker session is missing, subscriptions are rebuilt and SUBACK reason codes checked before readiness. Reconnect backoff uses bounded jitter (default 1–30 seconds). Initial readiness, publish and shutdown operations are bounded.
|
||
|
||
Defaults are 256 inbound messages, 1,000 retained inspection events, 128 pending commands, 1,024 simulator dedup entries retained for 10 minutes, 16 KiB payloads and 4 KiB previews. The simulator processes commands in one worker. Full dedup capacity rejects new requests rather than evicting an in-progress entry. Incoming overflow counts `event_gap`; a MQTT receipt ACK does not prove simulator admission. Dedup retention/cache and client protocol state are memory-only; restarting a client does not promise durable deduplication or recovery.
|
||
|
||
Commands are non-retained QoS 1, have a default 15-second deadline plus MQTT Message Expiry, and use exact native Response Topic/Correlation Data. Backend native correlation is the request ID's bytes; the simulator supports any nonempty correlation value up to 64 bytes and echoes it unchanged. It never executes beyond the application deadline. Completed duplicates replay a cached ACK; in-progress duplicates replay acceptance. Conflicting command/correlation reuse is rejected. Timeouts and connection loss report execution unknown, without an application outbox or automatic command retry. MQTT QoS 1 itself may retransmit an in-flight packet during reconnect, which is why deduplication is required.
|
||
|
||
## Verification
|
||
|
||
Offline checks:
|
||
|
||
```bash
|
||
gofmt -l cmd internal
|
||
go vet ./...
|
||
go test ./...
|
||
go test -race ./...
|
||
go mod verify
|
||
```
|
||
|
||
Real-broker tests are explicitly opt-in and use only the dedicated test accounts/topics. They run simulators, publish non-retained test commands/telemetry, update sim-001 retained status, and briefly drop their own local test connection to verify reconnection. Do not run them while another sim-001 simulator is active. They do not restart or reconfigure the broker.
|
||
|
||
```bash
|
||
MQTT5_INTEGRATION_CREDENTIAL_DIR="$HOME/mqtt-test-credentials" \
|
||
go test -race -v -count=1 -timeout=90s ./internal/integration
|
||
```
|
||
|
||
Without that variable, integration tests skip network access; those skips are not evidence of remote success. The recorded Sprint 3 remote run used the copied credential files against 109.122.252.129 and passed on both transports. Physical ESP32 interoperability and the larger reliability matrix remain later sprints. Browser viewer checks are recorded in [Sprint 4](sprints/04-browser-viewer.md).
|
||
|
||
## Postman and physical-device functional testing
|
||
|
||
The Docker observer subscribes to `robot-market/test/v1/#`. It displays every received message under that namespace as `message_received`, including arbitrary topics, plain text, any JSON shape, empty messages and literal timestamp placeholders. There is no observer payload schema, timestamp or device identity validation. `topics.observe_all=true` enables this mode. Outside functional observation, command/simulator contracts remain available for their structured round-trip tests.
|
||
|
||
The test broker has password authentication but no topic ACL. Any existing test account, including `esp32-lab-001`, may publish and subscribe to `robot-market/test/v1/sim-001/telemetry` or another test topic. Plaintext uses `mqtt://109.122.252.129:1884`; TLS uses `mqtts://109.122.252.129:8884` in Postman. Select a distinct client ID, MQTT 5.0, QoS 1 and click Connect before Send.
|
||
|
||
Send `hello from postman` to `robot-market/test/v1/sim-001/telemetry`. The literal payload must appear in the browser at http://127.0.0.1:8080. If it does not, inspect Postman's connection result and publish acknowledgement; the server must receive the publication before the observer can show it. Refresh the browser and clear filters. The simulator can stay stopped.
|
||
|
||
Traffic display is separate from protocol-feature evidence: raw receipt demonstrates delivery, not a native MQTT 5 correlated command completion. Resource limits remain: 16 KiB message payload, 4 KiB display preview and bounded event queues/history. Binary payloads display as base64.
|
||
|
||
## Device receipt ACKs
|
||
|
||
Docker enables `TEST_MQTT5_RECEIPTS__ENABLED=true`. Subscribe the device to `<namespace>/<topic-device>/acks` before sending telemetry/live status or a custom message. The backend sends `type=receipt_ack`, `ok=true`, source topic, timestamp, byte length and SHA-256, echoing optional JSON message_id/request_id. No JSON or identity validation is required. Native MQTT 5 Response Topic and Correlation Data are supported. Commands, ACK/receipt topics, receipt_ack payloads and retained replays are excluded to prevent feedback loops.
|
||
|
||
Replies have QoS 1, retain=false. The bounded reply queue reports receipt_ack_sent, receipt_ack_failed or receipt_ack_dropped in logs/UI. A duplicate live delivery can receive another receipt; no exactly-once, persistence or reconnect outbox is promised. Subscribe first: a non-retained reply sent before a subscription is established may be missed.
|
||
|
||
Run the independent-client verification with the simulator stopped:
|
||
|
||
```bash
|
||
python3 scripts/check_receipts.py --credentials esp32-test-kit/connection.txt --ca "$HOME/mqtt-test-credentials/ca.crt"
|
||
```
|
||
|
||
This checks plaintext/TLS, MQTT 3.1.1/5, text/arbitrary JSON/empty payloads, no ACK loop, and native MQTT 5 response/correlation echo using unique test topic paths.
|
||
|
||
### Reliability evidence in the viewer
|
||
|
||
Run the opt-in suite with `MQTT5_INTEGRATION_CREDENTIAL_DIR` pointing at the private client handoff directory (backend/device JSON and CA). Each named remote test writes a structured `scenario_result` to verbose test output and publishes it to `robot-market/test/v1/scenarios/results`. The raw observer exposes these as reported compatibility rows with environment, endpoint, protocol, observation and UTC time. These are test-client reports, not independently verified ESP32 claims. Results are bounded by viewer history and disappear on observer restart. Missing rows remain untested; test publication failure fails the test.
|
||
|
||
The raw viewer shows observation time and payload previews; it does not interpret an old retained `online` payload as current device health. A Will's `will_configured_at` is when the Will was created, not the instant the connection broke. See [VERIFICATION.md](VERIFICATION.md) for outcomes and limits.
|