Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 8 additions & 6 deletions docs/matrix-mqtt-protocol.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,12 +83,14 @@ retained desired state.

## Target status and acknowledgement

Targets publish `coreview.matrix.status.v1` with `online`, `width`, `height`,
`colorDepth`, `rotation`, `features`, and optional firmware version. CoreView
uses it for observability; the server-side target configuration remains the
authority for compilation. After applying a state or event, targets publish
`coreview.matrix.ack.v1` with its `revision` or `eventId` and either `ok: true`
or a bounded error code.
Targets publish retained `coreview.matrix.status.v1` on MQTT connection and as
a periodic heartbeat (the reference firmware reports every 30 seconds). It
contains `online`, `width`, `height`, `colorDepth`, `rotation`, `features`, and
an optional firmware version. CoreView uses it for observability; the
server-side target configuration remains the authority for compilation. After
applying a state or event, targets publish `coreview.matrix.ack.v1` with its
`revision` or `eventId` and either `ok: true` or a bounded error code. A valid
acknowledgement also refreshes the target's liveness in CoreView.

An unclaimed reference Beacon also includes a six-digit `claimCode` in its
status payload. CoreView uses this only to confirm physical possession while an
Expand Down
4 changes: 3 additions & 1 deletion esphome/hardware/esp32s3-hub75-3x2-32x16.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,9 @@ display:
for (int y = 0; y < height; y++) for (int x = 0; x < width; x++) {
uint32_t hash = (uint32_t) x * 374761393UL + (uint32_t) y * 668265263UL + frame * 2246822519UL;
hash = (hash ^ (hash >> 13)) * 1274126177UL; hash ^= hash >> 16;
int heat = (height - 1 - y) * 255 / std::max(1, height - 1) + (int) (hash & 0x3FU) - 70;
// ESPHome coordinates start at the top. Heat belongs at the
// bottom so the flame rises upward instead of hanging down.
int heat = y * 255 / std::max(1, height - 1) + (int) (hash & 0x3FU) - 70;
heat = std::max(0, std::min(255, heat));
it.draw_pixel_at(x, y, Color((uint8_t) heat, (uint8_t) (heat / 3), 0));
}
Expand Down
46 changes: 30 additions & 16 deletions esphome/packages/coreview-matrix-base.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,35 @@ globals:
restore_value: no
initial_value: '64'

script:
- id: coreview_publish_status
then:
- mqtt.publish_json:
topic: ${coreview_status_topic}
retain: true
payload: |-
root["schema"] = "coreview.matrix.status.v1";
root["online"] = true;
root["width"] = id(coreview_matrix).get_width();
root["height"] = id(coreview_matrix).get_height();
root["colorDepth"] = ${matrix_color_depth};
root["rotation"] = ${matrix_rotation};
root["firmwareVersion"] = "esphome";
JsonArray features = root["features"].to<JsonArray>();
features.add("clock"); features.add("status"); features.add("notification");
features.add("ticker"); features.add("icons"); features.add("music");
root["claimCode"] = id(coreview_claim_code);
root["claimed"] = id(coreview_claimed);

interval:
- interval: 30s
then:
- if:
condition:
mqtt.connected:
then:
- script.execute: coreview_publish_status

mqtt:
broker: !secret mqtt_broker
username: !secret mqtt_username
Expand All @@ -149,22 +178,7 @@ mqtt:
payload: '{"schema":"coreview.matrix.status.v1","online":false}'
retain: true
on_connect:
- mqtt.publish_json:
topic: ${coreview_status_topic}
retain: true
payload: |-
root["schema"] = "coreview.matrix.status.v1";
root["online"] = true;
root["width"] = id(coreview_matrix).get_width();
root["height"] = id(coreview_matrix).get_height();
root["colorDepth"] = ${matrix_color_depth};
root["rotation"] = ${matrix_rotation};
root["firmwareVersion"] = "esphome";
JsonArray features = root["features"].to<JsonArray>();
features.add("clock"); features.add("status"); features.add("notification");
features.add("ticker"); features.add("icons"); features.add("music");
root["claimCode"] = id(coreview_claim_code);
root["claimed"] = id(coreview_claimed);
- script.execute: coreview_publish_status
on_json_message:
topic: ${coreview_state_topic}
then:
Expand Down
20 changes: 17 additions & 3 deletions server/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ const CORS_ORIGINS = (process.env.CORS_ORIGINS || "")
.map((origin) => origin.trim())
.filter(Boolean);
const HEARTBEAT_STALE_MS = 45000;
// Matrix targets report retained MQTT status every 30 seconds. Allow two
// missed reports before declaring one stale so a short Wi-Fi/MQTT blip does
// not contradict a panel that is still receiving state.
const MATRIX_HEARTBEAT_STALE_MS = 90000;
const RUNTIME_OVERRIDE_SWEEP_MS = 5000;
const PENDING_DEVICE_TTL_MS = Math.max(
60 * 60 * 1000,
Expand Down Expand Up @@ -4877,15 +4881,17 @@ async function refreshFrigateStatus() {
}

function screenStateFor(target, info = {}) {
const matrixTarget = getMatrixTarget(target);
const matrixStatus = matrixTargetStatus.get(target);
const effectiveInfo = getMatrixTarget(target) && matrixStatus
const effectiveInfo = matrixTarget && matrixStatus
? { ...info, connected: Boolean(matrixStatus.online), lastSeen: matrixStatus.lastSeen, userAgent: matrixStatus.payload?.firmwareVersion || "matrix" }
: info;
const lastSeen = Number(effectiveInfo.lastSeen || 0);
const ageMs = lastSeen > 0 ? Date.now() - lastSeen : null;
const staleAfterMs = matrixTarget ? MATRIX_HEARTBEAT_STALE_MS : HEARTBEAT_STALE_MS;
let status = "offline";
if (effectiveInfo.connected) {
status = ageMs !== null && ageMs <= HEARTBEAT_STALE_MS ? "online" : "stale";
status = ageMs !== null && ageMs <= staleAfterMs ? "online" : "stale";
}
const screen = getScreenRecord(target);
const assignedView = screen?.viewId ? getView(screen.viewId) : null;
Expand Down Expand Up @@ -5397,7 +5403,15 @@ function attachMqttClientHandlers(client) {
const target = String(topic.slice(matrixPrefix.length, -"/ack".length) || "").trim().toLowerCase();
const existing = matrixTargetStatus.get(target) || {};
if (getMatrixTarget(target)) {
matrixTargetStatus.set(target, { ...existing, lastAckAt: Date.now(), lastAck: parsed && typeof parsed === "object" ? parsed : {} });
// An acknowledgement can only be emitted by a target that received
// and applied our state, so it is authoritative liveness evidence.
matrixTargetStatus.set(target, {
...existing,
online: true,
lastSeen: Date.now(),
lastAckAt: Date.now(),
lastAck: parsed && typeof parsed === "object" ? parsed : {}
});
}
return;
}
Expand Down
Loading