-
Notifications
You must be signed in to change notification settings - Fork 264
feat: hid rpc channel #755
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
Changes from 4 commits
Commits
Show all changes
20 commits
Select commit
Hold shift + click to select a range
58b72ad
feat: use hidRpcChannel to save bandwidth
ym eacc2a6
chore: simplify handshake of hid rpc
ym f7beae3
add logs
ym fefbc76
chore: add timeout when writing to hid endpoints
ym 3dd8645
fix issues
ym c459929
chore: show hid rpc version
ym d61ea21
refactor hidrpc marshal / unmarshal
ym 7389467
add queues for keyboard / mouse event
ym af8fff7
chore: change logging level of JSONRPC send event to trace
ym e181523
minor changes related to logging
ym 4592269
fix: nil check
ym 7feb92c
chore: add comments and remove unused code
ym a4f0c0d
add useMouse
ym 94a3883
chore: log msg data only when debug or trace mode
ym 2e1b6f1
chore: make tslint happy
ym 867ed88
chore: unlock keyboardStateLock before calling onKeysDownChange
ym 8abcd1e
chore: remove keyPressReportApiAvailable
ym bd63788
chore: change version handle
ym 223db94
chore: clean up unused functions
ym 113091a
remove comments
ym File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,150 @@ | ||
| package kvm | ||
|
|
||
| import ( | ||
| "fmt" | ||
| "time" | ||
|
|
||
| "github.com/jetkvm/kvm/internal/hidrpc" | ||
| "github.com/jetkvm/kvm/internal/usbgadget" | ||
| ) | ||
|
|
||
| func handleHidRpcMessage(message hidrpc.Message, session *Session) { | ||
| var rpcErr error | ||
|
|
||
| switch message.Type() { | ||
| case hidrpc.TypeHandshake: | ||
| message, err := hidrpc.NewHandshakeMessage().Marshal() | ||
| if err != nil { | ||
| logger.Warn().Err(err).Msg("failed to marshal handshake message") | ||
| return | ||
| } | ||
| if err := session.HidChannel.Send(message); err != nil { | ||
| logger.Warn().Err(err).Msg("failed to send handshake message") | ||
| return | ||
| } | ||
| session.hidRpcAvailable = true | ||
| case hidrpc.TypeKeypressReport, hidrpc.TypeKeyboardReport: | ||
| keysDownState, err := handleHidRpcKeyboardInput(message) | ||
| if keysDownState != nil { | ||
| reportHidRpcKeysDownState(*keysDownState, session) | ||
| } | ||
| rpcErr = err | ||
| case hidrpc.TypePointerReport: | ||
| pointerReport, err := message.PointerReport() | ||
| if err != nil { | ||
| logger.Warn().Err(err).Msg("failed to get pointer report") | ||
| return | ||
| } | ||
| rpcErr = rpcAbsMouseReport(pointerReport.X, pointerReport.Y, pointerReport.Button) | ||
| case hidrpc.TypeMouseReport: | ||
| mouseReport, err := message.MouseReport() | ||
| if err != nil { | ||
| logger.Warn().Err(err).Msg("failed to get mouse report") | ||
| return | ||
| } | ||
| rpcErr = rpcRelMouseReport(mouseReport.DX, mouseReport.DY, mouseReport.Button) | ||
| default: | ||
| logger.Warn().Uint8("type", uint8(message.Type())).Msg("unknown HID RPC message type") | ||
| } | ||
|
|
||
| if rpcErr != nil { | ||
| logger.Warn().Err(rpcErr).Msg("failed to handle HID RPC message") | ||
| } | ||
| } | ||
|
|
||
| func onHidMessage(data []byte, session *Session) { | ||
| scopedLogger := hidRpcLogger.With().Bytes("data", data).Logger() | ||
| scopedLogger.Debug().Msg("HID RPC message received") | ||
|
|
||
| if len(data) < 1 { | ||
| scopedLogger.Warn().Int("length", len(data)).Msg("received empty data in HID RPC message handler") | ||
| return | ||
| } | ||
|
|
||
| var message hidrpc.Message | ||
|
|
||
| if err := hidrpc.Unmarshal(data, &message); err != nil { | ||
| scopedLogger.Warn().Err(err).Msg("failed to unmarshal HID RPC message") | ||
| return | ||
| } | ||
|
|
||
| scopedLogger = scopedLogger.With().Str("descr", message.String()).Logger() | ||
|
|
||
| t := time.Now() | ||
|
|
||
| r := make(chan interface{}) | ||
| go func() { | ||
| handleHidRpcMessage(message, session) | ||
| r <- nil | ||
| }() | ||
| select { | ||
| case <-time.After(1 * time.Second): | ||
| scopedLogger.Warn().Msg("HID RPC message timed out") | ||
| case <-r: | ||
| scopedLogger.Debug().Dur("duration", time.Since(t)).Msg("HID RPC message handled") | ||
| } | ||
| } | ||
|
|
||
| func handleHidRpcKeyboardInput(message hidrpc.Message) (*usbgadget.KeysDownState, error) { | ||
| switch message.Type() { | ||
| case hidrpc.TypeKeypressReport: | ||
| keypressReport, err := message.KeypressReport() | ||
| if err != nil { | ||
| logger.Warn().Err(err).Msg("failed to get keypress report") | ||
| return nil, err | ||
| } | ||
| keysDownState, rpcError := rpcKeypressReport(keypressReport.Key, keypressReport.Press) | ||
| return &keysDownState, rpcError | ||
| case hidrpc.TypeKeyboardReport: | ||
| keyboardReport, err := message.KeyboardReport() | ||
| if err != nil { | ||
| logger.Warn().Err(err).Msg("failed to get keyboard report") | ||
| return nil, err | ||
| } | ||
| keysDownState, rpcError := rpcKeyboardReport(keyboardReport.Modifier, keyboardReport.Keys) | ||
| return &keysDownState, rpcError | ||
| } | ||
|
|
||
| return nil, fmt.Errorf("unknown HID RPC message type: %d", message.Type()) | ||
| } | ||
|
|
||
| func reportHidRpc(params any, session *Session) { | ||
| var ( | ||
| message []byte | ||
| err error | ||
| ) | ||
| switch params := params.(type) { | ||
| case usbgadget.KeyboardState: | ||
| message, err = hidrpc.NewKeyboardLedMessage(params).Marshal() | ||
| case usbgadget.KeysDownState: | ||
| message, err = hidrpc.NewKeydownStateMessage(params).Marshal() | ||
| } | ||
|
|
||
| if err != nil { | ||
| logger.Warn().Err(err).Msg("failed to marshal HID RPC message") | ||
| return | ||
| } | ||
|
|
||
| if message == nil { | ||
| logger.Warn().Msg("failed to marshal HID RPC message") | ||
| return | ||
| } | ||
|
|
||
| if err := session.HidChannel.Send(message); err != nil { | ||
| logger.Warn().Err(err).Msg("failed to send HID RPC message") | ||
| } | ||
| } | ||
|
|
||
| func reportHidRpcKeyboardLedState(state usbgadget.KeyboardState, session *Session) { | ||
| if !session.hidRpcAvailable { | ||
| writeJSONRPCEvent("keyboardLedState", state, currentSession) | ||
| } | ||
| reportHidRpc(state, session) | ||
| } | ||
|
|
||
| func reportHidRpcKeysDownState(state usbgadget.KeysDownState, session *Session) { | ||
| if !session.hidRpcAvailable { | ||
| writeJSONRPCEvent("keysDownState", state, currentSession) | ||
| } | ||
| reportHidRpc(state, session) | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,91 @@ | ||
| package hidrpc | ||
|
|
||
| import ( | ||
| "fmt" | ||
|
|
||
| "github.com/jetkvm/kvm/internal/usbgadget" | ||
| ) | ||
|
|
||
| // HID RPC is a variable-length packet format that is used to exchange keyboard and mouse events between the client and the server. | ||
| // The packet format is as follows: | ||
| // 1 byte: Event Type | ||
|
|
||
| // MessageType is the type of the HID RPC message | ||
| type MessageType uint8 | ||
ym marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| const ( | ||
| TypeHandshake MessageType = 0x01 | ||
| TypeKeyboardReport MessageType = 0x02 | ||
| TypePointerReport MessageType = 0x03 | ||
| TypeWheelReport MessageType = 0x04 | ||
| TypeKeypressReport MessageType = 0x05 | ||
| TypeMouseReport MessageType = 0x06 | ||
| TypeKeyboardLedState MessageType = 0x32 | ||
| TypeKeydownState MessageType = 0x33 | ||
| ) | ||
|
|
||
| // ShouldUseEnqueue returns true if the message type should be enqueued to the HID queue. | ||
| func ShouldUseEnqueue(messageType MessageType) bool { | ||
| return messageType == TypeMouseReport | ||
| } | ||
|
|
||
| // Unmarshal unmarshals the HID RPC message from the data. | ||
| func Unmarshal(data []byte, message *Message) error { | ||
| l := len(data) | ||
| if l < 1 { | ||
| return fmt.Errorf("invalid data length: %d", l) | ||
| } | ||
|
|
||
| message.t = MessageType(data[0]) | ||
| message.d = data[1:] | ||
| return nil | ||
| } | ||
|
|
||
| // Marshal marshals the HID RPC message to the data. | ||
| func Marshal(message *Message) ([]byte, error) { | ||
| if message.t == 0 { | ||
| return nil, fmt.Errorf("invalid message type: %d", message.t) | ||
| } | ||
|
|
||
| data := make([]byte, len(message.d)+1) | ||
| data[0] = byte(message.t) | ||
| copy(data[1:], message.d) | ||
|
|
||
| return data, nil | ||
| } | ||
|
|
||
| // NewHandshakeMessage creates a new handshake message. | ||
| func NewHandshakeMessage() *Message { | ||
| return &Message{ | ||
| t: TypeHandshake, | ||
| d: []byte{}, | ||
| } | ||
| } | ||
|
|
||
| // NewKeyboardReportMessage creates a new keyboard report message. | ||
| func NewKeyboardReportMessage(keys []byte, modifier uint8) *Message { | ||
| return &Message{ | ||
| t: TypeKeyboardReport, | ||
| d: append([]byte{modifier}, keys...), | ||
| } | ||
| } | ||
|
|
||
| // NewKeyboardLedMessage creates a new keyboard LED message. | ||
| func NewKeyboardLedMessage(state usbgadget.KeyboardState) *Message { | ||
| return &Message{ | ||
| t: TypeKeyboardLedState, | ||
| d: []byte{state.Byte()}, | ||
| } | ||
| } | ||
|
|
||
| // NewKeydownStateMessage creates a new keydown state message. | ||
| func NewKeydownStateMessage(state usbgadget.KeysDownState) *Message { | ||
| data := make([]byte, len(state.Keys)+1) | ||
| data[0] = state.Modifier | ||
| copy(data[1:], state.Keys) | ||
|
|
||
| return &Message{ | ||
| t: TypeKeydownState, | ||
| d: data, | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,121 @@ | ||
| package hidrpc | ||
|
|
||
| import ( | ||
| "fmt" | ||
| ) | ||
|
|
||
| // Message .. | ||
| type Message struct { | ||
| t MessageType | ||
| d []byte | ||
| } | ||
|
|
||
| // Marshal marshals the message to a byte array. | ||
| func (m *Message) Marshal() ([]byte, error) { | ||
| return Marshal(m) | ||
| } | ||
|
|
||
| func (m *Message) Type() MessageType { | ||
| return m.t | ||
| } | ||
|
|
||
| func (m *Message) String() string { | ||
| switch m.t { | ||
| case TypeHandshake: | ||
| return "Handshake" | ||
| case TypeKeypressReport: | ||
| return fmt.Sprintf("KeypressReport{Key: %d, Press: %v}", m.d[0], m.d[1] == uint8(1)) | ||
| case TypeKeyboardReport: | ||
| return fmt.Sprintf("KeyboardReport{Modifier: %d, Keys: %v}", m.d[0], m.d[1:]) | ||
| case TypePointerReport: | ||
| return fmt.Sprintf("PointerReport{X: %d, Y: %d, Button: %d}", m.d[0:4], m.d[4:8], m.d[8]) | ||
| case TypeMouseReport: | ||
| return fmt.Sprintf("MouseReport{DX: %d, DY: %d, Button: %d}", m.d[0:2], m.d[2:4], m.d[4]) | ||
| default: | ||
| return fmt.Sprintf("Unknown{Type: %d, Data: %v}", m.t, m.d) | ||
| } | ||
| } | ||
|
|
||
| // KeypressReport .. | ||
| type KeypressReport struct { | ||
| Key byte | ||
| Press bool | ||
| } | ||
|
|
||
| // KeypressReport returns the keypress report from the message. | ||
| func (m *Message) KeypressReport() (KeypressReport, error) { | ||
| if m.t != TypeKeypressReport { | ||
| return KeypressReport{}, fmt.Errorf("invalid message type: %d", m.t) | ||
| } | ||
|
|
||
| return KeypressReport{ | ||
| Key: m.d[0], | ||
| Press: m.d[1] == uint8(1), | ||
| }, nil | ||
| } | ||
|
|
||
| // KeyboardReport .. | ||
| type KeyboardReport struct { | ||
| Modifier byte | ||
| Keys []byte | ||
| } | ||
|
|
||
| // KeyboardReport returns the keyboard report from the message. | ||
| func (m *Message) KeyboardReport() (KeyboardReport, error) { | ||
| if m.t != TypeKeyboardReport { | ||
| return KeyboardReport{}, fmt.Errorf("invalid message type: %d", m.t) | ||
| } | ||
|
|
||
| return KeyboardReport{ | ||
| Modifier: m.d[0], | ||
| Keys: m.d[1:], | ||
| }, nil | ||
| } | ||
|
|
||
| // PointerReport .. | ||
| type PointerReport struct { | ||
| X int | ||
| Y int | ||
| Button uint8 | ||
| } | ||
|
|
||
| func toInt(b []byte) int { | ||
| return int(b[0])<<24 + int(b[1])<<16 + int(b[2])<<8 + int(b[3])<<0 | ||
| } | ||
|
|
||
| // PointerReport returns the point report from the message. | ||
| func (m *Message) PointerReport() (PointerReport, error) { | ||
| if m.t != TypePointerReport { | ||
| return PointerReport{}, fmt.Errorf("invalid message type: %d", m.t) | ||
| } | ||
|
|
||
| if len(m.d) != 9 { | ||
| return PointerReport{}, fmt.Errorf("invalid message length: %d", len(m.d)) | ||
| } | ||
|
|
||
| return PointerReport{ | ||
| X: toInt(m.d[0:4]), | ||
| Y: toInt(m.d[4:8]), | ||
| Button: uint8(m.d[8]), | ||
| }, nil | ||
| } | ||
|
|
||
| // MouseReport .. | ||
| type MouseReport struct { | ||
| DX int8 | ||
| DY int8 | ||
| Button uint8 | ||
| } | ||
|
|
||
| // MouseReport returns the mouse report from the message. | ||
| func (m *Message) MouseReport() (MouseReport, error) { | ||
| if m.t != TypeMouseReport { | ||
| return MouseReport{}, fmt.Errorf("invalid message type: %d", m.t) | ||
| } | ||
|
|
||
| return MouseReport{ | ||
| DX: int8(m.d[0]), | ||
| DY: int8(m.d[1]), | ||
| Button: uint8(m.d[2]), | ||
| }, nil | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,7 @@ | ||
| package usbgadget | ||
|
|
||
| import "time" | ||
|
|
||
| const dwc3Path = "/sys/bus/platform/drivers/dwc3" | ||
|
|
||
| const hidWriteTimeout = 10 * time.Millisecond | ||
adamshiervani marked this conversation as resolved.
Show resolved
Hide resolved
|
||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.