diff --git a/internal/usbgadget/config.go b/internal/usbgadget/config.go index e8eee9f2c..8266771d7 100644 --- a/internal/usbgadget/config.go +++ b/internal/usbgadget/config.go @@ -9,8 +9,9 @@ type gadgetConfigItem struct { order uint device string path []string - attrs gadgetAttributes - configAttrs gadgetAttributes + attrs gadgetAttributes + optionalAttrs gadgetAttributes // written with IgnoreErrors; for kernel features that may not exist + configAttrs gadgetAttributes configPath []string reportDesc []byte } @@ -34,7 +35,7 @@ var defaultGadgetConfig = map[string]gadgetConfigItem{ "bcdDevice": "0x0100", // USB2 }, configAttrs: gadgetAttributes{ - "MaxPower": "250", // in unit of 2mA + "MaxPower": "250", // in unit of 2mA "bmAttributes": "0xa0", // bus-powered + remote wakeup }, }, diff --git a/internal/usbgadget/config_tx.go b/internal/usbgadget/config_tx.go index f1e87a07f..750feb2ee 100644 --- a/internal/usbgadget/config_tx.go +++ b/internal/usbgadget/config_tx.go @@ -221,6 +221,16 @@ func (tx *UsbGadgetTransaction) writeGadgetItemConfig(item gadgetConfigItem, dep )...) } + if len(item.optionalAttrs) > 0 { + // write optional attributes (IgnoreErrors=true for backward compat with older kernels) + files = append(files, tx.writeGadgetAttrsOptional( + gadgetItemPath, + item.optionalAttrs, + component, + beforeChange, + )...) + } + // write report descriptor if available reportDescPath := path.Join(gadgetItemPath, "report_desc") if item.reportDesc != nil { @@ -295,6 +305,24 @@ func (tx *UsbGadgetTransaction) writeGadgetAttrs(basePath string, attrs gadgetAt return files } +func (tx *UsbGadgetTransaction) writeGadgetAttrsOptional(basePath string, attrs gadgetAttributes, component string, beforeChange []string) (files []string) { + files = make([]string, 0) + for key, val := range attrs { + filePath := filepath.Join(basePath, key) + tx.addFileChange(component, RequestedFileChange{ + Path: filePath, + ExpectedState: FileStateFileContentMatch, + ExpectedContent: []byte(val), + Description: "write optional gadget attribute", + DependsOn: []string{basePath}, + BeforeChange: beforeChange, + IgnoreErrors: true, + }) + files = append(files, filePath) + } + return files +} + func (tx *UsbGadgetTransaction) addReorderSymlinkChange(path string, target string, deps []string) { tx.log.Trace().Str("path", path).Str("target", target).Msg("add reorder symlink change") diff --git a/internal/usbgadget/hid_keyboard.go b/internal/usbgadget/hid_keyboard.go index 5b114be81..c511ce14f 100644 --- a/internal/usbgadget/hid_keyboard.go +++ b/internal/usbgadget/hid_keyboard.go @@ -23,6 +23,8 @@ var keyboardConfig = gadgetConfigItem{ "subclass": "1", "report_length": "8", "no_out_endpoint": "0", + }, + optionalAttrs: gadgetAttributes{ "wakeup_on_write": "1", }, reportDesc: keyboardReportDesc, diff --git a/internal/usbgadget/hid_mouse_absolute.go b/internal/usbgadget/hid_mouse_absolute.go index 93bde8f2f..09ea6675d 100644 --- a/internal/usbgadget/hid_mouse_absolute.go +++ b/internal/usbgadget/hid_mouse_absolute.go @@ -15,6 +15,8 @@ var absoluteMouseConfig = gadgetConfigItem{ "subclass": "0", "report_length": "6", "no_out_endpoint": "1", + }, + optionalAttrs: gadgetAttributes{ "wakeup_on_write": "1", }, reportDesc: absoluteMouseCombinedReportDesc, diff --git a/internal/usbgadget/hid_mouse_relative.go b/internal/usbgadget/hid_mouse_relative.go index 0cbe239a6..615b27fe8 100644 --- a/internal/usbgadget/hid_mouse_relative.go +++ b/internal/usbgadget/hid_mouse_relative.go @@ -15,6 +15,8 @@ var relativeMouseConfig = gadgetConfigItem{ "subclass": "1", "report_length": "5", "no_out_endpoint": "1", + }, + optionalAttrs: gadgetAttributes{ "wakeup_on_write": "1", }, reportDesc: relativeMouseCombinedReportDesc, diff --git a/ui/localization/messages/en.json b/ui/localization/messages/en.json index 93004e077..a23e95bf0 100644 --- a/ui/localization/messages/en.json +++ b/ui/localization/messages/en.json @@ -1093,5 +1093,8 @@ "wake_on_lan_invalid_mac": "Invalid MAC address", "wake_on_lan_magic_sent_success": "Magic Packet sent successfully", "welcome_to_jetkvm": "Welcome to JetKVM", - "welcome_to_jetkvm_description": "Control any computer remotely" + "welcome_to_jetkvm_description": "Control any computer remotely", + "video_overlay_no_hdmi_try_wake": "If the target computer is sleeping, you can try waking it with a simulated keyboard press", + "video_overlay_no_hdmi_wake_host": "Try Wake Host", + "video_overlay_no_hdmi_wake_host_sending": "Sending wake signal..." } diff --git a/ui/src/components/VideoOverlay.tsx b/ui/src/components/VideoOverlay.tsx index 1690aaebe..9ec2cf290 100644 --- a/ui/src/components/VideoOverlay.tsx +++ b/ui/src/components/VideoOverlay.tsx @@ -216,9 +216,11 @@ export function PeerConnectionDisconnectedOverlay({ show }: PeerConnectionDiscon interface HDMIErrorOverlayProps { readonly show: boolean; readonly hdmiState: string; + readonly onWakeHost?: () => void; + readonly isWaking?: boolean; } -export function HDMIErrorOverlay({ show, hdmiState }: HDMIErrorOverlayProps) { +export function HDMIErrorOverlay({ show, hdmiState, onWakeHost, isWaking }: HDMIErrorOverlayProps) { const isNoSignal = hdmiState === "no_signal"; const isOtherError = hdmiState === "no_lock" || hdmiState === "out_of_range"; @@ -247,9 +249,10 @@ export function HDMIErrorOverlay({ show, hdmiState }: HDMIErrorOverlayProps) {