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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions internal/cmd/status.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
12 changes: 9 additions & 3 deletions internal/printer/status.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
}

Expand Down Expand Up @@ -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
}

Expand Down
36 changes: 35 additions & 1 deletion internal/printer/status_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,14 +49,48 @@ 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 {
t.Fatal("protections")
}
}

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"
Expand Down
Loading