本文へスキップ
ブログに戻る

MQTT and WebSocket: one pipeline from device to browser

約6分1回閲覧

Why two protocols#

The question that always comes up about SMTrack+ is this: MQTT can run over WebSocket already, so why not let the browser connect straight to the broker? The short answer is that the two protocols were designed for opposite ends of the system, and collapsing them into one only moves the problem somewhere else.

ConcernMQTTWebSocket
Designed forConstrained devicesPages open in a browser
ModelPublish/subscribe through a brokerA two-way channel between browser and server
Per-message costTiny headers, tolerant of flaky linksLarger, which is fine on a building network
What we actually use it forQoS, retained messages, last willPushing state into an already-open dashboard

Device side: let MQTT do what it is good at#

  • Give each unit its own topic — smtrack/<deviceId>/temperature — so filtering is the brokers job, not the applications.
  • Publish readings at QoS 1: a duplicate reading is a far cheaper mistake than a lost one.
  • Set a last will per device, so when a cable is pulled or power drops the broker announces it for you instead of leaving you to interpret silence.
  • Keep the newest value as a retained message, so a freshly restarted service sees current state without waiting for the next sampling cycle.
javascript
// one broker connection for the whole server, not one per browser tab
private readonly latest = new Map<string, Reading>()

onModuleInit() {
  this.mqtt.subscribe('smtrack/+/temperature', { qos: 1 })
  this.mqtt.on('message', (topic, payload) => {
    const deviceId = topic.split('/')[1]
    this.latest.set(deviceId, JSON.parse(payload.toString()))
  })

  // devices publish every 2s; the dashboard only needs 1 Hz
  setInterval(() => this.flush(), 1000)
}

private flush() {
  for (const [deviceId, reading] of this.latest) {
    this.gateway.to(`device:${deviceId}`).emit('reading', reading)
  }
  this.latest.clear()
}

Browser side: send only what the screen needs#

The server is the bridge. It holds one connection to the broker and fans messages out over WebSocket into per-user rooms. The immediate payoff is that access control is enforced server-side, instead of trusting a browser not to subscribe to topics it should not see.

  1. Coalesce readings before sending. A dashboard does not want every sample a sensor produces, it wants the latest trustworthy value.
  2. Only stream the devices someone is actually looking at: join the room on mount, leave it on unmount.
  3. On reconnect, fetch a snapshot over REST once and then resume the stream — otherwise the UI sits on stale numbers and says nothing about it.
  4. Keep alerts on a separate channel from routine readings, because the two do not carry the same urgency.
The hard failure is never data that does not arrive. It is data that arrives while the screen quietly keeps showing the old value.
From watching the system run in production

What the rollout taught me#

  • Make the server the single bridge between the broker and every client
  • Coalesce readings before fan-out so browsers are not doing needless work
  • Detect dropped devices with last will instead of inferring it from silence
  • Always re-fetch a snapshot after a reconnect so nothing goes stale
  • Record sensor-to-screen latency so it can be tracked over time