Skip to content

feat(display.go): Add display brightness settings #17

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 27 commits into from
Feb 17, 2025
Merged
Show file tree
Hide file tree
Changes from all 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,24 +12,30 @@ 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"`
EdidString string `json:"hdmi_edid_string"`
DisplayMaxBrightness int `json:"display_max_brightness"`
DisplayDimAfterSec int `json:"display_dim_after_sec"`
DisplayOffAfterSec int `json:"display_off_after_sec"`
}

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: 64,
DisplayDimAfterSec: 120, // 2 minutes
DisplayOffAfterSec: 1800, // 30 minutes
}

var config *Config
Expand Down
169 changes: 169 additions & 0 deletions display.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,26 @@
package kvm

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

var currentScreen = "ui_Boot_Screen"
var backlightState = 0 // 0 - NORMAL, 1 - DIMMED, 2 - OFF

var (
dimTicker *time.Ticker
offTicker *time.Ticker
)

const (
touchscreenDevice string = "/dev/input/event1"
backlightControlClass string = "/sys/class/backlight/backlight/brightness"
)

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

fmt.Printf("display: set brightness to %v\n", brightness)
return nil
}

// 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)
}

dimTicker.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)
}

offTicker.Stop()

backlightState = 2
}
Comment on lines +126 to +150
Copy link

@joshuasing joshuasing Jan 30, 2025

Choose a reason for hiding this comment

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

These function names contain underscores (which as mentioned in my other review, Effective Go and Go stdlib recommend using lowerCamelCase/UpperCamelCase - https://go.dev/doc/effective_go#mixed-caps) - Also, I think it is very uncommon to put () after the function name in doc comments (see https://go.dev/doc/comment):

(this suggestion does not update uses of the functions)

Suggested change
// 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)
}
dimTicker.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)
}
offTicker.Stop()
backlightState = 2
}
// tickDisplayDim is called when when dim ticker expires, it simply reduces the brightness
// of the display by half of the max brightness.
func tickDisplayDim() {
err := setDisplayBrightness(config.DisplayMaxBrightness / 2)
if err != nil {
fmt.Printf("display: failed to dim display: %s\n", err)
}
dimTicker.Stop()
backlightState = 1
}
// tickDisplayOff is called when the off ticker expires, it turns off the display
// by setting the brightness to zero.
func tickDisplayOff() {
err := setDisplayBrightness(0)
if err != nil {
fmt.Printf("display: failed to turn off display: %s\n", err)
}
offTicker.Stop()
backlightState = 2
}

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! I'll update these afterwards.


// 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.
// 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
}

// Don't try to wake up if the display is turned off.
if config.DisplayMaxBrightness == 0 {
return
}

err := setDisplayBrightness(config.DisplayMaxBrightness)
if err != nil {
fmt.Printf("display wake failed, %s\n", err)
}

if config.DisplayDimAfterSec != 0 {
dimTicker.Reset(time.Duration(config.DisplayDimAfterSec) * time.Second)
}

if config.DisplayOffAfterSec != 0 {
offTicker.Reset(time.Duration(config.DisplayOffAfterSec) * time.Second)
}
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() {
ts, err := os.OpenFile(touchscreenDevice, os.O_RDONLY, 0666)
if err != nil {
fmt.Printf("display: failed to open touchscreen device: %s\n", err)
return
}

defer ts.Close()

// 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)
if err != nil {
fmt.Printf("display: failed to read from touchscreen device: %s\n", err)
return
}

wakeDisplay(false)
}
}

// 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()
// Don't start the tickers if the display is switched off.
// Set the display to off if that's the case.
if config.DisplayMaxBrightness == 0 {
setDisplayBrightness(0)
return
}

if dimTicker == nil && config.DisplayDimAfterSec != 0 {
fmt.Printf("display: dim_ticker has started\n")
dimTicker = time.NewTicker(time.Duration(config.DisplayDimAfterSec) * time.Second)
defer dimTicker.Stop()

go func() {
for {
select {
case <-dimTicker.C:
tick_displayDim()
}
}
}()
}

if offTicker == nil && config.DisplayOffAfterSec != 0 {
fmt.Printf("display: off_ticker has started\n")
offTicker = time.NewTicker(time.Duration(config.DisplayOffAfterSec) * time.Second)
defer offTicker.Stop()

go func() {
for {
select {
case <-offTicker.C:
tick_displayOff()
}
}
}()
}
}

func init() {
go func() {
waitCtrlClientConnected()
Expand All @@ -91,6 +256,10 @@ func init() {
updateStaticContents()
displayInited = true
fmt.Println("display inited")
startBacklightTickers()
wakeDisplay(true)
requestDisplayUpdate()
}()

go watchTsEvents()
}
60 changes: 59 additions & 1 deletion 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 @@ -225,6 +231,56 @@ func rpcTryUpdate() error {
return nil
}

func rpcSetBacklightSettings(params BacklightSettings) error {
LoadConfig()

blConfig := params

// 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 {
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.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
}

func rpcGetBacklightSettings() (*BacklightSettings, error) {
LoadConfig()

return &BacklightSettings{
MaxBrightness: config.DisplayMaxBrightness,
DimAfter: int(config.DisplayDimAfterSec),
OffAfter: int(config.DisplayOffAfterSec),
}, nil
}

const (
devModeFile = "/userdata/jetkvm/devmode.enable"
sshKeyDir = "/userdata/dropbear/.ssh"
Expand Down Expand Up @@ -385,7 +441,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)
Expand Down Expand Up @@ -560,4 +616,6 @@ var rpcHandlers = map[string]RPCHandler{
"getWakeOnLanDevices": {Func: rpcGetWakeOnLanDevices},
"setWakeOnLanDevices": {Func: rpcSetWakeOnLanDevices, Params: []string{"params"}},
"resetConfig": {Func: rpcResetConfig},
"setBacklightSettings": {Func: rpcSetBacklightSettings, Params: []string{"params"}},
"getBacklightSettings": {Func: rpcGetBacklightSettings},
}
Loading