Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
3271a17
feat(display.go): impl setDisplayBrightness()
Nevexo Jan 3, 2025
4fd8b1e
feat(config): add backlight control settings
Nevexo Jan 3, 2025
cd7258e
feat(display): add automatic dimming & switch off to display
Nevexo Jan 3, 2025
bec1443
feat(rpc): add methods to get and set BacklightSettings
Nevexo Jan 3, 2025
f4d88c7
WIP: feat(settings): add Max backlight setting
Nevexo Jan 3, 2025
db4c0c7
chore: use constant for backlight control file
Nevexo Jan 4, 2025
a267bb3
fix: only attempt to wake the display if it's off
Nevexo Jan 4, 2025
74cdeca
feat(display): wake on touch
Nevexo Jan 4, 2025
d6e4df2
fix: re-use buffer between reads
Nevexo Jan 4, 2025
7e7310b
fix: wakeDisplay() on start to fix warm start issue
Nevexo Jan 4, 2025
1fe71da
chore: various comment & string updates
Nevexo Jan 5, 2025
e9b5390
fix: newline on set brightness log
Nevexo Jan 5, 2025
daaddef
fix: set default value for display
Nevexo Jan 20, 2025
79bac39
feat(display.go): use tickers to countdown to dim/off
Nevexo Jan 20, 2025
34e42fd
chore: update config
Nevexo Jan 27, 2025
e9f140c
feat(display.go): wakeDisplay() force
Nevexo Jan 27, 2025
7d17779
feat(display.go): move tickers into their own method
Nevexo Jan 27, 2025
cabe5b0
feat(display.go): stop tickers when auto-dim/auto-off is disabled
Nevexo Jan 27, 2025
309d30d
feat(rpc): implement display backlight control methods
Nevexo Jan 27, 2025
a6eab94
feat(ui): implement display backlight control
Nevexo Jan 27, 2025
a05df7a
chore: update variable names
Nevexo Jan 28, 2025
6445628
fix(display): move backlightTicker setup into screen setup goroutine
Nevexo Jan 28, 2025
f5035f2
chore: fix some start-up timing issues
Nevexo Jan 28, 2025
9896eba
fix(display): Don't attempt to start the tickers if the display is di…
Nevexo Jan 28, 2025
8071f81
fix: wakeDisplay() doesn't need to stop the tickers
Nevexo Jan 28, 2025
ce54d10
fix: Don't wake up the display if it's turned off
Nevexo Jan 28, 2025
e177fdb
Merge branch 'dev' into nevexo/display-brightness
Nevexo Feb 17, 2025
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
30 changes: 18 additions & 12 deletions config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
130 changes: 130 additions & 0 deletions display.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,22 @@
package kvm

import (
"errors"
"fmt"
"log"
"os"
"strconv"
"time"
)

var currentScreen = "ui_Boot_Screen"
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"

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Effective Go and the Go stdlib usually prefers lowerCamelCase/UpperCamelCase (depending on whether exported) for constant names. E.g., per https://go.dev/wiki/CodeReviewComments:

See https://go.dev/doc/effective_go#mixed-caps. This applies even when it breaks conventions in other languages. For example an unexported constant is maxLength not MaxLength or MAX_LENGTH.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! Updated.

)

func switchToScreen(screen string) {
_, err := CallCtrlAction("lv_scr_load", map[string]interface{}{"obj": screen})
Expand Down Expand Up @@ -65,6 +75,7 @@ func requestDisplayUpdate() {
return
}
go func() {
wakeDisplay()
fmt.Println("display updating........................")
//TODO: only run once regardless how many pending updates
updateDisplay()
Expand All @@ -83,6 +94,109 @@ 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(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(BACKLIGHT_CONTROL_CLASS, bs, 0644)
if err != nil {
return err
}

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 backlightState == 0 {
return
}

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
}

// 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
buf := make([]byte, 24)
for {
_, 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()
Expand All @@ -91,6 +205,22 @@ func init() {
updateStaticContents()
displayInited = true
fmt.Println("display inited")
wakeDisplay()
requestDisplayUpdate()
}()

go func() {
// Start display auto-sleeping ticker
ticker := time.NewTicker(1 * time.Second)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since you are already using a ticker, you might want to consider using https://pkg.go.dev/time#Ticker.Reset to just Reset it back to the DisplayDimAfterMs time whenever there is activity. Would be a bit more efficient and would mean you wouldn't need to have this no-op loop running once every second.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Neat, I'll do that.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tutman96 Okay, pushed this - please take a look when you get chance, I'm not super happy with having two methods that are called when the tickers fire (tick_displayDim and tick_displayOff) as they practically do the same job. I wonder if it'd be possible to keep the tickers in an array and figure out which one of them fired, to have one handler for them?

defer ticker.Stop()

for {
select {
case <-ticker.C:
displayTimeoutTick()
}
}
}()

go watchTsEvents()
}
45 changes: 45 additions & 0 deletions jsonrpc.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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},
}
53 changes: 53 additions & 0 deletions ui/src/components/sidebar/settings.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import SidebarHeader from "@components/SidebarHeader";
import {
BacklightSettings,
useLocalAuthModalStore,
useSettingsStore,
useUiStore,
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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 };
Expand Down Expand Up @@ -797,6 +822,34 @@ export default function SettingsSidebar() {
/>
</SettingsItem>
<div className="h-[1px] w-full bg-slate-800/10 dark:bg-slate-300/20" />
<div className="pb-2 space-y-4">
<SectionHeader
title="Hardware"
description="Configure the JetKVM Hardware"
/>
</div>
<SettingsItem title="Display Brightness" description="Set the brightness of the display">
{/* TODO: Allow the user to pick any value between 0 and 100 */}
<SelectMenuBasic
size="SM"
label=""
value={settings.backlightSettings.max_brightness.toString()}
options={[
{ value: "0", label: "Off" },
{ value: "10", label: "Low" },
{ value: "50", label: "Medium" },
{ value: "100", label: "High" },
]}
onChange={e => {
handleBacklightSettingChange({
max_brightness: parseInt(e.target.value),
dim_after: settings.backlightSettings.dim_after,
off_after: settings.backlightSettings.off_after,
});
}}
/>
</SettingsItem>
<div className="h-[1px] w-full bg-slate-800/10 dark:bg-slate-300/20" />
<div className="pb-2 space-y-4">
<SectionHeader
title="Advanced"
Expand Down
16 changes: 16 additions & 0 deletions ui/src/hooks/stores.ts
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,12 @@ export interface VideoState {
}) => void;
}

export interface BacklightSettings {
max_brightness: number;
dim_after: number;
off_after: number;
}

export const useVideoStore = create<VideoState>(set => ({
width: 0,
height: 0,
Expand Down Expand Up @@ -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(
Expand All @@ -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",
Expand Down