Compare commits

...

6 Commits

Author SHA1 Message Date
Cameron Fleming a6eab94e0d feat(ui): implement display backlight control 2025-01-27 20:56:52 +00:00
Cameron Fleming 309d30d3c3 feat(rpc): implement display backlight control methods 2025-01-27 20:56:52 +00:00
Cameron Fleming cabe5b07ab feat(display.go): stop tickers when auto-dim/auto-off is disabled 2025-01-27 20:51:12 +00:00
Cameron Fleming 7d1777985f 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.
2025-01-27 20:51:12 +00:00
Cameron Fleming e9f140c735 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
2025-01-27 20:51:12 +00:00
Cameron Fleming 34e42fd37c chore: update config
Changed Dim & Off values to seconds instead of milliseconds, there's no
need for it to be that precise.
2025-01-27 20:51:12 +00:00
4 changed files with 148 additions and 54 deletions

View File

@ -23,8 +23,8 @@ type Config struct {
LocalAuthMode string `json:"localAuthMode"` //TODO: fix it with migration LocalAuthMode string `json:"localAuthMode"` //TODO: fix it with migration
WakeOnLanDevices []WakeOnLanDevice `json:"wake_on_lan_devices"` WakeOnLanDevices []WakeOnLanDevice `json:"wake_on_lan_devices"`
DisplayMaxBrightness int `json:"display_max_brightness"` DisplayMaxBrightness int `json:"display_max_brightness"`
DisplayDimAfterMs int64 `json:"display_dim_after_ms"` DisplayDimAfterSec int `json:"display_dim_after_sec"`
DisplayOffAfterMs int64 `json:"display_off_after_ms"` DisplayOffAfterSec int `json:"display_off_after_sec"`
} }
const configPath = "/userdata/kvm_config.json" const configPath = "/userdata/kvm_config.json"
@ -33,8 +33,8 @@ var defaultConfig = &Config{
CloudURL: "https://api.jetkvm.com", CloudURL: "https://api.jetkvm.com",
AutoUpdateEnabled: true, // Set a default value AutoUpdateEnabled: true, // Set a default value
DisplayMaxBrightness: 64, DisplayMaxBrightness: 64,
DisplayDimAfterMs: 120000, // 2 minutes DisplayDimAfterSec: 120, // 2 minutes
DisplayOffAfterMs: 1800000, // 30 minutes DisplayOffAfterSec: 1800, // 30 minutes
} }
var config *Config var config *Config

View File

@ -77,7 +77,7 @@ func requestDisplayUpdate() {
return return
} }
go func() { go func() {
wakeDisplay() wakeDisplay(false)
fmt.Println("display updating........................") fmt.Println("display updating........................")
//TODO: only run once regardless how many pending updates //TODO: only run once regardless how many pending updates
updateDisplay() updateDisplay()
@ -149,22 +149,28 @@ func tick_displayOff() {
// wakeDisplay sets the display brightness back to config.DisplayMaxBrightness and stores the time the display // 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. // last woke, ready for displayTimeoutTick to put the display back in the dim/off states.
func wakeDisplay() { // Set force to true to skip the backlight state check, this should be done if altering the tickers.
if backlightState == 0 { func wakeDisplay(force bool) {
if backlightState == 0 && !force {
return return
} }
if config.DisplayMaxBrightness == 0 {
config.DisplayMaxBrightness = 100
}
err := setDisplayBrightness(config.DisplayMaxBrightness) err := setDisplayBrightness(config.DisplayMaxBrightness)
if err != nil { if err != nil {
fmt.Printf("display wake failed, %s\n", err) fmt.Printf("display wake failed, %s\n", err)
} }
dim_ticker.Reset(time.Duration(config.DisplayDimAfterMs) * time.Millisecond) if config.DisplayDimAfterSec == 0 {
off_ticker.Reset(time.Duration(config.DisplayOffAfterMs) * time.Millisecond) 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 backlightState = 0
} }
@ -192,7 +198,43 @@ func watchTsEvents() {
return return
} }
wakeDisplay() 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()
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()
}
}
}()
} }
} }
@ -204,28 +246,11 @@ func init() {
updateStaticContents() updateStaticContents()
displayInited = true displayInited = true
fmt.Println("display inited") fmt.Println("display inited")
wakeDisplay() wakeDisplay(false)
requestDisplayUpdate() requestDisplayUpdate()
}() }()
go func() { startBacklightTickers()
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()
}
}
}()
go watchTsEvents() go watchTsEvents()
} }

View File

@ -225,13 +225,14 @@ func rpcTryUpdate() error {
return nil return nil
} }
func rpcSetBacklightSettings(data *BacklightSettings) error { func rpcSetBacklightSettings(params BacklightSettings) error {
LoadConfig() LoadConfig()
blConfig := *data blConfig := params
if blConfig.MaxBrightness > 100 || blConfig.MaxBrightness < 0 { // NOTE: by default, the frontend limits the brightness to 64, as that's what the device originally shipped with.
return fmt.Errorf("maxBrightness must be between 0 and 100") if blConfig.MaxBrightness > 255 || blConfig.MaxBrightness < 0 {
return fmt.Errorf("maxBrightness must be between 0 and 255")
} }
if blConfig.DimAfter < 0 { if blConfig.DimAfter < 0 {
@ -243,12 +244,24 @@ func rpcSetBacklightSettings(data *BacklightSettings) error {
} }
config.DisplayMaxBrightness = blConfig.MaxBrightness config.DisplayMaxBrightness = blConfig.MaxBrightness
config.DisplayDimAfterMs = int64(blConfig.DimAfter) config.DisplayDimAfterSec = blConfig.DimAfter
config.DisplayOffAfterMs = int64(blConfig.OffAfter) config.DisplayOffAfterSec = blConfig.OffAfter
if err := SaveConfig(); err != nil { if err := SaveConfig(); err != nil {
return fmt.Errorf("failed to save config: %w", err) 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 return nil
} }
@ -257,8 +270,8 @@ func rpcGetBacklightSettings() (*BacklightSettings, error) {
return &BacklightSettings{ return &BacklightSettings{
MaxBrightness: config.DisplayMaxBrightness, MaxBrightness: config.DisplayMaxBrightness,
DimAfter: int(config.DisplayDimAfterMs), DimAfter: int(config.DisplayDimAfterSec),
OffAfter: int(config.DisplayOffAfterMs), OffAfter: int(config.DisplayOffAfterSec),
}, nil }, nil
} }
@ -422,7 +435,7 @@ func callRPCHandler(handler RPCHandler, params map[string]interface{}) (interfac
} }
args[i] = reflect.ValueOf(newStruct).Elem() args[i] = reflect.ValueOf(newStruct).Elem()
} else { } 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 { } else {
args[i] = convertedValue.Convert(paramType) args[i] = convertedValue.Convert(paramType)
@ -597,6 +610,6 @@ var rpcHandlers = map[string]RPCHandler{
"getWakeOnLanDevices": {Func: rpcGetWakeOnLanDevices}, "getWakeOnLanDevices": {Func: rpcGetWakeOnLanDevices},
"setWakeOnLanDevices": {Func: rpcSetWakeOnLanDevices, Params: []string{"params"}}, "setWakeOnLanDevices": {Func: rpcSetWakeOnLanDevices, Params: []string{"params"}},
"resetConfig": {Func: rpcResetConfig}, "resetConfig": {Func: rpcResetConfig},
"setBacklightSettings": {Func: rpcSetBacklightSettings, Params: []string{"settings"}}, "setBacklightSettings": {Func: rpcSetBacklightSettings, Params: []string{"params"}},
"getBacklightSettings": {Func: rpcGetBacklightSettings}, "getBacklightSettings": {Func: rpcGetBacklightSettings},
} }

View File

@ -230,8 +230,18 @@ export default function SettingsSidebar() {
[send, setDeveloperMode], [send, setDeveloperMode],
); );
const handleBacklightSettingChange = useCallback((settings: BacklightSettings) => { const handleBacklightSettingsChange = (settings: BacklightSettings) => {
send("setBacklightSettings", { settings }, resp => { // 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) { if ("error" in resp) {
notifications.error( notifications.error(
`Failed to set backlight settings: ${resp.error.data || "Unknown 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"); notifications.success("Backlight settings updated successfully");
}); });
}, [send]); };
const handleUpdateSSHKey = useCallback(() => { const handleUpdateSSHKey = useCallback(() => {
send("setSSHKeyState", { sshKey }, resp => { send("setSSHKeyState", { sshKey }, resp => {
@ -829,7 +839,6 @@ export default function SettingsSidebar() {
/> />
</div> </div>
<SettingsItem title="Display Brightness" description="Set the brightness of the display"> <SettingsItem title="Display Brightness" description="Set the brightness of the display">
{/* TODO: Allow the user to pick any value between 0 and 100 */}
<SelectMenuBasic <SelectMenuBasic
size="SM" size="SM"
label="" label=""
@ -837,18 +846,65 @@ export default function SettingsSidebar() {
options={[ options={[
{ value: "0", label: "Off" }, { value: "0", label: "Off" },
{ value: "10", label: "Low" }, { value: "10", label: "Low" },
{ value: "50", label: "Medium" }, { value: "35", label: "Medium" },
{ value: "100", label: "High" }, { value: "64", label: "High" },
]} ]}
onChange={e => { onChange={e => {
handleBacklightSettingChange({ settings.backlightSettings.max_brightness = parseInt(e.target.value)
max_brightness: parseInt(e.target.value), handleBacklightSettingsChange(settings.backlightSettings);
dim_after: settings.backlightSettings.dim_after,
off_after: settings.backlightSettings.off_after,
});
}} }}
/> />
</SettingsItem> </SettingsItem>
{settings.backlightSettings.max_brightness != 0 && (
<>
<SettingsItem title="Dim Display After" description="Set how long to wait before dimming the display">
<SelectMenuBasic
size="SM"
label=""
value={settings.backlightSettings.dim_after.toString()}
options={[
{ value: "0", label: "Never" },
{ value: "60", label: "1 Minute" },
{ value: "300", label: "5 Minutes" },
{ value: "600", label: "10 Minutes" },
{ value: "1800", label: "30 Minutes" },
{ value: "3600", label: "1 Hour" },
]}
onChange={e => {
settings.backlightSettings.dim_after = parseInt(e.target.value)
handleBacklightSettingsChange(settings.backlightSettings);
}}
/>
</SettingsItem>
<SettingsItem title="Turn off Display After" description="Set how long to wait before turning off the display">
<SelectMenuBasic
size="SM"
label=""
value={settings.backlightSettings.off_after.toString()}
options={[
{ value: "0", label: "Never" },
{ value: "300", label: "5 Minutes" },
{ value: "600", label: "10 Minutes" },
{ value: "1800", label: "30 Minutes" },
{ value: "3600", label: "1 Hour" },
]}
onChange={e => {
settings.backlightSettings.off_after = parseInt(e.target.value)
handleBacklightSettingsChange(settings.backlightSettings);
}}
/>
</SettingsItem>
</>
)}
<p className="text-xs text-slate-600 dark:text-slate-400">
The display will wake up when the connection state changes, or when touched.
</p>
<Button
size="SM"
theme="primary"
text="Save Display Settings"
onClick={handleBacklightSettingsSave}
/>
<div className="h-[1px] w-full bg-slate-800/10 dark:bg-slate-300/20" /> <div className="h-[1px] w-full bg-slate-800/10 dark:bg-slate-300/20" />
<div className="pb-2 space-y-4"> <div className="pb-2 space-y-4">
<SectionHeader <SectionHeader