From 1c8be5488dd7c6e70be260d8154bd8a0e37d8824 Mon Sep 17 00:00:00 2001 From: Joe LaPenna Date: Sun, 4 Oct 2026 00:59:32 +0000 Subject: [PATCH] fix: preserve unknown camera live-view status --- CHANGELOG.md | 3 +++ internal/cmd/status.go | 4 ++-- internal/printer/status.go | 12 ++++++++--- internal/printer/status_test.go | 36 ++++++++++++++++++++++++++++++++- 4 files changed, 49 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bc63084..9406fa4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,9 @@ JSON field names and exit codes are part of the public API. ## [Unreleased] +### Changed +- `status` JSON `camera_lan_liveview` is now nullable: absent, null, empty or malformed `ipcam.rtsp_url` reports produce `null` (human output: `unknown`), not a false disabled-setting claim. Explicit `disable` remains `false`; a nonempty advertised route remains `true`. Consumers must handle the new unknown state. No printer settings are changed. Fixes #1. + ## [0.1.0] - 2026-09-23 ### Added diff --git a/internal/cmd/status.go b/internal/cmd/status.go index 474a7a2..4ddb5e8 100644 --- a/internal/cmd/status.go +++ b/internal/cmd/status.go @@ -146,8 +146,8 @@ func watch(g *Globals, conn printer.Conn) error { } func humanStatus(h *output.Human, p *config.Printer, s printer.Status) { - h.Line("%s %s %s / %s dev_mode=%s sdcard=%v liveview=%v", h.Bold(p.Name), h.Dim("("+p.Host+")"), h.Status(s.State), s.Stage, - h.Status(ptrBool(s.DevMode)), s.SDCard, s.Liveview) + h.Line("%s %s %s / %s dev_mode=%s sdcard=%v liveview=%s", h.Bold(p.Name), h.Dim("("+p.Host+")"), h.Status(s.State), s.Stage, + h.Status(ptrBool(s.DevMode)), s.SDCard, ptrBool(s.Liveview)) if s.Job.Name != "" { h.Line(" job %s %d%% layer %d/%d %d min left", s.Job.Name, s.Job.Percent, s.Job.Layer, s.Job.TotalLayers, s.Job.RemainingMin) } diff --git a/internal/printer/status.go b/internal/printer/status.go index 7cbddd8..674bc17 100644 --- a/internal/printer/status.go +++ b/internal/printer/status.go @@ -30,7 +30,7 @@ type Status struct { Protections Protections `json:"protections"` DevMode *bool `json:"dev_mode"` SDCard bool `json:"sdcard"` - Liveview bool `json:"camera_lan_liveview"` + Liveview *bool `json:"camera_lan_liveview"` // nil when no nonempty string was reported WifiSignal string `json:"wifi_signal,omitempty"` } @@ -193,8 +193,14 @@ func Summarize(p map[string]any) Status { PrintingMonitor: boolp(x["printing_monitor"]), BuildplateMarkerDetector: boolp(x["buildplate_marker_detector"]), PrintHalt: boolp(x["print_halt"]), HaltSensitivity: str(x["halt_print_sensitivity"]), } - rtsp := str(obj(p["ipcam"])["rtsp_url"]) - s.Liveview = rtsp != "" && rtsp != "disable" + // Missing, null, empty or malformed telemetry does not establish that + // the printer's LAN live-view setting is disabled. Preserve unknown just + // as we do for dev_mode and protection flags. An advertised route is + // not proof that a camera connection or frame capture will succeed. + if rtsp, ok := obj(p["ipcam"])["rtsp_url"].(string); ok && rtsp != "" { + enabled := rtsp != "disable" + s.Liveview = &enabled + } return s } diff --git a/internal/printer/status_test.go b/internal/printer/status_test.go index 6a12768..e9695e3 100644 --- a/internal/printer/status_test.go +++ b/internal/printer/status_test.go @@ -49,7 +49,7 @@ func TestSummarize(t *testing.T) { if len(s.Errors.HMS) != 1 || !s.Errors.HMS[0].Informational || s.Errors.Blocking { t.Fatalf("info HMS must not block: %+v", s.Errors) } - if !s.Liveview || !s.SDCard || s.External != "" { + if s.Liveview == nil || !*s.Liveview || !s.SDCard || s.External != "" { t.Fatalf("flags: %+v", s) } if s.Protections.FirstLayerInspector == nil || !*s.Protections.FirstLayerInspector { @@ -57,6 +57,40 @@ func TestSummarize(t *testing.T) { } } +func TestLiveviewReportedState(t *testing.T) { + for _, tc := range []struct { + name string + ipcam any + want string + }{ + {"missing", nil, "null"}, + {"empty object", map[string]any{}, "null"}, + {"null URL", map[string]any{"rtsp_url": nil}, "null"}, + {"empty URL", map[string]any{"rtsp_url": ""}, "null"}, + {"malformed URL", map[string]any{"rtsp_url": false}, "null"}, + {"disabled", map[string]any{"rtsp_url": "disable"}, "false"}, + {"advertised", map[string]any{"rtsp_url": "rtsps://192.0.2.1:322/streaming/live/1"}, "true"}, + } { + t.Run(tc.name, func(t *testing.T) { + p := map[string]any{} + if tc.ipcam != nil { + p["ipcam"] = tc.ipcam + } + b, err := json.Marshal(Summarize(p)) + if err != nil { + t.Fatal(err) + } + var result map[string]json.RawMessage + if err := json.Unmarshal(b, &result); err != nil { + t.Fatal(err) + } + if got := string(result["camera_lan_liveview"]); got != tc.want { + t.Fatalf("camera_lan_liveview = %s, want %s", got, tc.want) + } + }) + } +} + func TestDevModeOff(t *testing.T) { m := load(t) m["fun"] = "20011A30F9CFB"