Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
7 changes: 7 additions & 0 deletions hidrpc.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,13 @@ func handleHidRPCMessage(message hidrpc.Message, session *Session) {
return
}
rpcErr = rpcRelMouseReport(mouseReport.DX, mouseReport.DY, mouseReport.Button)
case hidrpc.TypeWheelReport:
wheelReport, err := message.WheelReport()
if err != nil {
logger.Warn().Err(err).Msg("failed to get wheel report")
return
}
rpcErr = rpcWheelReport(wheelReport.DeltaY, wheelReport.DeltaX)
default:
logger.Warn().Uint8("type", uint8(message.Type())).Msg("unknown HID RPC message type")
}
Expand Down
27 changes: 27 additions & 0 deletions internal/hidrpc/message.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,11 @@ func (m *Message) String() string {
return fmt.Sprintf("MouseReport{Malformed: %v}", m.d)
}
return fmt.Sprintf("MouseReport{DX: %d, DY: %d, Button: %d}", m.d[0], m.d[1], m.d[2])
case TypeWheelReport:
if len(m.d) < 2 {
return fmt.Sprintf("WheelReport{Malformed: %v}", m.d)
}
return fmt.Sprintf("WheelReport{DeltaY: %d, DeltaX: %d}", int8(m.d[0]), int8(m.d[1]))
case TypeKeypressKeepAliveReport:
return "KeypressKeepAliveReport"
case TypeKeyboardMacroReport:
Expand Down Expand Up @@ -189,6 +194,28 @@ func (m *Message) MouseReport() (MouseReport, error) {
}, nil
}

// WheelReport ..
type WheelReport struct {
DeltaY int8
DeltaX int8
}

// WheelReport returns the wheel report from the message.
func (m *Message) WheelReport() (WheelReport, error) {
if m.t != TypeWheelReport {
return WheelReport{}, fmt.Errorf("invalid message type: %d", m.t)
}

if len(m.d) != 2 {
return WheelReport{}, fmt.Errorf("invalid message length: %d", len(m.d))
}

return WheelReport{
DeltaY: int8(m.d[0]),
DeltaX: int8(m.d[1]),
}, nil
}

type KeyboardMacroState struct {
State bool
IsPaste bool
Expand Down
91 changes: 91 additions & 0 deletions internal/hidrpc/message_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package hidrpc

import "testing"

func TestWheelReport(t *testing.T) {
tests := []struct {
name string
message Message
wantDeltaY int8
wantDeltaX int8
wantErr bool
}{
{
name: "decode positive Y, zero X",
message: Message{t: TypeWheelReport, d: []byte{0x01, 0x00}},
wantDeltaY: 1,
wantDeltaX: 0,
},
{
name: "decode negative Y as two's complement",
message: Message{t: TypeWheelReport, d: []byte{0xFF, 0x00}},
wantDeltaY: -1,
wantDeltaX: 0,
},
{
name: "decode zero",
message: Message{t: TypeWheelReport, d: []byte{0x00, 0x00}},
wantDeltaY: 0,
wantDeltaX: 0,
},
{
name: "decode positive X, zero Y",
message: Message{t: TypeWheelReport, d: []byte{0x00, 0x01}},
wantDeltaY: 0,
wantDeltaX: 1,
},
{
name: "decode negative X as two's complement",
message: Message{t: TypeWheelReport, d: []byte{0x00, 0xFF}},
wantDeltaY: 0,
wantDeltaX: -1,
},
{
name: "decode both axes",
message: Message{t: TypeWheelReport, d: []byte{0x02, 0xFE}},
wantDeltaY: 2,
wantDeltaX: -2,
},
{
name: "wrong message type",
message: Message{t: TypeMouseReport, d: []byte{0x01, 0x02}},
wantErr: true,
},
{
name: "empty payload",
message: Message{t: TypeWheelReport, d: []byte{}},
wantErr: true,
},
{
name: "payload too short",
message: Message{t: TypeWheelReport, d: []byte{0x01}},
wantErr: true,
},
{
name: "payload too long",
message: Message{t: TypeWheelReport, d: []byte{0x01, 0x02, 0x03}},
wantErr: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := tt.message.WheelReport()
if tt.wantErr {
if err == nil {
t.Fatalf("WheelReport() expected error, got nil (deltaY=%d, deltaX=%d)", got.DeltaY, got.DeltaX)
}
return
}
if err != nil {
t.Fatalf("WheelReport() unexpected error: %v", err)
}
if got.DeltaY != tt.wantDeltaY {
t.Fatalf("WheelReport() DeltaY = %d, want %d", got.DeltaY, tt.wantDeltaY)
}
if got.DeltaX != tt.wantDeltaX {
t.Fatalf("WheelReport() DeltaX = %d, want %d", got.DeltaX, tt.wantDeltaX)
}
})
}
}
1 change: 0 additions & 1 deletion jsonrpc.go
Original file line number Diff line number Diff line change
Expand Up @@ -1344,7 +1344,6 @@ var rpcHandlers = map[string]RPCHandler{
"keypressReport": {Func: rpcKeypressReport, Params: []string{"key", "press"}},
"absMouseReport": {Func: rpcAbsMouseReport, Params: []string{"x", "y", "buttons"}},
"relMouseReport": {Func: rpcRelMouseReport, Params: []string{"dx", "dy", "buttons"}},
"wheelReport": {Func: rpcWheelReport, Params: []string{"wheelY", "wheelX"}},
"wakeHost": {Func: rpcWakeHost},
"getVideoState": {Func: rpcGetVideoState},
"getUSBState": {Func: rpcGetUSBState},
Expand Down
21 changes: 20 additions & 1 deletion ui/src/hooks/hidRpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,7 @@ export class KeyboardMacroReportMessage extends RpcMessage {
// Ensure the keys are within the KEYS_LENGTH range
const keys = step.keys;
if (keys.length > this.KEYS_LENGTH) {
throw new Error(`Keys ${keys} is not within the hidKeyBufferSize range`);
throw new Error(`Keys ${keys.join(",")} is not within the hidKeyBufferSize range`);
} else if (keys.length < this.KEYS_LENGTH) {
keys.push(...Array(this.KEYS_LENGTH - keys.length).fill(0));
}
Expand Down Expand Up @@ -400,6 +400,25 @@ export class KeypressKeepAliveMessage extends RpcMessage {
}
}

export class WheelReportMessage extends RpcMessage {
deltaY: number;
deltaX: number;

constructor(deltaY: number, deltaX: number) {
super(HID_RPC_MESSAGE_TYPES.WheelReport);
this.deltaY = deltaY;
this.deltaX = deltaX;
}

marshal(): Uint8Array {
return new Uint8Array([
this.messageType,
fromInt8ToUint8(this.deltaY),
fromInt8ToUint8(this.deltaX),
]);
}
}

export const messageRegistry = {
[HID_RPC_MESSAGE_TYPES.Handshake]: HandshakeMessage,
[HID_RPC_MESSAGE_TYPES.KeysDownState]: KeysDownStateMessage,
Expand Down
14 changes: 13 additions & 1 deletion ui/src/hooks/useHidRpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
MouseReportMessage,
PointerReportMessage,
RpcMessage,
WheelReportMessage,
unmarshalHidRpcMessage,
} from "./hidRpc";

Expand Down Expand Up @@ -273,6 +274,16 @@ export function useHidRpc(onHidRpcMessage?: (payload: RpcMessage) => void) {
[sendMessage],
);

const reportWheelEvent = useCallback(
(deltaY: number, deltaX: number) => {
// Wheel events are motion-like — a single dropped detent self-corrects
// via the next event, so we ride the unreliable-ordered channel for
// lower latency, matching how mouse motion is sent.
sendMessage(new WheelReportMessage(deltaY, deltaX), { useUnreliableChannel: true });
},
[sendMessage],
);

const reportKeyboardMacroEvent = useCallback(
(steps: KeyboardMacroStep[]) => {
sendMessage(new KeyboardMacroReportMessage(false, steps.length, steps));
Expand Down Expand Up @@ -314,7 +325,7 @@ export function useHidRpc(onHidRpcMessage?: (payload: RpcMessage) => void) {
};

const errorHandler = (e: Event) => {
console.error(`Error on rpcHidChannel '${rpcHidChannel.label}': ${e}`);
console.error(`Error on rpcHidChannel '${rpcHidChannel.label}': ${e.type}`);
};

rpcHidChannel.addEventListener("message", messageHandler);
Expand All @@ -331,6 +342,7 @@ export function useHidRpc(onHidRpcMessage?: (payload: RpcMessage) => void) {
reportKeypressEvent,
reportAbsMouseEvent,
reportRelMouseEvent,
reportWheelEvent,
reportKeyboardMacroEvent,
cancelOngoingKeyboardMacro,
reportKeypressKeepAlive,
Expand Down
6 changes: 3 additions & 3 deletions ui/src/hooks/useMouse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export default function useMouse() {

// RPC hooks
const { send } = useJsonRpc();
const { reportAbsMouseEvent, reportRelMouseEvent, rpcHidReady } = useHidRpc();
const { reportAbsMouseEvent, reportRelMouseEvent, reportWheelEvent, rpcHidReady } = useHidRpc();
// Mouse-related

const sendRelMouseMovement = useCallback(
Expand Down Expand Up @@ -142,15 +142,15 @@ export default function useMouse() {

if (wheelY === 0 && wheelX === 0) return;

send("wheelReport", { wheelY, wheelX });
reportWheelEvent(wheelY, wheelX);

// Apply blocking delay based of throttling settings
if (scrollThrottling && !blockWheelEvent) {
setBlockWheelEvent(true);
setTimeout(() => setBlockWheelEvent(false), scrollThrottling);
}
},
[send, blockWheelEvent, scrollThrottling, invertScroll],
[reportWheelEvent, blockWheelEvent, scrollThrottling, invertScroll],
);

const resetMousePosition = useCallback(() => {
Expand Down
Loading