Skip to content

Commit 5217377

Browse files
authored
feat(display.go): Add display brightness settings (#17)
* 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. * feat(config): add backlight control settings * 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 * feat(rpc): add methods to get and set BacklightSettings * WIP: feat(settings): add Max backlight setting * chore: use constant for backlight control file * fix: only attempt to wake the display if it's off * feat(display): wake on touch * fix: re-use buffer between reads * 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. * chore: various comment & string updates * fix: newline on set brightness log Noticed by @eric #17 (comment) * 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. * feat(display.go): use tickers to countdown to dim/off As suggested by tutman in #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. * chore: update config Changed Dim & Off values to seconds instead of milliseconds, there's no need for it to be that precise. * 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 * 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. * feat(display.go): stop tickers when auto-dim/auto-off is disabled * feat(rpc): implement display backlight control methods * feat(ui): implement display backlight control * chore: update variable names As part of @joshuasing's review on #17, updated variables & constants to match the Go best practices. Signed-off-by: Cameron Fleming <[email protected]> * fix(display): move backlightTicker setup into screen setup goroutine Signed-off-by: Cameron Fleming <[email protected]> * chore: fix some start-up timing issues * fix(display): Don't attempt to start the tickers if the display is disabled If max_brightness is zero, then there's no point in trying to dim it (or turn it off...) * fix: wakeDisplay() doesn't need to stop the tickers The tickers only need to be reset, if they're disabled, they won't have been started. * fix: Don't wake up the display if it's turned off --------- Signed-off-by: Cameron Fleming <[email protected]>
1 parent 951173b commit 5217377

File tree

5 files changed

+371
-13
lines changed

5 files changed

+371
-13
lines changed

config.go

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -12,24 +12,30 @@ type WakeOnLanDevice struct {
1212
}
1313

1414
type Config struct {
15-
CloudURL string `json:"cloud_url"`
16-
CloudToken string `json:"cloud_token"`
17-
GoogleIdentity string `json:"google_identity"`
18-
JigglerEnabled bool `json:"jiggler_enabled"`
19-
AutoUpdateEnabled bool `json:"auto_update_enabled"`
20-
IncludePreRelease bool `json:"include_pre_release"`
21-
HashedPassword string `json:"hashed_password"`
22-
LocalAuthToken string `json:"local_auth_token"`
23-
LocalAuthMode string `json:"localAuthMode"` //TODO: fix it with migration
24-
WakeOnLanDevices []WakeOnLanDevice `json:"wake_on_lan_devices"`
15+
CloudURL string `json:"cloud_url"`
16+
CloudToken string `json:"cloud_token"`
17+
GoogleIdentity string `json:"google_identity"`
18+
JigglerEnabled bool `json:"jiggler_enabled"`
19+
AutoUpdateEnabled bool `json:"auto_update_enabled"`
20+
IncludePreRelease bool `json:"include_pre_release"`
21+
HashedPassword string `json:"hashed_password"`
22+
LocalAuthToken string `json:"local_auth_token"`
23+
LocalAuthMode string `json:"localAuthMode"` //TODO: fix it with migration
24+
WakeOnLanDevices []WakeOnLanDevice `json:"wake_on_lan_devices"`
2525
EdidString string `json:"hdmi_edid_string"`
26+
DisplayMaxBrightness int `json:"display_max_brightness"`
27+
DisplayDimAfterSec int `json:"display_dim_after_sec"`
28+
DisplayOffAfterSec int `json:"display_off_after_sec"`
2629
}
2730

2831
const configPath = "/userdata/kvm_config.json"
2932

3033
var defaultConfig = &Config{
31-
CloudURL: "https://api.jetkvm.com",
32-
AutoUpdateEnabled: true, // Set a default value
34+
CloudURL: "https://api.jetkvm.com",
35+
AutoUpdateEnabled: true, // Set a default value
36+
DisplayMaxBrightness: 64,
37+
DisplayDimAfterSec: 120, // 2 minutes
38+
DisplayOffAfterSec: 1800, // 30 minutes
3339
}
3440

3541
var config *Config

display.go

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,26 @@
11
package kvm
22

33
import (
4+
"errors"
45
"fmt"
56
"log"
7+
"os"
8+
"strconv"
69
"time"
710
)
811

912
var currentScreen = "ui_Boot_Screen"
13+
var backlightState = 0 // 0 - NORMAL, 1 - DIMMED, 2 - OFF
14+
15+
var (
16+
dimTicker *time.Ticker
17+
offTicker *time.Ticker
18+
)
19+
20+
const (
21+
touchscreenDevice string = "/dev/input/event1"
22+
backlightControlClass string = "/sys/class/backlight/backlight/brightness"
23+
)
1024

1125
func switchToScreen(screen string) {
1226
_, err := CallCtrlAction("lv_scr_load", map[string]interface{}{"obj": screen})
@@ -65,6 +79,7 @@ func requestDisplayUpdate() {
6579
return
6680
}
6781
go func() {
82+
wakeDisplay(false)
6883
fmt.Println("display updating........................")
6984
//TODO: only run once regardless how many pending updates
7085
updateDisplay()
@@ -83,6 +98,156 @@ func updateStaticContents() {
8398
updateLabelIfChanged("ui_Status_Content_Device_Id_Content_Label", GetDeviceID())
8499
}
85100

101+
// setDisplayBrightness sets /sys/class/backlight/backlight/brightness to alter
102+
// the backlight brightness of the JetKVM hardware's display.
103+
func setDisplayBrightness(brightness int) error {
104+
// NOTE: The actual maximum value for this is 255, but out-of-the-box, the value is set to 64.
105+
// The maximum set here is set to 100 to reduce the risk of drawing too much power (and besides, 255 is very bright!).
106+
if brightness > 100 || brightness < 0 {
107+
return errors.New("brightness value out of bounds, must be between 0 and 100")
108+
}
109+
110+
// Check the display backlight class is available
111+
if _, err := os.Stat(backlightControlClass); errors.Is(err, os.ErrNotExist) {
112+
return errors.New("brightness value cannot be set, possibly not running on JetKVM hardware")
113+
}
114+
115+
// Set the value
116+
bs := []byte(strconv.Itoa(brightness))
117+
err := os.WriteFile(backlightControlClass, bs, 0644)
118+
if err != nil {
119+
return err
120+
}
121+
122+
fmt.Printf("display: set brightness to %v\n", brightness)
123+
return nil
124+
}
125+
126+
// tick_displayDim() is called when when dim ticker expires, it simply reduces the brightness
127+
// of the display by half of the max brightness.
128+
func tick_displayDim() {
129+
err := setDisplayBrightness(config.DisplayMaxBrightness / 2)
130+
if err != nil {
131+
fmt.Printf("display: failed to dim display: %s\n", err)
132+
}
133+
134+
dimTicker.Stop()
135+
136+
backlightState = 1
137+
}
138+
139+
// tick_displayOff() is called when the off ticker expires, it turns off the display
140+
// by setting the brightness to zero.
141+
func tick_displayOff() {
142+
err := setDisplayBrightness(0)
143+
if err != nil {
144+
fmt.Printf("display: failed to turn off display: %s\n", err)
145+
}
146+
147+
offTicker.Stop()
148+
149+
backlightState = 2
150+
}
151+
152+
// wakeDisplay sets the display brightness back to config.DisplayMaxBrightness and stores the time the display
153+
// last woke, ready for displayTimeoutTick to put the display back in the dim/off states.
154+
// Set force to true to skip the backlight state check, this should be done if altering the tickers.
155+
func wakeDisplay(force bool) {
156+
if backlightState == 0 && !force {
157+
return
158+
}
159+
160+
// Don't try to wake up if the display is turned off.
161+
if config.DisplayMaxBrightness == 0 {
162+
return
163+
}
164+
165+
err := setDisplayBrightness(config.DisplayMaxBrightness)
166+
if err != nil {
167+
fmt.Printf("display wake failed, %s\n", err)
168+
}
169+
170+
if config.DisplayDimAfterSec != 0 {
171+
dimTicker.Reset(time.Duration(config.DisplayDimAfterSec) * time.Second)
172+
}
173+
174+
if config.DisplayOffAfterSec != 0 {
175+
offTicker.Reset(time.Duration(config.DisplayOffAfterSec) * time.Second)
176+
}
177+
backlightState = 0
178+
}
179+
180+
// watchTsEvents monitors the touchscreen for events and simply calls wakeDisplay() to ensure the
181+
// touchscreen interface still works even with LCD dimming/off.
182+
// TODO: This is quite a hack, really we should be getting an event from jetkvm_native, or the whole display backlight
183+
// control should be hoisted up to jetkvm_native.
184+
func watchTsEvents() {
185+
ts, err := os.OpenFile(touchscreenDevice, os.O_RDONLY, 0666)
186+
if err != nil {
187+
fmt.Printf("display: failed to open touchscreen device: %s\n", err)
188+
return
189+
}
190+
191+
defer ts.Close()
192+
193+
// This buffer is set to 24 bytes as that's the normal size of events on /dev/input
194+
// Reference: https://www.kernel.org/doc/Documentation/input/input.txt
195+
// This could potentially be set higher, to require multiple events to wake the display.
196+
buf := make([]byte, 24)
197+
for {
198+
_, err := ts.Read(buf)
199+
if err != nil {
200+
fmt.Printf("display: failed to read from touchscreen device: %s\n", err)
201+
return
202+
}
203+
204+
wakeDisplay(false)
205+
}
206+
}
207+
208+
// startBacklightTickers starts the two tickers for dimming and switching off the display
209+
// if they're not already set. This is done separately to the init routine as the "never dim"
210+
// option has the value set to zero, but time.NewTicker only accept positive values.
211+
func startBacklightTickers() {
212+
LoadConfig()
213+
// Don't start the tickers if the display is switched off.
214+
// Set the display to off if that's the case.
215+
if config.DisplayMaxBrightness == 0 {
216+
setDisplayBrightness(0)
217+
return
218+
}
219+
220+
if dimTicker == nil && config.DisplayDimAfterSec != 0 {
221+
fmt.Printf("display: dim_ticker has started\n")
222+
dimTicker = time.NewTicker(time.Duration(config.DisplayDimAfterSec) * time.Second)
223+
defer dimTicker.Stop()
224+
225+
go func() {
226+
for {
227+
select {
228+
case <-dimTicker.C:
229+
tick_displayDim()
230+
}
231+
}
232+
}()
233+
}
234+
235+
if offTicker == nil && config.DisplayOffAfterSec != 0 {
236+
fmt.Printf("display: off_ticker has started\n")
237+
offTicker = time.NewTicker(time.Duration(config.DisplayOffAfterSec) * time.Second)
238+
defer offTicker.Stop()
239+
240+
go func() {
241+
for {
242+
select {
243+
case <-offTicker.C:
244+
tick_displayOff()
245+
}
246+
}
247+
}()
248+
}
249+
}
250+
86251
func init() {
87252
go func() {
88253
waitCtrlClientConnected()
@@ -91,6 +256,10 @@ func init() {
91256
updateStaticContents()
92257
displayInited = true
93258
fmt.Println("display inited")
259+
startBacklightTickers()
260+
wakeDisplay(true)
94261
requestDisplayUpdate()
95262
}()
263+
264+
go watchTsEvents()
96265
}

jsonrpc.go

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,12 @@ type JSONRPCEvent struct {
3434
Params interface{} `json:"params,omitempty"`
3535
}
3636

37+
type BacklightSettings struct {
38+
MaxBrightness int `json:"max_brightness"`
39+
DimAfter int `json:"dim_after"`
40+
OffAfter int `json:"off_after"`
41+
}
42+
3743
func writeJSONRPCResponse(response JSONRPCResponse, session *Session) {
3844
responseBytes, err := json.Marshal(response)
3945
if err != nil {
@@ -225,6 +231,56 @@ func rpcTryUpdate() error {
225231
return nil
226232
}
227233

234+
func rpcSetBacklightSettings(params BacklightSettings) error {
235+
LoadConfig()
236+
237+
blConfig := params
238+
239+
// NOTE: by default, the frontend limits the brightness to 64, as that's what the device originally shipped with.
240+
if blConfig.MaxBrightness > 255 || blConfig.MaxBrightness < 0 {
241+
return fmt.Errorf("maxBrightness must be between 0 and 255")
242+
}
243+
244+
if blConfig.DimAfter < 0 {
245+
return fmt.Errorf("dimAfter must be a positive integer")
246+
}
247+
248+
if blConfig.OffAfter < 0 {
249+
return fmt.Errorf("offAfter must be a positive integer")
250+
}
251+
252+
config.DisplayMaxBrightness = blConfig.MaxBrightness
253+
config.DisplayDimAfterSec = blConfig.DimAfter
254+
config.DisplayOffAfterSec = blConfig.OffAfter
255+
256+
if err := SaveConfig(); err != nil {
257+
return fmt.Errorf("failed to save config: %w", err)
258+
}
259+
260+
log.Printf("rpc: display: settings applied, max_brightness: %d, dim after: %ds, off after: %ds", config.DisplayMaxBrightness, config.DisplayDimAfterSec, config.DisplayOffAfterSec)
261+
262+
// If the device started up with auto-dim and/or auto-off set to zero, the display init
263+
// method will not have started the tickers. So in case that has changed, attempt to start the tickers now.
264+
startBacklightTickers()
265+
266+
// Wake the display after the settings are altered, this ensures the tickers
267+
// are reset to the new settings, and will bring the display up to maxBrightness.
268+
// Calling with force set to true, to ignore the current state of the display, and force
269+
// it to reset the tickers.
270+
wakeDisplay(true)
271+
return nil
272+
}
273+
274+
func rpcGetBacklightSettings() (*BacklightSettings, error) {
275+
LoadConfig()
276+
277+
return &BacklightSettings{
278+
MaxBrightness: config.DisplayMaxBrightness,
279+
DimAfter: int(config.DisplayDimAfterSec),
280+
OffAfter: int(config.DisplayOffAfterSec),
281+
}, nil
282+
}
283+
228284
const (
229285
devModeFile = "/userdata/jetkvm/devmode.enable"
230286
sshKeyDir = "/userdata/dropbear/.ssh"
@@ -385,7 +441,7 @@ func callRPCHandler(handler RPCHandler, params map[string]interface{}) (interfac
385441
}
386442
args[i] = reflect.ValueOf(newStruct).Elem()
387443
} else {
388-
return nil, fmt.Errorf("invalid parameter type for: %s", paramName)
444+
return nil, fmt.Errorf("invalid parameter type for: %s, type: %s", paramName, paramType.Kind())
389445
}
390446
} else {
391447
args[i] = convertedValue.Convert(paramType)
@@ -560,4 +616,6 @@ var rpcHandlers = map[string]RPCHandler{
560616
"getWakeOnLanDevices": {Func: rpcGetWakeOnLanDevices},
561617
"setWakeOnLanDevices": {Func: rpcSetWakeOnLanDevices, Params: []string{"params"}},
562618
"resetConfig": {Func: rpcResetConfig},
619+
"setBacklightSettings": {Func: rpcSetBacklightSettings, Params: []string{"params"}},
620+
"getBacklightSettings": {Func: rpcGetBacklightSettings},
563621
}

0 commit comments

Comments
 (0)