From 3271a1796b58f0e85c8e278e332de0bdda2d9cd2 Mon Sep 17 00:00:00 2001 From: Cameron Fleming Date: Fri, 3 Jan 2025 11:23:32 +0000 Subject: [PATCH 01/26] feat(display.go): impl setDisplayBrightness() Implements setDisplayBrightness(brightness int) which allows setting the backlight brightness on JetKVM's hardware. Needs to be implemented into the RPC, config and frontend. --- display.go | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/display.go b/display.go index f312eb6..607e764 100644 --- a/display.go +++ b/display.go @@ -4,6 +4,9 @@ import ( "fmt" "log" "time" + "os" + "errors" + "strconv" ) var currentScreen = "ui_Boot_Screen" @@ -83,6 +86,29 @@ func updateStaticContents() { updateLabelIfChanged("ui_Status_Content_Device_Id_Content_Label", GetDeviceID()) } +// setDisplayBrightness sets /sys/class/backlight/backlight/brightness to alter +// the backlight brightness of the JetKVM hardware's display. +func setDisplayBrightness(brightness int) (error) { + if brightness > 100 || brightness < 0 { + return errors.New("brightness value out of bounds, must be between 0 and 100") + } + + // Check the display backlight class is available + if _, err := os.Stat("/sys/class/backlight/backlight/brightness"); errors.Is(err, os.ErrNotExist) { + return errors.New("brightness value cannot be set, possibly not running on JetKVM hardware.") + } + + // Set the value + bs := []byte(strconv.Itoa(brightness)) + err := os.WriteFile("/sys/class/backlight/backlight/brightness", bs, 0644) + if err != nil { + return err + } + + fmt.Print("display: set brightness to %v", brightness) + return nil +} + func init() { go func() { waitCtrlClientConnected() From 4fd8b1e6ff7d5a91a175214055e12abe7980ea48 Mon Sep 17 00:00:00 2001 From: Cameron Fleming Date: Fri, 3 Jan 2025 22:05:46 +0000 Subject: [PATCH 02/26] feat(config): add backlight control settings --- config.go | 30 ++++++++++++++++++------------ 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/config.go b/config.go index 1636434..1972a70 100644 --- a/config.go +++ b/config.go @@ -12,23 +12,29 @@ type WakeOnLanDevice struct { } type Config struct { - CloudURL string `json:"cloud_url"` - CloudToken string `json:"cloud_token"` - GoogleIdentity string `json:"google_identity"` - JigglerEnabled bool `json:"jiggler_enabled"` - AutoUpdateEnabled bool `json:"auto_update_enabled"` - IncludePreRelease bool `json:"include_pre_release"` - HashedPassword string `json:"hashed_password"` - LocalAuthToken string `json:"local_auth_token"` - LocalAuthMode string `json:"localAuthMode"` //TODO: fix it with migration - WakeOnLanDevices []WakeOnLanDevice `json:"wake_on_lan_devices"` + CloudURL string `json:"cloud_url"` + CloudToken string `json:"cloud_token"` + GoogleIdentity string `json:"google_identity"` + JigglerEnabled bool `json:"jiggler_enabled"` + AutoUpdateEnabled bool `json:"auto_update_enabled"` + IncludePreRelease bool `json:"include_pre_release"` + HashedPassword string `json:"hashed_password"` + LocalAuthToken string `json:"local_auth_token"` + LocalAuthMode string `json:"localAuthMode"` //TODO: fix it with migration + WakeOnLanDevices []WakeOnLanDevice `json:"wake_on_lan_devices"` + DisplayMaxBrightness int `json:"display_max_brightness"` + DisplayDimAfterMs int64 `json:"display_dim_after_ms"` + DisplayOffAfterMs int64 `json:"display_off_after_ms"` } const configPath = "/userdata/kvm_config.json" var defaultConfig = &Config{ - CloudURL: "https://api.jetkvm.com", - AutoUpdateEnabled: true, // Set a default value + CloudURL: "https://api.jetkvm.com", + AutoUpdateEnabled: true, // Set a default value + DisplayMaxBrightness: 100, + DisplayDimAfterMs: 0, + DisplayOffAfterMs: 0, } var config *Config From cd7258efd087eb8835b58802f438f1492364a822 Mon Sep 17 00:00:00 2001 From: Cameron Fleming Date: Fri, 3 Jan 2025 22:07:05 +0000 Subject: [PATCH 03/26] feat(display): add automatic dimming & switch off to display WIP, dims the display to 50% of the BacklightMaxBrightness after BacklightDimAfterMS expires. Turns the display off after BacklightOffAfterMS --- display.go | 72 +++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 68 insertions(+), 4 deletions(-) diff --git a/display.go b/display.go index 607e764..a6dd5f1 100644 --- a/display.go +++ b/display.go @@ -1,15 +1,17 @@ package kvm import ( + "errors" "fmt" "log" - "time" "os" - "errors" "strconv" + "time" ) var currentScreen = "ui_Boot_Screen" +var lastWakeTime = time.Now() +var backlightState = 0 // 0 - NORMAL, 1 - DIMMED, 2 - OFF func switchToScreen(screen string) { _, err := CallCtrlAction("lv_scr_load", map[string]interface{}{"obj": screen}) @@ -68,6 +70,7 @@ func requestDisplayUpdate() { return } go func() { + wakeDisplay() fmt.Println("display updating........................") //TODO: only run once regardless how many pending updates updateDisplay() @@ -88,7 +91,7 @@ func updateStaticContents() { // setDisplayBrightness sets /sys/class/backlight/backlight/brightness to alter // the backlight brightness of the JetKVM hardware's display. -func setDisplayBrightness(brightness int) (error) { +func setDisplayBrightness(brightness int) error { if brightness > 100 || brightness < 0 { return errors.New("brightness value out of bounds, must be between 0 and 100") } @@ -105,10 +108,58 @@ func setDisplayBrightness(brightness int) (error) { return err } - fmt.Print("display: set brightness to %v", brightness) + fmt.Printf("display: set brightness to %v", brightness) return nil } +// displayTimeoutTick checks the time the display was last woken, and compares that to the +// config's displayTimeout values to decide whether or not to dim/switch off the display. +func displayTimeoutTick() { + tn := time.Now() + td := tn.Sub(lastWakeTime).Milliseconds() + + // fmt.Printf("display: tick: time since wake: %vms, dim after: %v, off after: %v\n", td, config.DisplayDimAfterMs, config.DisplayOffAfterMs) + + if td > config.DisplayOffAfterMs && config.DisplayOffAfterMs != 0 && (backlightState == 1 || backlightState == 0) { + // Display fully off + + backlightState = 2 + err := setDisplayBrightness(0) + if err != nil { + fmt.Printf("display: timeout: Failed to switch off backlight: %s\n", err) + } + + } else if td > config.DisplayDimAfterMs && config.DisplayDimAfterMs != 0 && backlightState == 0 { + // Display dimming + + // Get 50% of max brightness, rounded up. + dimBright := config.DisplayMaxBrightness / 2 + fmt.Printf("display: timeout: target dim brightness: %v\n", dimBright) + + backlightState = 1 + err := setDisplayBrightness(dimBright) + if err != nil { + fmt.Printf("display: timeout: Failed to dim backlight: %s\n", err) + } + } +} + +// wakeDisplay sets the display brightness back to config.DisplayMaxBrightness and stores the time the display +// last woke, ready for displayTimeoutTick to put the display back in the dim/off states. +func wakeDisplay() { + if config.DisplayMaxBrightness == 0 { + config.DisplayMaxBrightness = 100 + } + + err := setDisplayBrightness(config.DisplayMaxBrightness) + if err != nil { + fmt.Printf("display wake failed, %s\n", err) + } + + lastWakeTime = time.Now() + backlightState = 0 +} + func init() { go func() { waitCtrlClientConnected() @@ -119,4 +170,17 @@ func init() { fmt.Println("display inited") requestDisplayUpdate() }() + + go func() { + // Start display auto-sleeping ticker + ticker := time.NewTicker(1 * time.Second) + defer ticker.Stop() + + for { + select { + case <-ticker.C: + displayTimeoutTick() + } + } + }() } From bec1443fe6ca4204309e5c322e1affa5bc22f72c Mon Sep 17 00:00:00 2001 From: Cameron Fleming Date: Fri, 3 Jan 2025 22:07:21 +0000 Subject: [PATCH 04/26] feat(rpc): add methods to get and set BacklightSettings --- jsonrpc.go | 45 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/jsonrpc.go b/jsonrpc.go index 2ce5f18..738ee47 100644 --- a/jsonrpc.go +++ b/jsonrpc.go @@ -34,6 +34,12 @@ type JSONRPCEvent struct { Params interface{} `json:"params,omitempty"` } +type BacklightSettings struct { + MaxBrightness int `json:"max_brightness"` + DimAfter int `json:"dim_after"` + OffAfter int `json:"off_after"` +} + func writeJSONRPCResponse(response JSONRPCResponse, session *Session) { responseBytes, err := json.Marshal(response) if err != nil { @@ -219,6 +225,43 @@ func rpcTryUpdate() error { return nil } +func rpcSetBacklightSettings(data *BacklightSettings) error { + LoadConfig() + + blConfig := *data + + if blConfig.MaxBrightness > 100 || blConfig.MaxBrightness < 0 { + return fmt.Errorf("maxBrightness must be between 0 and 100") + } + + if blConfig.DimAfter < 0 { + return fmt.Errorf("dimAfter must be a positive integer") + } + + if blConfig.OffAfter < 0 { + return fmt.Errorf("offAfter must be a positive integer") + } + + config.DisplayMaxBrightness = blConfig.MaxBrightness + config.DisplayDimAfterMs = int64(blConfig.DimAfter) + config.DisplayOffAfterMs = int64(blConfig.OffAfter) + + if err := SaveConfig(); err != nil { + return fmt.Errorf("failed to save config: %w", err) + } + return nil +} + +func rpcGetBacklightSettings() (*BacklightSettings, error) { + LoadConfig() + + return &BacklightSettings{ + MaxBrightness: config.DisplayMaxBrightness, + DimAfter: int(config.DisplayDimAfterMs), + OffAfter: int(config.DisplayOffAfterMs), + }, nil +} + const ( devModeFile = "/userdata/jetkvm/devmode.enable" sshKeyDir = "/userdata/dropbear/.ssh" @@ -554,4 +597,6 @@ var rpcHandlers = map[string]RPCHandler{ "getWakeOnLanDevices": {Func: rpcGetWakeOnLanDevices}, "setWakeOnLanDevices": {Func: rpcSetWakeOnLanDevices, Params: []string{"params"}}, "resetConfig": {Func: rpcResetConfig}, + "setBacklightSettings": {Func: rpcSetBacklightSettings, Params: []string{"settings"}}, + "getBacklightSettings": {Func: rpcGetBacklightSettings}, } From f4d88c716203b3d4d75de5639b85066186b05e8c Mon Sep 17 00:00:00 2001 From: Cameron Fleming Date: Fri, 3 Jan 2025 22:07:34 +0000 Subject: [PATCH 05/26] WIP: feat(settings): add Max backlight setting --- ui/src/components/sidebar/settings.tsx | 53 ++++++++++++++++++++++++++ ui/src/hooks/stores.ts | 16 ++++++++ 2 files changed, 69 insertions(+) diff --git a/ui/src/components/sidebar/settings.tsx b/ui/src/components/sidebar/settings.tsx index ec606a6..a02af45 100644 --- a/ui/src/components/sidebar/settings.tsx +++ b/ui/src/components/sidebar/settings.tsx @@ -1,5 +1,6 @@ import SidebarHeader from "@components/SidebarHeader"; import { + BacklightSettings, useLocalAuthModalStore, useSettingsStore, useUiStore, @@ -95,6 +96,7 @@ export default function SettingsSidebar() { const hideCursor = useSettingsStore(state => state.isCursorHidden); const setHideCursor = useSettingsStore(state => state.setCursorVisibility); const setDeveloperMode = useSettingsStore(state => state.setDeveloperMode); + const setBacklightSettings = useSettingsStore(state => state.setBacklightSettings); const [currentVersions, setCurrentVersions] = useState<{ appVersion: string; @@ -228,6 +230,18 @@ export default function SettingsSidebar() { [send, setDeveloperMode], ); + const handleBacklightSettingChange = useCallback((settings: BacklightSettings) => { + send("setBacklightSettings", { settings }, resp => { + if ("error" in resp) { + notifications.error( + `Failed to set backlight settings: ${resp.error.data || "Unknown error"}`, + ); + return; + } + notifications.success("Backlight settings updated successfully"); + }); + }, [send]); + const handleUpdateSSHKey = useCallback(() => { send("setSSHKeyState", { sshKey }, resp => { if ("error" in resp) { @@ -302,6 +316,17 @@ export default function SettingsSidebar() { } }); + send("getBacklightSettings", {}, resp => { + if ("error" in resp) { + notifications.error( + `Failed to get backlight settings: ${resp.error.data || "Unknown error"}`, + ); + return; + } + const result = resp.result as BacklightSettings; + setBacklightSettings(result); + }) + send("getDevModeState", {}, resp => { if ("error" in resp) return; const result = resp.result as { enabled: boolean }; @@ -797,6 +822,34 @@ export default function SettingsSidebar() { />
+
+ +
+ + {/* TODO: Allow the user to pick any value between 0 and 100 */} + { + handleBacklightSettingChange({ + max_brightness: parseInt(e.target.value), + dim_after: settings.backlightSettings.dim_after, + off_after: settings.backlightSettings.off_after, + }); + }} + /> + +
void; } +export interface BacklightSettings { + max_brightness: number; + dim_after: number; + off_after: number; +} + export const useVideoStore = create(set => ({ width: 0, height: 0, @@ -270,6 +276,9 @@ interface SettingsState { // Add new developer mode state developerMode: boolean; setDeveloperMode: (enabled: boolean) => void; + + backlightSettings: BacklightSettings; + setBacklightSettings: (settings: BacklightSettings) => void; } export const useSettingsStore = create( @@ -287,6 +296,13 @@ export const useSettingsStore = create( // Add developer mode with default value developerMode: false, setDeveloperMode: enabled => set({ developerMode: enabled }), + + backlightSettings: { + max_brightness: 100, + dim_after: 10000, + off_after: 50000, + }, + setBacklightSettings: (settings: BacklightSettings) => set({ backlightSettings: settings }), }), { name: "settings", From db4c0c71366b248e8bb2da07ee031041db301bbf Mon Sep 17 00:00:00 2001 From: Cameron Fleming Date: Sat, 4 Jan 2025 22:22:06 +0000 Subject: [PATCH 06/26] chore: use constant for backlight control file --- display.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/display.go b/display.go index a6dd5f1..ebf6533 100644 --- a/display.go +++ b/display.go @@ -13,6 +13,10 @@ var currentScreen = "ui_Boot_Screen" var lastWakeTime = time.Now() var backlightState = 0 // 0 - NORMAL, 1 - DIMMED, 2 - OFF +const ( + BACKLIGHT_CONTROL_CLASS string = "/sys/class/backlight/backlight/brightness" +) + func switchToScreen(screen string) { _, err := CallCtrlAction("lv_scr_load", map[string]interface{}{"obj": screen}) if err != nil { @@ -97,13 +101,13 @@ func setDisplayBrightness(brightness int) error { } // Check the display backlight class is available - if _, err := os.Stat("/sys/class/backlight/backlight/brightness"); errors.Is(err, os.ErrNotExist) { + if _, err := os.Stat(BACKLIGHT_CONTROL_CLASS); errors.Is(err, os.ErrNotExist) { return errors.New("brightness value cannot be set, possibly not running on JetKVM hardware.") } // Set the value bs := []byte(strconv.Itoa(brightness)) - err := os.WriteFile("/sys/class/backlight/backlight/brightness", bs, 0644) + err := os.WriteFile(BACKLIGHT_CONTROL_CLASS, bs, 0644) if err != nil { return err } From a267bb3a1d74f2f3f4751056afe170a0c72b26dd Mon Sep 17 00:00:00 2001 From: Cameron Fleming Date: Sat, 4 Jan 2025 22:22:29 +0000 Subject: [PATCH 07/26] fix: only attempt to wake the display if it's off --- display.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/display.go b/display.go index ebf6533..578f3b0 100644 --- a/display.go +++ b/display.go @@ -151,6 +151,10 @@ func displayTimeoutTick() { // wakeDisplay sets the display brightness back to config.DisplayMaxBrightness and stores the time the display // last woke, ready for displayTimeoutTick to put the display back in the dim/off states. func wakeDisplay() { + if backlightState == 0 { + return + } + if config.DisplayMaxBrightness == 0 { config.DisplayMaxBrightness = 100 } From 74cdeca2307685cee6cb5791a882ff766ea76c3a Mon Sep 17 00:00:00 2001 From: Cameron Fleming Date: Sat, 4 Jan 2025 22:22:50 +0000 Subject: [PATCH 08/26] feat(display): wake on touch --- display.go | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/display.go b/display.go index 578f3b0..7aaea7c 100644 --- a/display.go +++ b/display.go @@ -14,6 +14,7 @@ var lastWakeTime = time.Now() var backlightState = 0 // 0 - NORMAL, 1 - DIMMED, 2 - OFF const ( + TOUCHSCREEN_DEVICE string = "/dev/input/event1" BACKLIGHT_CONTROL_CLASS string = "/sys/class/backlight/backlight/brightness" ) @@ -168,6 +169,34 @@ func wakeDisplay() { backlightState = 0 } +// watchTsEvents monitors the touchscreen for events and simply calls wakeDisplay() to ensure the +// touchscreen interface still works even with LCD dimming/off. +// TODO: This is quite a hack, really we should be getting an event from jetkvm_native, or the whole display backlight +// control should be hoisted up to jetkvm_native. +func watchTsEvents() { + // Open touchscreen device + ts, err := os.OpenFile(TOUCHSCREEN_DEVICE, os.O_RDONLY, 0666) + if err != nil { + fmt.Printf("display: failed to open touchscreen device: %s\n", err) + return + } + + defer ts.Close() + + // Watch for events + for { + buf := make([]byte, 24) + _, err := ts.Read(buf) + if err != nil { + fmt.Printf("display: failed to read from touchscreen device: %s\n", err) + return + } + + // Touchscreen event, wake the display + wakeDisplay() + } +} + func init() { go func() { waitCtrlClientConnected() @@ -191,4 +220,6 @@ func init() { } } }() + + go watchTsEvents() } From d6e4df21095cfa1e76be87291220918dd9670570 Mon Sep 17 00:00:00 2001 From: Cameron Fleming Date: Sat, 4 Jan 2025 22:26:39 +0000 Subject: [PATCH 09/26] fix: re-use buffer between reads --- display.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/display.go b/display.go index 7aaea7c..86245f5 100644 --- a/display.go +++ b/display.go @@ -184,8 +184,8 @@ func watchTsEvents() { defer ts.Close() // Watch for events + buf := make([]byte, 24) for { - buf := make([]byte, 24) _, err := ts.Read(buf) if err != nil { fmt.Printf("display: failed to read from touchscreen device: %s\n", err) From 7e7310b1767229712e031b0e67fcb305ad94dde6 Mon Sep 17 00:00:00 2001 From: Cameron Fleming Date: Sat, 4 Jan 2025 22:27:10 +0000 Subject: [PATCH 10/26] fix: wakeDisplay() on start to fix warm start issue If the application had turned off the display before exiting, it wouldn't be brought on when the application restarted without a device reboot. --- display.go | 1 + 1 file changed, 1 insertion(+) diff --git a/display.go b/display.go index 86245f5..fc75849 100644 --- a/display.go +++ b/display.go @@ -205,6 +205,7 @@ func init() { updateStaticContents() displayInited = true fmt.Println("display inited") + wakeDisplay() requestDisplayUpdate() }() From 1fe71da77dc519a2cb4b8351106cdf15ebd3df3e Mon Sep 17 00:00:00 2001 From: Cameron Fleming Date: Sun, 5 Jan 2025 02:24:53 +0000 Subject: [PATCH 11/26] chore: various comment & string updates --- display.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/display.go b/display.go index fc75849..ab39516 100644 --- a/display.go +++ b/display.go @@ -97,13 +97,15 @@ func updateStaticContents() { // setDisplayBrightness sets /sys/class/backlight/backlight/brightness to alter // the backlight brightness of the JetKVM hardware's display. func setDisplayBrightness(brightness int) error { + // NOTE: The actual maximum value for this is 255, but out-of-the-box, the value is set to 64. + // The maximum set here is set to 100 to reduce the risk of drawing too much power (and besides, 255 is very bright!). if brightness > 100 || brightness < 0 { return errors.New("brightness value out of bounds, must be between 0 and 100") } // Check the display backlight class is available if _, err := os.Stat(BACKLIGHT_CONTROL_CLASS); errors.Is(err, os.ErrNotExist) { - return errors.New("brightness value cannot be set, possibly not running on JetKVM hardware.") + return errors.New("brightness value cannot be set, possibly not running on JetKVM hardware") } // Set the value @@ -123,8 +125,6 @@ func displayTimeoutTick() { tn := time.Now() td := tn.Sub(lastWakeTime).Milliseconds() - // fmt.Printf("display: tick: time since wake: %vms, dim after: %v, off after: %v\n", td, config.DisplayDimAfterMs, config.DisplayOffAfterMs) - if td > config.DisplayOffAfterMs && config.DisplayOffAfterMs != 0 && (backlightState == 1 || backlightState == 0) { // Display fully off @@ -174,7 +174,6 @@ func wakeDisplay() { // TODO: This is quite a hack, really we should be getting an event from jetkvm_native, or the whole display backlight // control should be hoisted up to jetkvm_native. func watchTsEvents() { - // Open touchscreen device ts, err := os.OpenFile(TOUCHSCREEN_DEVICE, os.O_RDONLY, 0666) if err != nil { fmt.Printf("display: failed to open touchscreen device: %s\n", err) @@ -183,7 +182,9 @@ func watchTsEvents() { defer ts.Close() - // Watch for events + // This buffer is set to 24 bytes as that's the normal size of events on /dev/input + // Reference: https://www.kernel.org/doc/Documentation/input/input.txt + // This could potentially be set higher, to require multiple events to wake the display. buf := make([]byte, 24) for { _, err := ts.Read(buf) @@ -192,7 +193,6 @@ func watchTsEvents() { return } - // Touchscreen event, wake the display wakeDisplay() } } From e9b539053bd9956f121884d7d25c93a724a4c2fd Mon Sep 17 00:00:00 2001 From: Cameron Fleming Date: Sun, 5 Jan 2025 21:13:09 +0000 Subject: [PATCH 12/26] fix: newline on set brightness log Noticed by @eric https://github.com/jetkvm/kvm/pull/17#discussion_r1903338705 --- display.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/display.go b/display.go index ab39516..9982357 100644 --- a/display.go +++ b/display.go @@ -115,7 +115,7 @@ func setDisplayBrightness(brightness int) error { return err } - fmt.Printf("display: set brightness to %v", brightness) + fmt.Printf("display: set brightness to %v\n", brightness) return nil } From daaddefe5105ff8b110a9b3946983c9894b45ec0 Mon Sep 17 00:00:00 2001 From: Cameron Fleming Date: Mon, 20 Jan 2025 18:38:01 +0000 Subject: [PATCH 13/26] fix: set default value for display Set the DisplayMaxBrightness to the default brightness used out-of-the-box by JetKVM. Also sets the dim/timeout to 2 minutes and 30 mintes respectively. --- config.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/config.go b/config.go index 1972a70..84034d7 100644 --- a/config.go +++ b/config.go @@ -32,9 +32,9 @@ const configPath = "/userdata/kvm_config.json" var defaultConfig = &Config{ CloudURL: "https://api.jetkvm.com", AutoUpdateEnabled: true, // Set a default value - DisplayMaxBrightness: 100, - DisplayDimAfterMs: 0, - DisplayOffAfterMs: 0, + DisplayMaxBrightness: 64, + DisplayDimAfterMs: 120000, // 2 minutes + DisplayOffAfterMs: 1800000, // 30 minutes } var config *Config From 79bac39b6b40f8e4ac72bd7aea0880366fe3a799 Mon Sep 17 00:00:00 2001 From: Cameron Fleming Date: Mon, 20 Jan 2025 19:51:52 +0000 Subject: [PATCH 14/26] feat(display.go): use tickers to countdown to dim/off As suggested by tutman in https://github.com/jetkvm/kvm/pull/17, use tickers set to the duration of dim/off to avoid a loop running every second. The tickers are reset to the dim/off times whenever wakeDisplay() is called. --- display.go | 73 +++++++++++++++++++++++++++++------------------------- 1 file changed, 39 insertions(+), 34 deletions(-) diff --git a/display.go b/display.go index 9982357..059b180 100644 --- a/display.go +++ b/display.go @@ -10,9 +10,11 @@ import ( ) var currentScreen = "ui_Boot_Screen" -var lastWakeTime = time.Now() var backlightState = 0 // 0 - NORMAL, 1 - DIMMED, 2 - OFF +var dim_ticker *time.Ticker +var off_ticker *time.Ticker + const ( TOUCHSCREEN_DEVICE string = "/dev/input/event1" BACKLIGHT_CONTROL_CLASS string = "/sys/class/backlight/backlight/brightness" @@ -119,34 +121,30 @@ func setDisplayBrightness(brightness int) error { return nil } -// displayTimeoutTick checks the time the display was last woken, and compares that to the -// config's displayTimeout values to decide whether or not to dim/switch off the display. -func displayTimeoutTick() { - tn := time.Now() - td := tn.Sub(lastWakeTime).Milliseconds() - - if td > config.DisplayOffAfterMs && config.DisplayOffAfterMs != 0 && (backlightState == 1 || backlightState == 0) { - // Display fully off - - backlightState = 2 - err := setDisplayBrightness(0) - if err != nil { - fmt.Printf("display: timeout: Failed to switch off backlight: %s\n", err) - } - - } else if td > config.DisplayDimAfterMs && config.DisplayDimAfterMs != 0 && backlightState == 0 { - // Display dimming - - // Get 50% of max brightness, rounded up. - dimBright := config.DisplayMaxBrightness / 2 - fmt.Printf("display: timeout: target dim brightness: %v\n", dimBright) - - backlightState = 1 - err := setDisplayBrightness(dimBright) - if err != nil { - fmt.Printf("display: timeout: Failed to dim backlight: %s\n", err) - } +// tick_displayDim() is called when when dim ticker expires, it simply reduces the brightness +// of the display by half of the max brightness. +func tick_displayDim() { + err := setDisplayBrightness(config.DisplayMaxBrightness / 2) + if err != nil { + fmt.Printf("display: failed to dim display: %s\n", err) } + + dim_ticker.Stop() + + backlightState = 1 +} + +// tick_displayOff() is called when the off ticker expires, it turns off the display +// by setting the brightness to zero. +func tick_displayOff() { + err := setDisplayBrightness(0) + if err != nil { + fmt.Printf("display: failed to turn off display: %s\n", err) + } + + off_ticker.Stop() + + backlightState = 2 } // wakeDisplay sets the display brightness back to config.DisplayMaxBrightness and stores the time the display @@ -165,7 +163,8 @@ func wakeDisplay() { fmt.Printf("display wake failed, %s\n", err) } - lastWakeTime = time.Now() + dim_ticker.Reset(time.Duration(config.DisplayDimAfterMs) * time.Millisecond) + off_ticker.Reset(time.Duration(config.DisplayOffAfterMs) * time.Millisecond) backlightState = 0 } @@ -210,14 +209,20 @@ func init() { }() go func() { - // Start display auto-sleeping ticker - ticker := time.NewTicker(1 * time.Second) - defer ticker.Stop() + LoadConfig() + // Start display auto-sleeping tickers + dim_ticker = time.NewTicker(time.Duration(config.DisplayDimAfterMs) * time.Millisecond) + defer dim_ticker.Stop() + + off_ticker = time.NewTicker(time.Duration(config.DisplayOffAfterMs) * time.Millisecond) + defer off_ticker.Stop() for { select { - case <-ticker.C: - displayTimeoutTick() + case <-dim_ticker.C: + tick_displayDim() + case <-off_ticker.C: + tick_displayOff() } } }() From 34e42fd37c045ef9f36f031c2dd1ee5414f65dbb Mon Sep 17 00:00:00 2001 From: Cameron Fleming Date: Mon, 27 Jan 2025 20:46:54 +0000 Subject: [PATCH 15/26] chore: update config Changed Dim & Off values to seconds instead of milliseconds, there's no need for it to be that precise. --- config.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/config.go b/config.go index 84034d7..4230cfb 100644 --- a/config.go +++ b/config.go @@ -23,8 +23,8 @@ type Config struct { LocalAuthMode string `json:"localAuthMode"` //TODO: fix it with migration WakeOnLanDevices []WakeOnLanDevice `json:"wake_on_lan_devices"` DisplayMaxBrightness int `json:"display_max_brightness"` - DisplayDimAfterMs int64 `json:"display_dim_after_ms"` - DisplayOffAfterMs int64 `json:"display_off_after_ms"` + DisplayDimAfterSec int `json:"display_dim_after_sec"` + DisplayOffAfterSec int `json:"display_off_after_sec"` } const configPath = "/userdata/kvm_config.json" @@ -33,8 +33,8 @@ var defaultConfig = &Config{ CloudURL: "https://api.jetkvm.com", AutoUpdateEnabled: true, // Set a default value DisplayMaxBrightness: 64, - DisplayDimAfterMs: 120000, // 2 minutes - DisplayOffAfterMs: 1800000, // 30 minutes + DisplayDimAfterSec: 120, // 2 minutes + DisplayOffAfterSec: 1800, // 30 minutes } var config *Config From e9f140c735006cf4f05717195bbdcea454919949 Mon Sep 17 00:00:00 2001 From: Cameron Fleming Date: Mon, 27 Jan 2025 20:48:27 +0000 Subject: [PATCH 16/26] feat(display.go): wakeDisplay() force Adds the force boolean to wakedisplay() which allows skipping the backlightState == 0 check, this means you can force a ticker reset, even if the display is currently in the "full bright" state --- display.go | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/display.go b/display.go index 059b180..4cb619f 100644 --- a/display.go +++ b/display.go @@ -77,7 +77,7 @@ func requestDisplayUpdate() { return } go func() { - wakeDisplay() + wakeDisplay(false) fmt.Println("display updating........................") //TODO: only run once regardless how many pending updates updateDisplay() @@ -149,15 +149,12 @@ func tick_displayOff() { // wakeDisplay sets the display brightness back to config.DisplayMaxBrightness and stores the time the display // last woke, ready for displayTimeoutTick to put the display back in the dim/off states. -func wakeDisplay() { - if backlightState == 0 { +// Set force to true to skip the backlight state check, this should be done if altering the tickers. +func wakeDisplay(force bool) { + if backlightState == 0 && !force { return } - if config.DisplayMaxBrightness == 0 { - config.DisplayMaxBrightness = 100 - } - err := setDisplayBrightness(config.DisplayMaxBrightness) if err != nil { fmt.Printf("display wake failed, %s\n", err) @@ -192,7 +189,7 @@ func watchTsEvents() { return } - wakeDisplay() + wakeDisplay(false) } } @@ -204,7 +201,7 @@ func init() { updateStaticContents() displayInited = true fmt.Println("display inited") - wakeDisplay() + wakeDisplay(false) requestDisplayUpdate() }() From 7d1777985fcebec6408c1b3999a86a19fb55c83b Mon Sep 17 00:00:00 2001 From: Cameron Fleming Date: Mon, 27 Jan 2025 20:49:10 +0000 Subject: [PATCH 17/26] feat(display.go): move tickers into their own method This allows them to only be started if necessary. If the user has chosen to keep the display on and not-dimmed all the time, the tickers can't start as their tick value must be a positive integer. --- display.go | 55 ++++++++++++++++++++++++++++++++++++------------------ 1 file changed, 37 insertions(+), 18 deletions(-) diff --git a/display.go b/display.go index 4cb619f..4179250 100644 --- a/display.go +++ b/display.go @@ -193,6 +193,42 @@ func watchTsEvents() { } } +// startBacklightTickers starts the two tickers for dimming and switching off the display +// if they're not already set. This is done separately to the init routine as the "never dim" +// option has the value set to zero, but time.NewTicker only accept positive values. +func startBacklightTickers() { + LoadConfig() + if dim_ticker == nil && config.DisplayDimAfterSec != 0 { + fmt.Printf("display: dim_ticker has started.") + dim_ticker = time.NewTicker(time.Duration(config.DisplayDimAfterSec) * time.Second) + defer dim_ticker.Stop() + + go func() { + for { + select { + case <-dim_ticker.C: + tick_displayDim() + } + } + }() + } + + if off_ticker == nil && config.DisplayOffAfterSec != 0 { + fmt.Printf("display: off_ticker has started.") + off_ticker = time.NewTicker(time.Duration(config.DisplayOffAfterSec) * time.Second) + defer off_ticker.Stop() + + go func() { + for { + select { + case <-off_ticker.C: + tick_displayOff() + } + } + }() + } +} + func init() { go func() { waitCtrlClientConnected() @@ -205,24 +241,7 @@ func init() { requestDisplayUpdate() }() - go func() { - LoadConfig() - // Start display auto-sleeping tickers - dim_ticker = time.NewTicker(time.Duration(config.DisplayDimAfterMs) * time.Millisecond) - defer dim_ticker.Stop() - - off_ticker = time.NewTicker(time.Duration(config.DisplayOffAfterMs) * time.Millisecond) - defer off_ticker.Stop() - - for { - select { - case <-dim_ticker.C: - tick_displayDim() - case <-off_ticker.C: - tick_displayOff() - } - } - }() + startBacklightTickers() go watchTsEvents() } From cabe5b07ab4538f5c702efeff914a640ef61a5e0 Mon Sep 17 00:00:00 2001 From: Cameron Fleming Date: Mon, 27 Jan 2025 20:49:27 +0000 Subject: [PATCH 18/26] feat(display.go): stop tickers when auto-dim/auto-off is disabled --- display.go | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/display.go b/display.go index 4179250..f8f9a5e 100644 --- a/display.go +++ b/display.go @@ -160,8 +160,17 @@ func wakeDisplay(force bool) { fmt.Printf("display wake failed, %s\n", err) } - dim_ticker.Reset(time.Duration(config.DisplayDimAfterMs) * time.Millisecond) - off_ticker.Reset(time.Duration(config.DisplayOffAfterMs) * time.Millisecond) + if config.DisplayDimAfterSec == 0 { + dim_ticker.Stop() + } else { + dim_ticker.Reset(time.Duration(config.DisplayDimAfterSec) * time.Second) + } + + if config.DisplayOffAfterSec == 0 { + off_ticker.Stop() + } else { + off_ticker.Reset(time.Duration(config.DisplayOffAfterSec) * time.Second) + } backlightState = 0 } From 309d30d3c366c63ea24ae45e9d1e85b083b06979 Mon Sep 17 00:00:00 2001 From: Cameron Fleming Date: Mon, 27 Jan 2025 20:49:41 +0000 Subject: [PATCH 19/26] feat(rpc): implement display backlight control methods --- jsonrpc.go | 33 +++++++++++++++++++++++---------- 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/jsonrpc.go b/jsonrpc.go index 738ee47..349ad83 100644 --- a/jsonrpc.go +++ b/jsonrpc.go @@ -225,13 +225,14 @@ func rpcTryUpdate() error { return nil } -func rpcSetBacklightSettings(data *BacklightSettings) error { +func rpcSetBacklightSettings(params BacklightSettings) error { LoadConfig() - blConfig := *data + blConfig := params - if blConfig.MaxBrightness > 100 || blConfig.MaxBrightness < 0 { - return fmt.Errorf("maxBrightness must be between 0 and 100") + // NOTE: by default, the frontend limits the brightness to 64, as that's what the device originally shipped with. + if blConfig.MaxBrightness > 255 || blConfig.MaxBrightness < 0 { + return fmt.Errorf("maxBrightness must be between 0 and 255") } if blConfig.DimAfter < 0 { @@ -243,12 +244,24 @@ func rpcSetBacklightSettings(data *BacklightSettings) error { } config.DisplayMaxBrightness = blConfig.MaxBrightness - config.DisplayDimAfterMs = int64(blConfig.DimAfter) - config.DisplayOffAfterMs = int64(blConfig.OffAfter) + config.DisplayDimAfterSec = blConfig.DimAfter + config.DisplayOffAfterSec = blConfig.OffAfter if err := SaveConfig(); err != nil { return fmt.Errorf("failed to save config: %w", err) } + + log.Printf("rpc: display: settings applied, max_brightness: %d, dim after: %ds, off after: %ds", config.DisplayMaxBrightness, config.DisplayDimAfterSec, config.DisplayOffAfterSec) + + // If the device started up with auto-dim and/or auto-off set to zero, the display init + // method will not have started the tickers. So in case that has changed, attempt to start the tickers now. + startBacklightTickers() + + // Wake the display after the settings are altered, this ensures the tickers + // are reset to the new settings, and will bring the display up to maxBrightness. + // Calling with force set to true, to ignore the current state of the display, and force + // it to reset the tickers. + wakeDisplay(true) return nil } @@ -257,8 +270,8 @@ func rpcGetBacklightSettings() (*BacklightSettings, error) { return &BacklightSettings{ MaxBrightness: config.DisplayMaxBrightness, - DimAfter: int(config.DisplayDimAfterMs), - OffAfter: int(config.DisplayOffAfterMs), + DimAfter: int(config.DisplayDimAfterSec), + OffAfter: int(config.DisplayOffAfterSec), }, nil } @@ -422,7 +435,7 @@ func callRPCHandler(handler RPCHandler, params map[string]interface{}) (interfac } args[i] = reflect.ValueOf(newStruct).Elem() } else { - return nil, fmt.Errorf("invalid parameter type for: %s", paramName) + return nil, fmt.Errorf("invalid parameter type for: %s, type: %s", paramName, paramType.Kind()) } } else { args[i] = convertedValue.Convert(paramType) @@ -597,6 +610,6 @@ var rpcHandlers = map[string]RPCHandler{ "getWakeOnLanDevices": {Func: rpcGetWakeOnLanDevices}, "setWakeOnLanDevices": {Func: rpcSetWakeOnLanDevices, Params: []string{"params"}}, "resetConfig": {Func: rpcResetConfig}, - "setBacklightSettings": {Func: rpcSetBacklightSettings, Params: []string{"settings"}}, + "setBacklightSettings": {Func: rpcSetBacklightSettings, Params: []string{"params"}}, "getBacklightSettings": {Func: rpcGetBacklightSettings}, } From a6eab94e0da6bdb26fd9abbbc1c7ccf4be8a3e94 Mon Sep 17 00:00:00 2001 From: Cameron Fleming Date: Mon, 27 Jan 2025 20:49:47 +0000 Subject: [PATCH 20/26] feat(ui): implement display backlight control --- ui/src/components/sidebar/settings.tsx | 78 ++++++++++++++++++++++---- 1 file changed, 67 insertions(+), 11 deletions(-) diff --git a/ui/src/components/sidebar/settings.tsx b/ui/src/components/sidebar/settings.tsx index a02af45..48212a5 100644 --- a/ui/src/components/sidebar/settings.tsx +++ b/ui/src/components/sidebar/settings.tsx @@ -230,8 +230,18 @@ export default function SettingsSidebar() { [send, setDeveloperMode], ); - const handleBacklightSettingChange = useCallback((settings: BacklightSettings) => { - send("setBacklightSettings", { settings }, resp => { + const handleBacklightSettingsChange = (settings: BacklightSettings) => { + // If the user has set the display to dim after it turns off, set the dim_after + // value to never. + if (settings.dim_after > settings.off_after && settings.off_after != 0) { + settings.dim_after = 0; + } + + setBacklightSettings(settings); + } + + const handleBacklightSettingsSave = () => { + send("setBacklightSettings", { params: settings.backlightSettings }, resp => { if ("error" in resp) { notifications.error( `Failed to set backlight settings: ${resp.error.data || "Unknown error"}`, @@ -240,7 +250,7 @@ export default function SettingsSidebar() { } notifications.success("Backlight settings updated successfully"); }); - }, [send]); + }; const handleUpdateSSHKey = useCallback(() => { send("setSSHKeyState", { sshKey }, resp => { @@ -829,7 +839,6 @@ export default function SettingsSidebar() { />
- {/* TODO: Allow the user to pick any value between 0 and 100 */} { - handleBacklightSettingChange({ - max_brightness: parseInt(e.target.value), - dim_after: settings.backlightSettings.dim_after, - off_after: settings.backlightSettings.off_after, - }); + settings.backlightSettings.max_brightness = parseInt(e.target.value) + handleBacklightSettingsChange(settings.backlightSettings); }} /> + {settings.backlightSettings.max_brightness != 0 && ( + <> + + { + settings.backlightSettings.dim_after = parseInt(e.target.value) + handleBacklightSettingsChange(settings.backlightSettings); + }} + /> + + + { + settings.backlightSettings.off_after = parseInt(e.target.value) + handleBacklightSettingsChange(settings.backlightSettings); + }} + /> + + + )} +

+ The display will wake up when the connection state changes, or when touched. +

+