Files
Ryan 71ef622b4f
Test and deploy MQTT backend / test (push) Successful in 2m8s
Test and deploy MQTT backend / deploy (push) Successful in 18s
Deploy MQTT test backend with private viewer and Gitea CI/CD
2026-10-08 14:48:53 +03:30

14 KiB
Raw Permalink Blame History

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:

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:

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:

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

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:

./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:

./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:

./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:

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:

./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:

[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:

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.

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.

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:

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 for outcomes and limits.