diff --git a/apps/m9zCtrlTty/firefox_install.go b/apps/m9zCtrlTty/firefox_install.go index 01c16dc..ab6075c 100644 --- a/apps/m9zCtrlTty/firefox_install.go +++ b/apps/m9zCtrlTty/firefox_install.go @@ -46,6 +46,16 @@ var forbiddenFirefoxDebPackages = map[string]struct{}{ "util-linux": {}, } +var optionalFirefoxDebPackages = map[string]string{ + "lubuntu-icon-theme": "theme package may conflict with existing adwaita-icon-theme-full and is not required by Firefox", +} + +type firefoxBundleInstallPlan struct { + TotalPackageCount int + HasFirefoxPackage bool + BaseSystemPackages []string +} + func isFirefoxUsable() bool { cmd := exec.Command("firefox", "--version") out, err := cmd.CombinedOutput() @@ -55,19 +65,41 @@ func isFirefoxUsable() bool { } func isFirefoxInstalled() bool { - cmd := exec.Command("which", "firefox") - out, err := cmd.CombinedOutput() - ok := err == nil - kioskDebugf("firefox install path check ok=%v output=%q err=%v", ok, strings.TrimSpace(string(out)), err) - return ok + if path, err := exec.LookPath("firefox"); err == nil { + kioskDebugf("firefox install path check ok=true path=%s source=PATH", path) + return true + } + + for _, path := range []string{ + "/usr/bin/firefox", + "/usr/local/bin/firefox", + "/snap/bin/firefox", + } { + if isExecutableFile(path) { + kioskDebugf("firefox install path check ok=true path=%s source=fallback", path) + return true + } + } + + kioskDebugf("firefox install path check ok=false") + return false +} + +func isExecutableFile(path string) bool { + info, err := os.Stat(path) + return err == nil && !info.IsDir() && info.Mode()&0111 != 0 } func installFirefox() error { bundlePath := firefoxBundlePath() - if err := validateFirefoxBundle(bundlePath); err != nil { + plan, err := validateFirefoxBundle(bundlePath) + if err != nil { kioskErrorf("firefox install refused bundle=%s err=%v", bundlePath, err) return err } + baseSystemPackages := strings.Join(plan.BaseSystemPackages, " ") + optionalSkipPackages := strings.Join(optionalFirefoxDebPackageNames(), " ") + kioskInfof("firefox install package filter baseSystemPackages=%q optionalSkipPackages=%q", baseSystemPackages, optionalSkipPackages) var p struct { DebPkgPath string `json:"DebPkgPath"` @@ -89,7 +121,7 @@ echo "skip automatic dpkg purge to avoid touching base system packages" echo "" echo "=== clean previous offline bundle files ===" -rm -rf /opt/firefox-deb /opt/firefox-deb.tar.gz 2>/dev/null +rm -rf /opt/firefox-deb /opt/firefox-installable /opt/firefox-deb.tar.gz 2>/dev/null echo "" echo "=== start Firefox install ===" @@ -101,20 +133,112 @@ echo "2. extract offline bundle..." cd /opt && tar -xzvf firefox-deb.tar.gz || { echo "error: extract offline bundle failed"; exit 1; } echo "" -echo "3. install Firefox deb packages..." -cd /opt/firefox-deb && sudo dpkg -i *.deb || { echo "error: install Firefox deb packages failed"; exit 1; } +echo "3. filter Firefox deb packages..." +base_system_packages=%s +optional_skip_packages=%s +mkdir -p /opt/firefox-installable +selected_count=0 +skipped_installed_count=0 +skipped_optional_count=0 +selected_missing_base_count=0 +selected_regular_count=0 +firefox_selected=0 +select_deb() { + cp -v "$deb" /opt/firefox-installable/ + selected_count=$((selected_count + 1)) +} +package_name_from_deb() { + pkg=$(dpkg-deb -f "$1" Package 2>/dev/null || true) + if [ -n "$pkg" ]; then + echo "$pkg" + return 0 + fi + file=$(basename "$1") + echo "$file" | cut -d '_' -f 1 | sed 's/[.]deb$//' +} +for deb in /opt/firefox-deb/*.deb; do + [ -e "$deb" ] || continue + file=$(basename "$deb") + pkg=$(package_name_from_deb "$deb") + if [ -z "$pkg" ]; then + echo "skip package with empty name: $file" + continue + fi + echo "package candidate: $file package=$pkg" + + status=$(dpkg-query -W -f='${Status}' "$pkg" 2>/dev/null || true) + installed=0 + if [ "$status" = "install ok installed" ]; then + installed=1 + fi + + if [ "$pkg" = "firefox" ]; then + select_deb + firefox_selected=1 + continue + fi + + case " $optional_skip_packages " in + *" $pkg "*) + echo "skip optional package: $file package=$pkg" + skipped_optional_count=$((skipped_optional_count + 1)) + continue + ;; + esac + + case " $base_system_packages " in + *" $pkg "*) + if [ "$installed" -eq 1 ]; then + echo "skip installed base system package: $file" + skipped_installed_count=$((skipped_installed_count + 1)) + continue + fi + echo "select missing base system package: $file" + select_deb + selected_missing_base_count=$((selected_missing_base_count + 1)) + continue + ;; + esac + + if [ "$installed" -eq 1 ]; then + echo "skip installed package: $file" + skipped_installed_count=$((skipped_installed_count + 1)) + continue + fi + + select_deb + selected_regular_count=$((selected_regular_count + 1)) +done +echo "selected packages: $selected_count, selected missing base: $selected_missing_base_count, selected regular: $selected_regular_count, skipped installed: $skipped_installed_count, skipped optional: $skipped_optional_count" +if [ "$selected_count" -eq 0 ]; then + echo "error: no packages selected for install" + exit 1 +fi +if [ "$firefox_selected" -ne 1 ]; then + echo "error: Firefox deb package was not selected" + exit 1 +fi +echo "" + +echo "4. install selected Firefox packages..." +cd /opt/firefox-installable || { echo "error: enter installable package directory failed"; exit 1; } +install_status=0 +sudo dpkg -i ./*.deb || install_status=$? +if [ "$install_status" -ne 0 ]; then + echo "warning: selected Firefox deb package install returned status $install_status, verify Firefox next" +fi echo "" -echo "4. verify Firefox..." +echo "5. verify Firefox..." firefox --version || { echo "error: Firefox verification failed"; exit 1; } echo "" echo "=== Firefox install complete ===" -`, shellQuote(bundlePath))) +`, shellQuote(bundlePath), shellQuote(baseSystemPackages), shellQuote(optionalSkipPackages))) command.Stdout = os.Stdout command.Stderr = os.Stderr - err := command.Run() + err = command.Run() if err != nil { kioskErrorf("firefox install command failed err=%v", err) return err @@ -155,22 +279,38 @@ func firefoxBundlePath() string { return filepath.Join(system.GetPathOfProgram(), g.Config().Firefox.DebPkgPath, g.Config().Firefox.DebPkgName) } -func validateFirefoxBundle(bundlePath string) error { +func validateFirefoxBundle(bundlePath string) (firefoxBundleInstallPlan, error) { packages, err := firefoxBundlePackageNames(bundlePath) if err != nil { - return fmt.Errorf("read firefox bundle packages: %w", err) + return firefoxBundleInstallPlan{}, fmt.Errorf("read firefox bundle packages: %w", err) } if len(packages) == 0 { - return fmt.Errorf("firefox bundle has no deb packages") + return firefoxBundleInstallPlan{}, fmt.Errorf("firefox bundle has no deb packages") } - blocked := unsafeFirefoxDebPackages(packages) - if len(blocked) > 0 { - return fmt.Errorf("firefox bundle contains blocked base system packages: %s", strings.Join(blocked, ", ")) + plan := firefoxBundleInstallPlanFromPackages(packages) + if !plan.HasFirefoxPackage { + return firefoxBundleInstallPlan{}, fmt.Errorf("firefox bundle has no firefox deb package") } - kioskInfof("firefox bundle validation passed bundle=%s packages=%d", bundlePath, len(packages)) - return nil + if len(plan.BaseSystemPackages) > 0 { + kioskWarnf("firefox bundle contains base system packages; installed ones will be skipped and missing ones may be installed packages=%s", strings.Join(plan.BaseSystemPackages, ", ")) + } + kioskInfof("firefox bundle validation passed bundle=%s totalPackages=%d baseSystemPackages=%d hasFirefox=%v", bundlePath, plan.TotalPackageCount, len(plan.BaseSystemPackages), plan.HasFirefoxPackage) + return plan, nil +} + +func firefoxBundleInstallPlanFromPackages(packages []string) firefoxBundleInstallPlan { + plan := firefoxBundleInstallPlan{ + TotalPackageCount: len(packages), + BaseSystemPackages: unsafeFirefoxDebPackages(packages), + } + for _, name := range packages { + if name == "firefox" { + plan.HasFirefoxPackage = true + } + } + return plan } func firefoxBundlePackageNames(bundlePath string) ([]string, error) { @@ -232,6 +372,15 @@ func unsafeFirefoxDebPackages(packages []string) []string { return blocked } +func optionalFirefoxDebPackageNames() []string { + names := make([]string, 0, len(optionalFirefoxDebPackages)) + for name := range optionalFirefoxDebPackages { + names = append(names, name) + } + sort.Strings(names) + return names +} + func shellQuote(value string) string { return "'" + strings.ReplaceAll(value, "'", "'\\''") + "'" } diff --git a/apps/m9zCtrlTty/firefox_install_test.go b/apps/m9zCtrlTty/firefox_install_test.go index adcb2d7..6ee8feb 100644 --- a/apps/m9zCtrlTty/firefox_install_test.go +++ b/apps/m9zCtrlTty/firefox_install_test.go @@ -35,6 +35,37 @@ func TestUnsafeFirefoxDebPackages(t *testing.T) { } } +func TestFirefoxBundleInstallPlanDetectsBasePackages(t *testing.T) { + packages := []string{ + "firefox", + "libgtk-3-0", + "dpkg", + "coreutils", + "libc6", + "tar", + } + + plan := firefoxBundleInstallPlanFromPackages(packages) + wantBlocked := []string{"coreutils", "dpkg", "libc6", "tar"} + if !plan.HasFirefoxPackage { + t.Fatal("plan should detect firefox package") + } + if plan.TotalPackageCount != len(packages) { + t.Fatalf("TotalPackageCount = %d, want %d", plan.TotalPackageCount, len(packages)) + } + if !stringSlicesEqual(plan.BaseSystemPackages, wantBlocked) { + t.Fatalf("BaseSystemPackages = %v, want %v", plan.BaseSystemPackages, wantBlocked) + } +} + +func TestOptionalFirefoxDebPackageNames(t *testing.T) { + got := optionalFirefoxDebPackageNames() + want := []string{"lubuntu-icon-theme"} + if !stringSlicesEqual(got, want) { + t.Fatalf("optionalFirefoxDebPackageNames() = %v, want %v", got, want) + } +} + func stringSlicesEqual(left []string, right []string) bool { if len(left) != len(right) { return false diff --git a/apps/m9zCtrlTty/install.go b/apps/m9zCtrlTty/install.go index c995d76..3641bf6 100644 --- a/apps/m9zCtrlTty/install.go +++ b/apps/m9zCtrlTty/install.go @@ -7,20 +7,33 @@ import ( ) func install() error { - kioskInfof("checking firefox usability") + kioskInfof("checking firefox installation") var firefoxInstallErr error - if isFirefoxUsable() { - kioskInfof("firefox is usable") + + firefoxInstalled := isFirefoxInstalled() + firefoxUsable := false + if firefoxInstalled { + firefoxUsable = isFirefoxUsable() + } + markerExists := hasFirefoxInstallMarker() + kioskInfof("firefox startup state installed=%v usable=%v markerExists=%v", firefoxInstalled, firefoxUsable, markerExists) + + switch { + case firefoxInstalled && firefoxUsable: + kioskInfof("firefox installed and usable") fmt.Println("Firefox is installed and usable") if err := writeFirefoxInstallMarker("usable"); err != nil { kioskWarnf("write firefox install marker failed err=%v", err) } - } else if hasFirefoxInstallMarker() { - kioskWarnf("firefox unusable but install marker exists; skip automatic repair") - fmt.Println("Firefox unusable; skip automatic repair on startup") - } else { - kioskWarnf("firefox unusable and no install marker; starting first-time install") - fmt.Println("Firefox missing or unusable, installing...") + case firefoxInstalled: + kioskWarnf("firefox is installed but version check failed; skip automatic reinstall") + fmt.Println("Firefox installed but unusable; skip automatic reinstall on startup") + case markerExists: + kioskWarnf("firefox executable missing but install marker exists; skip automatic reinstall") + fmt.Println("Firefox missing; skip automatic reinstall on startup") + case shouldAutoInstallFirefox(firefoxInstalled, markerExists): + kioskWarnf("firefox executable missing and no install marker; starting first-time install") + fmt.Println("Firefox missing, installing...") if err := installFirefox(); err != nil { kioskErrorf("firefox first-time install failed err=%v", err) fmt.Printf("install Firefox failed: %v\n", err) @@ -59,3 +72,7 @@ func install() error { } return nil } + +func shouldAutoInstallFirefox(installed bool, markerExists bool) bool { + return !installed && !markerExists +} diff --git a/apps/m9zCtrlTty/install_test.go b/apps/m9zCtrlTty/install_test.go new file mode 100644 index 0000000..517e74c --- /dev/null +++ b/apps/m9zCtrlTty/install_test.go @@ -0,0 +1,39 @@ +package main + +import "testing" + +func TestShouldAutoInstallFirefox(t *testing.T) { + tests := []struct { + name string + installed bool + markerExists bool + want bool + }{ + { + name: "missing without marker can install", + installed: false, + markerExists: false, + want: true, + }, + { + name: "installed must not reinstall even when unusable", + installed: true, + markerExists: false, + want: false, + }, + { + name: "missing with marker must not reinstall", + installed: false, + markerExists: true, + want: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := shouldAutoInstallFirefox(tt.installed, tt.markerExists); got != tt.want { + t.Fatalf("shouldAutoInstallFirefox(%v, %v) = %v, want %v", tt.installed, tt.markerExists, got, tt.want) + } + }) + } +} diff --git a/apps/m9zCtrlTty/log3.png b/apps/m9zCtrlTty/log3.png new file mode 100644 index 0000000..baa158a Binary files /dev/null and b/apps/m9zCtrlTty/log3.png differ diff --git a/apps/m9zCtrlTty/m9zCtrlTty.go b/apps/m9zCtrlTty/m9zCtrlTty.go index 5fbaffb..49130ad 100644 --- a/apps/m9zCtrlTty/m9zCtrlTty.go +++ b/apps/m9zCtrlTty/m9zCtrlTty.go @@ -85,23 +85,27 @@ func start() { } } - kioskInfof("starting firefox client goroutine port=%d", g.Config().Server.Port) - go func(port int) { - time.Sleep(500 * time.Millisecond) - runClient(port) - }(g.Config().Server.Port) - go runStartupChecks() + if err := runStartupChecks(); err != nil { + kioskErrorf("skip firefox client startup because startup checks failed err=%v", err) + } else { + kioskInfof("starting firefox client goroutine port=%d", g.Config().Server.Port) + go func(port int) { + time.Sleep(500 * time.Millisecond) + runClient(port) + }(g.Config().Server.Port) + } core.RunServer() } -func runStartupChecks() { +func runStartupChecks() error { startedAt := time.Now() kioskInfof("startup checks begin") if err := install(); err != nil { kioskErrorf("startup checks failed elapsed=%s err=%v", time.Since(startedAt).Round(time.Millisecond), err) - return + return err } kioskInfof("startup checks done elapsed=%s", time.Since(startedAt).Round(time.Millisecond)) + return nil } // TODO Build Cmd CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -o m9zCtrlTty-arm64 . diff --git a/internal/module/m9zTtyApi/automatic_mode_demo.go b/internal/module/m9zTtyApi/automatic_mode_demo.go new file mode 100644 index 0000000..09410bd --- /dev/null +++ b/internal/module/m9zTtyApi/automatic_mode_demo.go @@ -0,0 +1,138 @@ +package m9zApi + +import ( + "sync" + "time" + + "tgk-touch/internal/library/m9z" +) + +const automaticModeUseDemoAPI = false + +type automaticModeTaskReq struct { + CommUid string `json:"comm_uid"` + Mode uint8 `json:"mode"` +} + +type automaticModeDemoStore struct { + mu sync.RWMutex + currentMode uint8 + startTasks [3]m9z.ControlTask + stopTasks [3]m9z.ControlTask + middleTasks [3]m9z.MiddleControlTask +} + +var automaticModeDemo = newAutomaticModeDemoStore() + +func newAutomaticModeDemoStore() *automaticModeDemoStore { + store := &automaticModeDemoStore{} + for mode := uint8(0); mode < 3; mode++ { + store.startTasks[mode] = buildAutomaticModeDemoTask(6+int(mode), 30, true, uint8(60+mode*10)) + store.stopTasks[mode] = buildAutomaticModeDemoTask(22, int(mode)*10, false, 0) + store.middleTasks[mode] = m9z.MiddleControlTask{ + ControlTaskList: []m9z.ControlTask{ + buildAutomaticModeDemoTask(12, int(mode)*10, true, uint8(45+mode*5)), + }, + } + } + return store +} + +func buildAutomaticModeDemoTask(hour int, minute int, turnOn bool, pwm uint8) m9z.ControlTask { + loops := make([]m9z.TaskLoop, 10) + for i := range loops { + loops[i] = m9z.TaskLoop{ + Index: uint(i), + TurnOn: turnOn, + Pwm: pwm, + } + } + return m9z.ControlTask{ + ExecTime: time.Date(2025, 6, 7, hour, minute, 0, 0, time.Local), + TimeType: 0, + Loops: loops, + } +} + +func normalizeAutomaticMode(mode uint8) uint8 { + if mode > 2 { + return 0 + } + return mode +} + +func cloneAutomaticControlTask(task m9z.ControlTask) m9z.ControlTask { + task.Loops = append([]m9z.TaskLoop(nil), task.Loops...) + return task +} + +func cloneAutomaticMiddleTask(task m9z.MiddleControlTask) m9z.MiddleControlTask { + task.ControlTaskList = append([]m9z.ControlTask(nil), task.ControlTaskList...) + for i := range task.ControlTaskList { + task.ControlTaskList[i] = cloneAutomaticControlTask(task.ControlTaskList[i]) + } + return task +} + +func (s *automaticModeDemoStore) getMode() uint8 { + s.mu.RLock() + defer s.mu.RUnlock() + return s.currentMode +} + +func (s *automaticModeDemoStore) setMode(mode uint8) { + s.mu.Lock() + defer s.mu.Unlock() + s.currentMode = normalizeAutomaticMode(mode) +} + +func (s *automaticModeDemoStore) getStartTask(mode uint8) m9z.ControlTask { + s.mu.RLock() + defer s.mu.RUnlock() + return cloneAutomaticControlTask(s.startTasks[normalizeAutomaticMode(mode)]) +} + +func (s *automaticModeDemoStore) getStartTaskProgram(mode uint8) (*m9z.TaskProgram, error) { + normalizedMode := normalizeAutomaticMode(mode) + return m9z.ControlTaskToTaskProgram(s.getStartTask(normalizedMode), normalizedMode) +} + +func (s *automaticModeDemoStore) setStartTask(mode uint8, task m9z.ControlTask) { + s.mu.Lock() + defer s.mu.Unlock() + s.startTasks[normalizeAutomaticMode(mode)] = cloneAutomaticControlTask(task) +} + +func (s *automaticModeDemoStore) getStopTask(mode uint8) m9z.ControlTask { + s.mu.RLock() + defer s.mu.RUnlock() + return cloneAutomaticControlTask(s.stopTasks[normalizeAutomaticMode(mode)]) +} + +func (s *automaticModeDemoStore) getStopTaskProgram(mode uint8) (*m9z.TaskProgram, error) { + normalizedMode := normalizeAutomaticMode(mode) + return m9z.ControlTaskToTaskProgram(s.getStopTask(normalizedMode), normalizedMode) +} + +func (s *automaticModeDemoStore) setStopTask(mode uint8, task m9z.ControlTask) { + s.mu.Lock() + defer s.mu.Unlock() + s.stopTasks[normalizeAutomaticMode(mode)] = cloneAutomaticControlTask(task) +} + +func (s *automaticModeDemoStore) getMiddleTask(mode uint8) m9z.MiddleControlTask { + s.mu.RLock() + defer s.mu.RUnlock() + return cloneAutomaticMiddleTask(s.middleTasks[normalizeAutomaticMode(mode)]) +} + +func (s *automaticModeDemoStore) getMiddleTaskProgram(mode uint8) (*m9z.TaskProgram, error) { + normalizedMode := normalizeAutomaticMode(mode) + return m9z.MiddleControlTaskToTaskProgram(s.getMiddleTask(normalizedMode), normalizedMode) +} + +func (s *automaticModeDemoStore) setMiddleTask(mode uint8, task m9z.MiddleControlTask) { + s.mu.Lock() + defer s.mu.Unlock() + s.middleTasks[normalizeAutomaticMode(mode)] = cloneAutomaticMiddleTask(task) +} diff --git a/internal/module/m9zTtyApi/automatic_mode_demo_test.go b/internal/module/m9zTtyApi/automatic_mode_demo_test.go new file mode 100644 index 0000000..511c1b8 --- /dev/null +++ b/internal/module/m9zTtyApi/automatic_mode_demo_test.go @@ -0,0 +1,71 @@ +package m9zApi + +import ( + "testing" + + "tgk-touch/internal/library/m9z" +) + +func TestAutomaticModeDemoStoreReturnsCopies(t *testing.T) { + store := newAutomaticModeDemoStore() + + start := store.getStartTask(0) + if len(start.Loops) == 0 { + t.Fatal("demo start task should include loops") + } + start.Loops[0].TurnOn = !start.Loops[0].TurnOn + start.Loops[0].Pwm = 1 + + got := store.getStartTask(0) + if got.Loops[0].Pwm == 1 { + t.Fatal("demo start task loop was mutated through returned value") + } + + middle := store.getMiddleTask(0) + if len(middle.ControlTaskList) == 0 || len(middle.ControlTaskList[0].Loops) == 0 { + t.Fatal("demo middle task should include control tasks and loops") + } + middle.ControlTaskList[0].Loops[0].Pwm = 1 + + gotMiddle := store.getMiddleTask(0) + if gotMiddle.ControlTaskList[0].Loops[0].Pwm == 1 { + t.Fatal("demo middle task loop was mutated through returned value") + } +} + +func TestNormalizeAutomaticMode(t *testing.T) { + if got := normalizeAutomaticMode(2); got != 2 { + t.Fatalf("normalizeAutomaticMode(2) = %d, want 2", got) + } + if got := normalizeAutomaticMode(3); got != 0 { + t.Fatalf("normalizeAutomaticMode(3) = %d, want 0", got) + } +} + +func TestAutomaticModeDemoStoreBuildsTaskPrograms(t *testing.T) { + store := newAutomaticModeDemoStore() + + programs := map[string]func(uint8) (*m9z.TaskProgram, error){ + "start": store.getStartTaskProgram, + "middle": store.getMiddleTaskProgram, + "stop": store.getStopTaskProgram, + } + + for name, build := range programs { + t.Run(name, func(t *testing.T) { + program, err := build(1) + if err != nil { + t.Fatalf("build %s demo task program failed: %v", name, err) + } + if program == nil { + t.Fatalf("%s demo task program is nil", name) + } + if program.Index != 1 { + t.Fatalf("%s demo task program index = %d, want 1", name, program.Index) + } + if len(program.Instructions) == 0 { + t.Fatalf("%s demo task program should include instructions", name) + } + }) + } +} diff --git a/internal/module/m9zTtyApi/read.go b/internal/module/m9zTtyApi/read.go index 670c98b..9fe0f30 100644 --- a/internal/module/m9zTtyApi/read.go +++ b/internal/module/m9zTtyApi/read.go @@ -39,6 +39,18 @@ func getOutStatus(rpc towgo.JsonRpcConnection) { // todo 有问题 不管写入什么模式返回都是FF 获取设备编程模式 1-3 对应模式01~模式03 编程模式0~2 func getDeviceMode(rpc towgo.JsonRpcConnection) { + if automaticModeUseDemoAPI { + getDeviceModeDemo(rpc) + return + } + getDeviceModeReal(rpc) +} + +func getDeviceModeDemo(rpc towgo.JsonRpcConnection) { + rpc.WriteResult(automaticModeDemo.getMode()) +} + +func getDeviceModeReal(rpc towgo.JsonRpcConnection) { cuid, _ := getDeviceId(rpc) read, err := m9z.ModeRead(cuid) @@ -135,8 +147,9 @@ type DeviceStatusLoop struct { } type DeviceStatus struct { m9z.DeviceStatus - ScanDeviceCount int `json:"scan_device_count"` - Loops []DeviceStatusLoop `json:"Loops"` + ScanDeviceCount int `json:"scan_device_count"` + AlarmDisplayEnabled bool `json:"alarm_display_enabled"` + Loops []DeviceStatusLoop `json:"Loops"` } // 实时监控数据 @@ -157,7 +170,9 @@ func getDeviceStatus2Real(rpc towgo.JsonRpcConnection) { d.DeviceStatus = *read scanLoopCount := configuredLoopCount() displayLoopCount := monitorDisplayLoopCount() + alarmDisplayEnabled := scanConfigManager.GetAlarmDisplayEnabled() d.ScanDeviceCount = scanLoopCount + d.AlarmDisplayEnabled = alarmDisplayEnabled loops := make([]DeviceStatusLoop, displayLoopCount) var wg sync.WaitGroup for i := 0; i < displayLoopCount; i++ { @@ -176,7 +191,7 @@ func getDeviceStatus2Real(rpc towgo.JsonRpcConnection) { loop.A = parametersRead.DCCurrent kw := float64(loop.V*loop.A) / 1000 loop.Kw = float32(math.Round(kw*1000) / 1000) - if idx < scanLoopCount { + if alarmDisplayEnabled && idx < scanLoopCount { loop.Faults = m9z.BuildLoopFaults(uint(idx+1), parametersRead.AlarmStatus, parametersRead.AlarmCode) loop.HasFault = len(loop.Faults) > 0 loop.FaultMsg = joinLoopFaultMessages(loop.Faults) @@ -194,7 +209,7 @@ func getDeviceStatus2Real(rpc towgo.JsonRpcConnection) { return d.Loops[i].Index < d.Loops[j].Index }) for _, loop := range d.Loops { - if loop.HasFault && loop.Index < scanLoopCount { + if alarmDisplayEnabled && loop.HasFault && loop.Index < scanLoopCount { recordLoopFaults(cuid, uint(loop.Index+1), loop.V, loop.Faults) } } @@ -205,9 +220,11 @@ func getDeviceStatus2Real(rpc towgo.JsonRpcConnection) { func buildDemoDeviceStatus2() *DeviceStatus { displayLoopCount := monitorDisplayLoopCount() scanLoopCount := configuredLoopCount() + alarmDisplayEnabled := scanConfigManager.GetAlarmDisplayEnabled() d := &DeviceStatus{ - ScanDeviceCount: scanLoopCount, - Loops: make([]DeviceStatusLoop, displayLoopCount), + ScanDeviceCount: scanLoopCount, + AlarmDisplayEnabled: alarmDisplayEnabled, + Loops: make([]DeviceStatusLoop, displayLoopCount), } d.Mode = 0 d.DeviceTime = time.Now().UTC() @@ -230,7 +247,7 @@ func buildDemoDeviceStatus2() *DeviceStatus { kw := float64(loop.V*loop.A) / 1000 loop.Kw = float32(math.Round(kw*1000) / 1000) - if i < scanLoopCount { + if alarmDisplayEnabled && i < scanLoopCount { switch i { case 0: loop.Faults = m9z.BuildLoopFaults(uint(i+1), 0x01, "01 00") @@ -299,6 +316,24 @@ func getDeviceSunRiseTime(rpc towgo.JsonRpcConnection) { // 读取开始任务 (优化指令 CMD=0x13) func getStartTask(rpc towgo.JsonRpcConnection) { + if automaticModeUseDemoAPI { + getStartTaskDemo(rpc) + return + } + getStartTaskReal(rpc) +} + +func getStartTaskDemo(rpc towgo.JsonRpcConnection) { + var p automaticModeTaskReq + rpc.ReadParams(&p) + program, err := automaticModeDemo.getStartTaskProgram(p.Mode) + if err != nil { + panic(err) + } + rpc.WriteResult(program) +} + +func getStartTaskReal(rpc towgo.JsonRpcConnection) { var p struct { CommUid string `json:"comm_uid"` Mode uint8 `json:"mode"` // 0x00-0x02 @@ -326,6 +361,24 @@ func getStartTask(rpc towgo.JsonRpcConnection) { // 读取中间任务 (优化指令 CMD=0x14) func getMiddleTaskRead(rpc towgo.JsonRpcConnection) { + if automaticModeUseDemoAPI { + getMiddleTaskReadDemo(rpc) + return + } + getMiddleTaskReadReal(rpc) +} + +func getMiddleTaskReadDemo(rpc towgo.JsonRpcConnection) { + var p automaticModeTaskReq + rpc.ReadParams(&p) + program, err := automaticModeDemo.getMiddleTaskProgram(p.Mode) + if err != nil { + panic(err) + } + rpc.WriteResult(program) +} + +func getMiddleTaskReadReal(rpc towgo.JsonRpcConnection) { var p struct { CommUid string `json:"comm_uid"` Mode uint8 `json:"mode"` // 0x00-0x02 @@ -344,6 +397,24 @@ func getMiddleTaskRead(rpc towgo.JsonRpcConnection) { // 读取结束任务 (优化指令 CMD=0x15) func getStopTaskRead(rpc towgo.JsonRpcConnection) { + if automaticModeUseDemoAPI { + getStopTaskReadDemo(rpc) + return + } + getStopTaskReadReal(rpc) +} + +func getStopTaskReadDemo(rpc towgo.JsonRpcConnection) { + var p automaticModeTaskReq + rpc.ReadParams(&p) + program, err := automaticModeDemo.getStopTaskProgram(p.Mode) + if err != nil { + panic(err) + } + rpc.WriteResult(program) +} + +func getStopTaskReadReal(rpc towgo.JsonRpcConnection) { var p struct { CommUid string `json:"comm_uid"` Mode uint8 `json:"mode"` // 0x00-0x02 @@ -361,6 +432,20 @@ func getStopTaskRead(rpc towgo.JsonRpcConnection) { rpc.WriteResult(read) } func getStartTask2(rpc towgo.JsonRpcConnection) { + if automaticModeUseDemoAPI { + getStartTask2Demo(rpc) + return + } + getStartTask2Real(rpc) +} + +func getStartTask2Demo(rpc towgo.JsonRpcConnection) { + var p automaticModeTaskReq + rpc.ReadParams(&p) + rpc.WriteResult(automaticModeDemo.getStartTask(p.Mode)) +} + +func getStartTask2Real(rpc towgo.JsonRpcConnection) { var p struct { CommUid string `json:"comm_uid"` Mode uint8 `json:"mode"` // 0x00-0x02 @@ -390,6 +475,20 @@ func getStartTask2(rpc towgo.JsonRpcConnection) { } func getStopTask2(rpc towgo.JsonRpcConnection) { + if automaticModeUseDemoAPI { + getStopTask2Demo(rpc) + return + } + getStopTask2Real(rpc) +} + +func getStopTask2Demo(rpc towgo.JsonRpcConnection) { + var p automaticModeTaskReq + rpc.ReadParams(&p) + rpc.WriteResult(automaticModeDemo.getStopTask(p.Mode)) +} + +func getStopTask2Real(rpc towgo.JsonRpcConnection) { var p struct { CommUid string `json:"comm_uid"` Mode uint8 `json:"mode"` // 0x00-0x02 @@ -419,6 +518,20 @@ func getStopTask2(rpc towgo.JsonRpcConnection) { } func getMiddleTask2(rpc towgo.JsonRpcConnection) { + if automaticModeUseDemoAPI { + getMiddleTask2Demo(rpc) + return + } + getMiddleTask2Real(rpc) +} + +func getMiddleTask2Demo(rpc towgo.JsonRpcConnection) { + var p automaticModeTaskReq + rpc.ReadParams(&p) + rpc.WriteResult(automaticModeDemo.getMiddleTask(p.Mode)) +} + +func getMiddleTask2Real(rpc towgo.JsonRpcConnection) { var p struct { CommUid string `json:"comm_uid"` diff --git a/internal/module/m9zTtyApi/scan_config.go b/internal/module/m9zTtyApi/scan_config.go index 60961ff..439cbc0 100644 --- a/internal/module/m9zTtyApi/scan_config.go +++ b/internal/module/m9zTtyApi/scan_config.go @@ -12,16 +12,20 @@ import ( "github.com/towgo/towgo/towgo" ) +const defaultAlarmDisplayEnabled = true + type ScanConfigManager struct { - mu sync.RWMutex - loopCount int + mu sync.RWMutex + loopCount int + alarmDisplayEnabled bool } type ScanConfigData struct { - LoopCount int `json:"loop_count"` + LoopCount int `json:"loop_count"` + AlarmDisplayEnabled *bool `json:"alarm_display_enabled,omitempty"` } -var scanConfigManager = &ScanConfigManager{} +var scanConfigManager = &ScanConfigManager{alarmDisplayEnabled: defaultAlarmDisplayEnabled} func getScanConfigFilePath() string { return filepath.Join(system.GetPathOfProgram(), "data", "scanConfig.json") @@ -33,15 +37,38 @@ func (m *ScanConfigManager) GetLoopCount() int { return normalizeLoopCount(m.loopCount) } +func (m *ScanConfigManager) GetAlarmDisplayEnabled() bool { + m.mu.RLock() + defer m.mu.RUnlock() + return m.alarmDisplayEnabled +} + func (m *ScanConfigManager) SetLoopCount(loopCount int) error { m.mu.Lock() defer m.mu.Unlock() - m.loopCount = loopCount + m.loopCount = normalizeLoopCount(loopCount) + if err := m.saveToFile(); err != nil { + return err + } + g.GVA_Config.DeviceInfo.LoopCount = m.loopCount + return nil +} + +func (m *ScanConfigManager) SetConfig(loopCount int, alarmDisplayEnabled *bool) error { + m.mu.Lock() + defer m.mu.Unlock() + + if loopCount > 0 { + m.loopCount = normalizeLoopCount(loopCount) + g.GVA_Config.DeviceInfo.LoopCount = m.loopCount + } + if alarmDisplayEnabled != nil { + m.alarmDisplayEnabled = *alarmDisplayEnabled + } if err := m.saveToFile(); err != nil { return err } - g.GVA_Config.DeviceInfo.LoopCount = loopCount return nil } @@ -54,6 +81,7 @@ func (m *ScanConfigManager) loadFromFile() error { if err != nil { if os.IsNotExist(err) { m.loopCount = normalizeLoopCount(g.Config().DeviceInfo.LoopCount) + m.alarmDisplayEnabled = defaultAlarmDisplayEnabled return m.saveToFile() } return err @@ -64,6 +92,11 @@ func (m *ScanConfigManager) loadFromFile() error { return err } m.loopCount = normalizeLoopCount(data.LoopCount) + if data.AlarmDisplayEnabled == nil { + m.alarmDisplayEnabled = defaultAlarmDisplayEnabled + } else { + m.alarmDisplayEnabled = *data.AlarmDisplayEnabled + } g.GVA_Config.DeviceInfo.LoopCount = m.loopCount return nil } @@ -76,7 +109,8 @@ func (m *ScanConfigManager) saveToFile() error { } jsonData, err := json.Marshal(ScanConfigData{ - LoopCount: normalizeLoopCount(m.loopCount), + LoopCount: normalizeLoopCount(m.loopCount), + AlarmDisplayEnabled: boolPtr(m.alarmDisplayEnabled), }) if err != nil { return err @@ -86,32 +120,43 @@ func (m *ScanConfigManager) saveToFile() error { func getScanConfig(rpcConn towgo.JsonRpcConnection) { rpcConn.WriteResult(map[string]interface{}{ - "loop_count": scanConfigManager.GetLoopCount(), - "max_loop_count": maxLoopCount, - "min_loop_count": 1, + "loop_count": scanConfigManager.GetLoopCount(), + "alarm_display_enabled": scanConfigManager.GetAlarmDisplayEnabled(), + "max_loop_count": maxLoopCount, + "min_loop_count": 1, }) } type SetScanConfigReq struct { - LoopCount int `json:"loop_count"` + LoopCount int `json:"loop_count"` + AlarmDisplayEnabled *bool `json:"alarm_display_enabled"` } func setScanConfig(rpcConn towgo.JsonRpcConnection) { var req SetScanConfigReq rpcConn.ReadParams(&req) - if req.LoopCount < 1 || req.LoopCount > maxLoopCount { + if req.LoopCount != 0 && (req.LoopCount < 1 || req.LoopCount > maxLoopCount) { rpcConn.WriteError(400, "loop_count must be between 1 and 10") return } - if err := scanConfigManager.SetLoopCount(req.LoopCount); err != nil { + if req.LoopCount == 0 && req.AlarmDisplayEnabled == nil { + rpcConn.WriteError(400, "loop_count or alarm_display_enabled is required") + return + } + if err := scanConfigManager.SetConfig(req.LoopCount, req.AlarmDisplayEnabled); err != nil { rpcConn.WriteError(500, "save scan config failed: "+err.Error()) return } rpcConn.WriteResult(map[string]interface{}{ - "success": true, - "loop_count": scanConfigManager.GetLoopCount(), - "max_loop_count": maxLoopCount, - "min_loop_count": 1, + "success": true, + "loop_count": scanConfigManager.GetLoopCount(), + "alarm_display_enabled": scanConfigManager.GetAlarmDisplayEnabled(), + "max_loop_count": maxLoopCount, + "min_loop_count": 1, }) } + +func boolPtr(value bool) *bool { + return &value +} diff --git a/internal/module/m9zTtyApi/scan_test.go b/internal/module/m9zTtyApi/scan_test.go index c32c8ba..ccd88a8 100644 --- a/internal/module/m9zTtyApi/scan_test.go +++ b/internal/module/m9zTtyApi/scan_test.go @@ -32,11 +32,14 @@ func TestMonitorDisplayLoopCountAlwaysUsesMaxLoopCount(t *testing.T) { func TestBuildDemoDeviceStatus2KeepsDisplayLoopsIndependentFromScanCount(t *testing.T) { scanConfigManager.mu.Lock() oldLoopCount := scanConfigManager.loopCount + oldAlarmDisplayEnabled := scanConfigManager.alarmDisplayEnabled scanConfigManager.loopCount = 6 + scanConfigManager.alarmDisplayEnabled = true scanConfigManager.mu.Unlock() t.Cleanup(func() { scanConfigManager.mu.Lock() scanConfigManager.loopCount = oldLoopCount + scanConfigManager.alarmDisplayEnabled = oldAlarmDisplayEnabled scanConfigManager.mu.Unlock() }) @@ -53,4 +56,29 @@ func TestBuildDemoDeviceStatus2KeepsDisplayLoopsIndependentFromScanCount(t *test if status.Loops[6].HasFault { t.Fatal("status.Loops[6].HasFault = true, want false outside the scan loop count") } + if !status.AlarmDisplayEnabled { + t.Fatal("status.AlarmDisplayEnabled = false, want true") + } +} + +func TestBuildDemoDeviceStatus2IncludesAlarmDisplayConfig(t *testing.T) { + scanConfigManager.mu.Lock() + oldAlarmDisplayEnabled := scanConfigManager.alarmDisplayEnabled + scanConfigManager.alarmDisplayEnabled = false + scanConfigManager.mu.Unlock() + t.Cleanup(func() { + scanConfigManager.mu.Lock() + scanConfigManager.alarmDisplayEnabled = oldAlarmDisplayEnabled + scanConfigManager.mu.Unlock() + }) + + status := buildDemoDeviceStatus2() + if status.AlarmDisplayEnabled { + t.Fatal("status.AlarmDisplayEnabled = true, want false") + } + for _, loop := range status.Loops { + if loop.HasFault || loop.FaultMsg != "" || len(loop.Faults) > 0 { + t.Fatalf("loop %d still has fault data when alarm display disabled", loop.Index) + } + } } diff --git a/internal/module/m9zTtyApi/set.go b/internal/module/m9zTtyApi/set.go index bf00298..78b8768 100644 --- a/internal/module/m9zTtyApi/set.go +++ b/internal/module/m9zTtyApi/set.go @@ -15,6 +15,21 @@ import ( // 写入设备编程模式 func setDeviceMode(rpc towgo.JsonRpcConnection) { + if automaticModeUseDemoAPI { + setDeviceModeDemo(rpc) + return + } + setDeviceModeReal(rpc) +} + +func setDeviceModeDemo(rpc towgo.JsonRpcConnection) { + var p automaticModeTaskReq + rpc.ReadParams(&p) + automaticModeDemo.setMode(p.Mode) + rpc.WriteResult("ok") +} + +func setDeviceModeReal(rpc towgo.JsonRpcConnection) { var p struct { CommUid string `json:"comm_uid"` Mode uint8 `json:"mode"` @@ -292,6 +307,30 @@ func setStopTaskWrite(rpc towgo.JsonRpcConnection) { // 写入开始任务 (优化指令 CMD=0x13,经纬度模式使用日落时间) func setStartTask2(rpc towgo.JsonRpcConnection) { + if automaticModeUseDemoAPI { + setStartTask2Demo(rpc) + return + } + setStartTask2Real(rpc) +} + +func setStartTask2Demo(rpc towgo.JsonRpcConnection) { + var p struct { + ExecTime string `json:"execTime"` + Mode uint8 `json:"mode"` + m9z.ControlTask + } + rpc.ReadParams(&p) + parse, err := utility.ParseTime(p.ExecTime) + if err != nil { + panic(err) + } + p.ControlTask.ExecTime = parse + automaticModeDemo.setStartTask(p.Mode, p.ControlTask) + rpc.WriteResult("ok") +} + +func setStartTask2Real(rpc towgo.JsonRpcConnection) { var p struct { ExecTime string `json:"execTime"` CommUid string `json:"comm_uid"` @@ -331,6 +370,30 @@ func setStartTask2(rpc towgo.JsonRpcConnection) { // 写入结束任务 (优化指令 CMD=0x15,经纬度模式使用日出时间) func setStopTask2(rpc towgo.JsonRpcConnection) { + if automaticModeUseDemoAPI { + setStopTask2Demo(rpc) + return + } + setStopTask2Real(rpc) +} + +func setStopTask2Demo(rpc towgo.JsonRpcConnection) { + var p struct { + ExecTime string `json:"execTime"` + Mode uint8 `json:"mode"` + m9z.ControlTask + } + rpc.ReadParams(&p) + parse, err := utility.ParseTime(p.ExecTime) + if err != nil { + panic(err) + } + p.ControlTask.ExecTime = parse + automaticModeDemo.setStopTask(p.Mode, p.ControlTask) + rpc.WriteResult("ok") +} + +func setStopTask2Real(rpc towgo.JsonRpcConnection) { var p struct { ExecTime string `json:"execTime"` CommUid string `json:"comm_uid"` @@ -371,6 +434,37 @@ func setStopTask2(rpc towgo.JsonRpcConnection) { } func setMiddleTask2(rpc towgo.JsonRpcConnection) { + if automaticModeUseDemoAPI { + setMiddleTask2Demo(rpc) + return + } + setMiddleTask2Real(rpc) +} + +func setMiddleTask2Demo(rpc towgo.JsonRpcConnection) { + var p struct { + Mode uint8 `json:"mode"` + ControlTaskList []struct { + m9z.ControlTask + ExecTime string `json:"execTime"` + } `json:"controlTaskList"` + } + rpc.ReadParams(&p) + middleTask := m9z.MiddleControlTask{} + middleTask.ControlTaskList = make([]m9z.ControlTask, len(p.ControlTaskList)) + for i, controlTask := range p.ControlTaskList { + parse, err := utility.ParseTime(controlTask.ExecTime) + if err != nil { + panic(gerror.NewCodef(gcode.CodeInvalidParameter, " %s parse control task time error, %s", controlTask.ExecTime, err)) + } + controlTask.ControlTask.ExecTime = parse + middleTask.ControlTaskList[i] = controlTask.ControlTask + } + automaticModeDemo.setMiddleTask(p.Mode, middleTask) + rpc.WriteResult("ok") +} + +func setMiddleTask2Real(rpc towgo.JsonRpcConnection) { var p struct { CommUid string `json:"comm_uid"` Mode uint8 `json:"mode"` diff --git a/需求.md b/需求.md index 11bb2db..6ad1bce 100644 --- a/需求.md +++ b/需求.md @@ -1,6 +1,61 @@ - -/Users/rice/Documents/300-code/310-projects/313-smertLamp/tgk-touch/apps/m9zCtrlTty/log1.png -/Users/rice/Documents/300-code/310-projects/313-smertLamp/tgk-touch/apps/m9zCtrlTty/log2.png -看一下这两个图片,现在那个我们的软件安到那个弯图的触摸屏上 -,第一次就是可以正常启动。然后我只要把那个触摸屏一重启,它就会卡在系统的logo上面进不去了。然后这两个图片是开发人员截的日志。 -他说有下载联网,下载什么东西?我记得我们安装呃火狐浏览器的都是离线安装包,没有这个联网下载的东西。然后我也看不懂这个日志啥的,你帮我分析一下,很着急。 +ebDriver BiDi listening on ws://127.0.0.1:18089 +2026-08-10 23:27:17.020 DEBUG /Users/rice/Documents/300-code/310-projects/313-smertLamp/tgk-touch/apps/m9zCtrlTty/kiosk_log.go:12 [touch-kiosk] firefox remote debug request url=http://127.0.0.1:18089/json/list +2026-08-10 23:27:17.675 WARN /Users/rice/Documents/300-code/310-projects/313-smertLamp/tgk-touch/apps/m9zCtrlTty/kiosk_log.go:16 [touch-kiosk] firefox remote debug bad status url=http://127.0.0.1:18089/json/list status=404 +2026-08-10 23:27:17.676 WARN /Users/rice/Documents/300-code/310-projects/313-smertLamp/tgk-touch/apps/m9zCtrlTty/kiosk_log.go:16 [touch-kiosk] firefox kiosk url check failed hasKioskURL=false err=firefox remote debugging returned status 404 +2026-08-10 23:27:17.677 DEBUG /Users/rice/Documents/300-code/310-projects/313-smertLamp/tgk-touch/apps/m9zCtrlTty/kiosk_log.go:12 [touch-kiosk] firefox health check pending during startup grace elapsed=6s err=firefox remote debugging returned status 404 +2026-08-10 23:27:22.023 DEBUG /Users/rice/Documents/300-code/310-projects/313-smertLamp/tgk-touch/apps/m9zCtrlTty/kiosk_log.go:12 [touch-kiosk] firefox remote debug request url=http://127.0.0.1:18089/json/list +2026-08-10 23:27:22.057 WARN /Users/rice/Documents/300-code/310-projects/313-smertLamp/tgk-touch/apps/m9zCtrlTty/kiosk_log.go:16 [touch-kiosk] firefox remote debug bad status url=http://127.0.0.1:18089/json/list status=404 +2026-08-10 23:27:22.058 WARN /Users/rice/Documents/300-code/310-projects/313-smertLamp/tgk-touch/apps/m9zCtrlTty/kiosk_log.go:16 [touch-kiosk] firefox kiosk url check failed hasKioskURL=false err=firefox remote debugging returned status 404 +2026-08-10 23:27:22.059 DEBUG /Users/rice/Documents/300-code/310-projects/313-smertLamp/tgk-touch/apps/m9zCtrlTty/kiosk_log.go:12 [touch-kiosk] firefox health check pending during startup grace elapsed=10s err=firefox remote debugging returned status 404 +[ERROR glean_core] Error setting metrics feature config: Json(Error("EOF while parsing a value", line: 1, column: 0)) +DevTools listening on ws://127.0.0.1:18089/devtools/browser/21bb3f75-cb31-419e-be27-cb315e2546ea +2026-08-10 23:27:27.020 DEBUG /Users/rice/Documents/300-code/310-projects/313-smertLamp/tgk-touch/apps/m9zCtrlTty/kiosk_log.go:12 [touch-kiosk] firefox remote debug request url=http://127.0.0.1:18089/json/list +2026-08-10 23:27:27.047 DEBUG /Users/rice/Documents/300-code/310-projects/313-smertLamp/tgk-touch/apps/m9zCtrlTty/kiosk_log.go:12 [touch-kiosk] firefox remote debug targets=1 expectedURL=http://127.0.0.1:8089 +2026-08-10 23:27:27.047 DEBUG /Users/rice/Documents/300-code/310-projects/313-smertLamp/tgk-touch/apps/m9zCtrlTty/kiosk_log.go:12 [touch-kiosk] firefox target type=page url=http://127.0.0.1:8089/menu +2026-08-10 23:27:27.048 DEBUG /Users/rice/Documents/300-code/310-projects/313-smertLamp/tgk-touch/apps/m9zCtrlTty/kiosk_log.go:12 [touch-kiosk] firefox kiosk url matched targetURL=http://127.0.0.1:8089/menu +2026-08-10 23:27:27.057 DEBUG /Users/rice/Documents/300-code/310-projects/313-smertLamp/tgk-touch/apps/m9zCtrlTty/kiosk_log.go:12 [touch-kiosk] command availability name=xdotool ok=false +2026-08-10 23:27:27.057 DEBUG /Users/rice/Documents/300-code/310-projects/313-smertLamp/tgk-touch/apps/m9zCtrlTty/kiosk_log.go:12 [touch-kiosk] fullscreen check skipped because xdotool/xprop unavailable +2026-08-10 23:27:27.057 DEBUG /Users/rice/Documents/300-code/310-projects/313-smertLamp/tgk-touch/apps/m9zCtrlTty/kiosk_log.go:12 [touch-kiosk] firefox health check ok elapsed=15s +2026-08-10 23:27:32.020 DEBUG /Users/rice/Documents/300-code/310-projects/313-smertLamp/tgk-touch/apps/m9zCtrlTty/kiosk_log.go:12 [touch-kiosk] firefox remote debug request url=http://127.0.0.1:18089/json/list +2026-08-10 23:27:32.070 DEBUG /Users/rice/Documents/300-code/310-projects/313-smertLamp/tgk-touch/apps/m9zCtrlTty/kiosk_log.go:12 [touch-kiosk] firefox remote debug targets=1 expectedURL=http://127.0.0.1:8089 +2026-08-10 23:27:32.074 DEBUG /Users/rice/Documents/300-code/310-projects/313-smertLamp/tgk-touch/apps/m9zCtrlTty/kiosk_log.go:12 [touch-kiosk] firefox target type=page url=http://127.0.0.1:8089/menu +2026-08-10 23:27:32.074 DEBUG /Users/rice/Documents/300-code/310-projects/313-smertLamp/tgk-touch/apps/m9zCtrlTty/kiosk_log.go:12 [touch-kiosk] firefox kiosk url matched targetURL=http://127.0.0.1:8089/menu +2026-08-10 23:27:32.081 DEBUG /Users/rice/Documents/300-code/310-projects/313-smertLamp/tgk-touch/apps/m9zCtrlTty/kiosk_log.go:12 [touch-kiosk] command availability name=xdotool ok=false +2026-08-10 23:27:32.082 DEBUG /Users/rice/Documents/300-code/310-projects/313-smertLamp/tgk-touch/apps/m9zCtrlTty/kiosk_log.go:12 [touch-kiosk] fullscreen check skipped because xdotool/xprop unavailable +2026-08-10 23:27:32.082 DEBUG /Users/rice/Documents/300-code/310-projects/313-smertLamp/tgk-touch/apps/m9zCtrlTty/kiosk_log.go:12 [touch-kiosk] firefox health check ok elapsed=20s +ALSA lib seq_hw.c:466:(snd_seq_hw_open) open /dev/snd/seq failed: No such file or directory +2026-08-10 23:27:37.020 DEBUG /Users/rice/Documents/300-code/310-projects/313-smertLamp/tgk-touch/apps/m9zCtrlTty/kiosk_log.go:12 [touch-kiosk] firefox remote debug request url=http://127.0.0.1:18089/json/list +2026-08-10 23:27:37.061 DEBUG /Users/rice/Documents/300-code/310-projects/313-smertLamp/tgk-touch/apps/m9zCtrlTty/kiosk_log.go:12 [touch-kiosk] firefox remote debug targets=1 expectedURL=http://127.0.0.1:8089 +2026-08-10 23:27:37.062 DEBUG /Users/rice/Documents/300-code/310-projects/313-smertLamp/tgk-touch/apps/m9zCtrlTty/kiosk_log.go:12 [touch-kiosk] firefox target type=page url=http://127.0.0.1:8089/menu +2026-08-10 23:27:37.062 DEBUG /Users/rice/Documents/300-code/310-projects/313-smertLamp/tgk-touch/apps/m9zCtrlTty/kiosk_log.go:12 [touch-kiosk] firefox kiosk url matched targetURL=http://127.0.0.1:8089/menu +2026-08-10 23:27:37.076 DEBUG /Users/rice/Documents/300-code/310-projects/313-smertLamp/tgk-touch/apps/m9zCtrlTty/kiosk_log.go:12 [touch-kiosk] command availability name=xdotool ok=false +2026-08-10 23:27:37.076 DEBUG /Users/rice/Documents/300-code/310-projects/313-smertLamp/tgk-touch/apps/m9zCtrlTty/kiosk_log.go:12 [touch-kiosk] fullscreen check skipped because xdotool/xprop unavailable +2026-08-10 23:27:37.077 DEBUG /Users/rice/Documents/300-code/310-projects/313-smertLamp/tgk-touch/apps/m9zCtrlTty/kiosk_log.go:12 [touch-kiosk] firefox health check ok elapsed=25s +2026-08-10 23:27:42.020 DEBUG /Users/rice/Documents/300-code/310-projects/313-smertLamp/tgk-touch/apps/m9zCtrlTty/kiosk_log.go:12 [touch-kiosk] firefox remote debug request url=http://127.0.0.1:18089/json/list +2026-08-10 23:27:42.052 DEBUG /Users/rice/Documents/300-code/310-projects/313-smertLamp/tgk-touch/apps/m9zCtrlTty/kiosk_log.go:12 [touch-kiosk] firefox remote debug targets=1 expectedURL=http://127.0.0.1:8089 +2026-08-10 23:27:42.065 DEBUG /Users/rice/Documents/300-code/310-projects/313-smertLamp/tgk-touch/apps/m9zCtrlTty/kiosk_log.go:12 [touch-kiosk] firefox target type=page url=http://127.0.0.1:8089/menu +2026-08-10 23:27:42.065 DEBUG /Users/rice/Documents/300-code/310-projects/313-smertLamp/tgk-touch/apps/m9zCtrlTty/kiosk_log.go:12 [touch-kiosk] firefox kiosk url matched targetURL=http://127.0.0.1:8089/menu +2026-08-10 23:27:42.074 DEBUG /Users/rice/Documents/300-code/310-projects/313-smertLamp/tgk-touch/apps/m9zCtrlTty/kiosk_log.go:12 [touch-kiosk] command availability name=xdotool ok=false +2026-08-10 23:27:42.074 DEBUG /Users/rice/Documents/300-code/310-projects/313-smertLamp/tgk-touch/apps/m9zCtrlTty/kiosk_log.go:12 [touch-kiosk] fullscreen check skipped because xdotool/xprop unavailable +2026-08-10 23:27:42.074 DEBUG /Users/rice/Documents/300-code/310-projects/313-smertLamp/tgk-touch/apps/m9zCtrlTty/kiosk_log.go:12 [touch-kiosk] firefox health check ok elapsed=30s +2026-08-10 23:27:47.021 DEBUG /Users/rice/Documents/300-code/310-projects/313-smertLamp/tgk-touch/apps/m9zCtrlTty/kiosk_log.go:12 [touch-kiosk] firefox remote debug request url=http://127.0.0.1:18089/json/list +2026-08-10 23:27:47.048 DEBUG /Users/rice/Documents/300-code/310-projects/313-smertLamp/tgk-touch/apps/m9zCtrlTty/kiosk_log.go:12 [touch-kiosk] firefox remote debug targets=1 expectedURL=http://127.0.0.1:8089 +2026-08-10 23:27:47.052 DEBUG /Users/rice/Documents/300-code/310-projects/313-smertLamp/tgk-touch/apps/m9zCtrlTty/kiosk_log.go:12 [touch-kiosk] firefox target type=page url=http://127.0.0.1:8089/menu +2026-08-10 23:27:47.053 DEBUG /Users/rice/Documents/300-code/310-projects/313-smertLamp/tgk-touch/apps/m9zCtrlTty/kiosk_log.go:12 [touch-kiosk] firefox kiosk url matched targetURL=http://127.0.0.1:8089/menu +2026-08-10 23:27:47.062 DEBUG /Users/rice/Documents/300-code/310-projects/313-smertLamp/tgk-touch/apps/m9zCtrlTty/kiosk_log.go:12 [touch-kiosk] command availability name=xdotool ok=false +2026-08-10 23:27:47.063 DEBUG /Users/rice/Documents/300-code/310-projects/313-smertLamp/tgk-touch/apps/m9zCtrlTty/kiosk_log.go:12 [touch-kiosk] fullscreen check skipped because xdotool/xprop unavailable +2026-08-10 23:27:47.063 DEBUG /Users/rice/Documents/300-code/310-projects/313-smertLamp/tgk-touch/apps/m9zCtrlTty/kiosk_log.go:12 [touch-kiosk] firefox health check ok elapsed=35s +2026-08-10 23:27:52.020 DEBUG /Users/rice/Documents/300-code/310-projects/313-smertLamp/tgk-touch/apps/m9zCtrlTty/kiosk_log.go:12 [touch-kiosk] firefox remote debug request url=http://127.0.0.1:18089/json/list +2026-08-10 23:27:52.047 DEBUG /Users/rice/Documents/300-code/310-projects/313-smertLamp/tgk-touch/apps/m9zCtrlTty/kiosk_log.go:12 [touch-kiosk] firefox remote debug targets=1 expectedURL=http://127.0.0.1:8089 +2026-08-10 23:27:52.049 DEBUG /Users/rice/Documents/300-code/310-projects/313-smertLamp/tgk-touch/apps/m9zCtrlTty/kiosk_log.go:12 [touch-kiosk] firefox target type=page url=http://127.0.0.1:8089/menu +2026-08-10 23:27:52.049 DEBUG /Users/rice/Documents/300-code/310-projects/313-smertLamp/tgk-touch/apps/m9zCtrlTty/kiosk_log.go:12 [touch-kiosk] firefox kiosk url matched targetURL=http://127.0.0.1:8089/menu +2026-08-10 23:27:52.061 DEBUG /Users/rice/Documents/300-code/310-projects/313-smertLamp/tgk-touch/apps/m9zCtrlTty/kiosk_log.go:12 [touch-kiosk] command availability name=xdotool ok=false +2026-08-10 23:27:52.062 DEBUG /Users/rice/Documents/300-code/310-projects/313-smertLamp/tgk-touch/apps/m9zCtrlTty/kiosk_log.go:12 [touch-kiosk] fullscreen check skipped because xdotool/xprop unavailable +2026-08-10 23:27:52.062 DEBUG /Users/rice/Documents/300-code/310-projects/313-smertLamp/tgk-touch/apps/m9zCtrlTty/kiosk_log.go:12 [touch-kiosk] firefox health check ok elapsed=40s +2026-08-10 23:27:57.020 DEBUG /Users/rice/Documents/300-code/310-projects/313-smertLamp/tgk-touch/apps/m9zCtrlTty/kiosk_log.go:12 [touch-kiosk] firefox remote debug request url=http://127.0.0.1:18089/json/list +2026-08-10 23:27:57.057 DEBUG /Users/rice/Documents/300-code/310-projects/313-smertLamp/tgk-touch/apps/m9zCtrlTty/kiosk_log.go:12 [touch-kiosk] firefox remote debug targets=1 expectedURL=http://127.0.0.1:8089 +2026-08-10 23:27:57.059 DEBUG /Users/rice/Documents/300-code/310-projects/313-smertLamp/tgk-touch/apps/m9zCtrlTty/kiosk_log.go:12 [touch-kiosk] firefox target type=page url=http://127.0.0.1:8089/menu +2026-08-10 23:27:57.059 DEBUG /Users/rice/Documents/300-code/310-projects/313-smertLamp/tgk-touch/apps/m9zCtrlTty/kiosk_log.go:12 [touch-kiosk] firefox kiosk url matched targetURL=http://127.0.0.1:8089/menu +2026-08-10 23:27:57.071 DEBUG /Users/rice/Documents/300-code/310-projects/313-smertLamp/tgk-touch/apps/m9zCtrlTty/kiosk_log.go:12 [touch-kiosk] command availability name=xdotool ok=false +2026-08-10 23:27:57.072 DEBUG /Users/rice/Documents/300-code/310-projects/313-smertLamp/tgk-touch/apps/m9zCtrlTty/kiosk_log.go:12 [touch-kiosk] fullscreen check skipped because xdotool/xprop unavailable +2026-08-10 23:27:57.072 DEBUG /Users/rice/Documents/300-code/310-projects/313-smertLamp/tgk-touch/apps/m9zCtrlTty/kiosk_log.go:12 [touch-kiosk] firefox health check ok elapsed=45s