Fix/ws wait for displaying ack (#884)
Build and test / Lint & Quality Checks (push) Has been cancelled
Build and test / build-and-test (tronbyt-server-darwin-arm64, arm64, darwin, macos-26) (push) Has been cancelled
Build and test / build-and-test (tronbyt-server-linux-amd64, amd64, linux, ubuntu-24.04) (push) Has been cancelled
Build and test / build-and-test (tronbyt-server-linux-arm64, arm64, linux, ubuntu-24.04-arm) (push) Has been cancelled
Build and test / build-and-test (tronbyt-server-windows-amd64.exe, amd64, windows, windows-2025) (push) Has been cancelled
Create and publish a container image / build-and-push-image (push) Has been cancelled
Build and test / Create Release (push) Has been cancelled

* Wait indefinitely for displaying ACK on WS v1+ devices.

Remove the 30s fallback timeout so long-running WebP apps are not skipped while the device is still animating. Legacy firmware without protocol_version still uses dwell-based timing.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Add 10-minute safety timeout when displaying ACK is missing.

Prevents WS rotation from stalling forever on v1+ firmware while still allowing long WebP animations to complete.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Tavis
2026-08-18 23:25:44 -07:00
committed by GitHub
parent ed52825d80
commit 4e1a228bb8
2 changed files with 40 additions and 19 deletions
+3 -2
View File
@@ -102,8 +102,9 @@ The device sends JSON messages to the server to acknowledge the state of images.
The server uses a sophisticated acknowledgment system to manage the flow of images.
* After sending an image, the server waits for an `Image Displaying` message from the device.
* **Timeout for Old Firmware**: If the server doesn't receive an acknowledgment within a certain timeout period, it assumes the device is running older firmware that doesn't send acknowledgments. It then falls back to a simple time-based delay (`dwell_time`) between sending images.
* After sending an image, the server waits for an `Image Displaying` message from the device before sending the next image.
* **v1+ Firmware (protocol_version ≥ 1)**: The server waits for `displaying` before sending the next image. If no ACK arrives within 10 minutes (safety cap for dropped images or firmware bugs), the server logs a warning and advances rotation. Pushed preview images can interrupt the wait immediately.
* **Legacy Firmware**: Devices without `protocol_version` do not send `displaying` ACKs. The server falls back to a dwell-time delay before sending the next image.
* **Push Notifications**: The waiting process can be interrupted by server-side events, such as:
* An ephemeral app being pushed to the device.
* A change in the device's settings (e.g., brightness).
+37 -17
View File
@@ -15,9 +15,10 @@ import (
)
const (
// minAckTimeoutSeconds is the minimum time to wait for device ACK.
// This accounts for the previous app's dwell time, network latency, and processing overhead.
minAckTimeoutSeconds = 30
// maxDisplayingAckTimeoutSeconds is a safety cap for v1+ firmware when displaying
// ACK never arrives (dropped image, firmware bug). Long enough for very long WebP
// animations but prevents indefinite rotation stall.
maxDisplayingAckTimeoutSeconds = 600
)
type WSMessage struct {
@@ -286,19 +287,19 @@ func (s *Server) wsWriteLoop(ctx context.Context, conn *websocket.Conn, initialD
sendImmediate = false
}
// 3. Wait for ACK or Timeout or Interrupt
var timeoutSec int
// 3. Wait for displaying ACK, safety/legacy timeout, or interrupt.
// v1+ firmware sends displaying when the image is on screen (including long
// animations). We wait for that ACK, with a long safety timeout if it never arrives.
var timer *time.Timer
var timerC <-chan time.Time
if device.Info.ProtocolVersion != nil {
// Device may delay ACK until previous app completes its dwell time.
// Wait at least minAckTimeoutSeconds OR 2x the dwell time, whichever is greater.
timeoutSec = max(dwell*2, minAckTimeoutSeconds)
timer = time.NewTimer(time.Duration(maxDisplayingAckTimeoutSeconds) * time.Second)
timerC = timer.C
} else {
// Old firmware: wait exactly dwell time
timeoutSec = dwell
timer = time.NewTimer(time.Duration(dwell) * time.Second)
timerC = timer.C
}
timer := time.NewTimer(time.Duration(timeoutSec) * time.Second)
interrupted := false
waiting := true
@@ -325,7 +326,6 @@ func (s *Server) wsWriteLoop(ctx context.Context, conn *websocket.Conn, initialD
slog.Debug("Received ACK for default or pushed image (no app context)", "device", device.ID)
}
}
// If just Queued, we keep waiting for Displaying.
case val := <-broadcastCh:
// Update available (Reload device first)
reloaded, err := gorm.G[data.Device](s.DB).Preload("Apps", nil).Where("id = ?", initialDevice.ID).First(ctx)
@@ -379,15 +379,35 @@ func (s *Server) wsWriteLoop(ctx context.Context, conn *websocket.Conn, initialD
lastSentBrightness = newBrightness
}
}
case <-timer.C:
// Timeout
case <-timerC:
if device.Info.ProtocolVersion != nil {
appIname := ""
if app != nil {
appIname = app.Iname
}
slog.Warn(
"Timed out waiting for displaying ACK, advancing rotation",
"device", device.ID,
"app", appIname,
"timeout_secs", maxDisplayingAckTimeoutSeconds,
)
}
waiting = false
case <-stopCh:
timer.Stop()
if timer != nil {
timer.Stop()
}
return
}
}
timer.Stop()
if timer != nil {
if !timer.Stop() {
select {
case <-timer.C:
default:
}
}
}
if interrupted {
continue