From 74128b159fd17e1863e703243490e58f48e97170 Mon Sep 17 00:00:00 2001 From: Eddy Meals Date: Thu, 17 Oct 2024 14:28:06 -0700 Subject: [PATCH] Send gamepad and keyboard inputs from renderer to main --- .eslintrc.js | 1 + src/common/IpcEventTypes.ts | 27 ++++++++++- src/main/MainApp.ts | 15 ++++++ src/main/main.ts | 2 +- src/main/preload.ts | 5 ++ src/renderer/App.tsx | 79 +++++++++++++++++++++++++++++-- src/renderer/robotKeyNumberMap.ts | 54 +++++++++++++++++++++ 7 files changed, 176 insertions(+), 7 deletions(-) create mode 100644 src/renderer/robotKeyNumberMap.ts diff --git a/.eslintrc.js b/.eslintrc.js index 85089f7..50424ca 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -9,6 +9,7 @@ module.exports = { 'import/extensions': 'off', 'import/no-unresolved': 'off', 'import/no-import-module-exports': 'off', + 'no-bitwise': 'off', 'no-shadow': 'off', '@typescript-eslint/no-shadow': 'error', 'no-unused-vars': 'off', diff --git a/src/common/IpcEventTypes.ts b/src/common/IpcEventTypes.ts index 2d52295..f52d8eb 100644 --- a/src/common/IpcEventTypes.ts +++ b/src/common/IpcEventTypes.ts @@ -1,6 +1,9 @@ import type AppConsoleMessage from './AppConsoleMessage'; import type DeviceInfoState from './DeviceInfoState'; -import { Mode as RobotRunMode } from '../../protos-main/protos'; +import { + Mode as RobotRunMode, + Input as RobotInput, +} from '../../protos-main/protos'; /** * IPC event channels used for communication from the main process to the renderer. @@ -19,7 +22,8 @@ export type RendererChannels = export type MainChannels = | 'main-file-control' | 'main-quit' - | 'main-update-robot-mode'; + | 'main-update-robot-mode' + | 'main-robot-input'; /** * Data for the renderer-init event, sent when the renderer process has finished initializing and is @@ -305,3 +309,22 @@ export interface MainQuitData { * a specified opmode. */ export type MainUpdateRobotModeData = RobotRunMode; +/** + * Describes a single snapshot of gamepad or keyboard input. + */ +export interface MainRobotInputDatum { + /** + * The index of the gamepad that produced this input, or null if the input was produced by a + * keyboard. + */ + gamepadIndex: number | null; + /** + * The input, encoded as a protos Input object. + */ + input: RobotInput; +} +/** + * Data for the main-robot-input event sent by the renderer with gamepad or keyboard inputs bound + * for the robot. + */ +export type MainRobotInputData = MainRobotInputDatum[]; diff --git a/src/main/MainApp.ts b/src/main/MainApp.ts index 19a6310..98c2bed 100644 --- a/src/main/MainApp.ts +++ b/src/main/MainApp.ts @@ -16,6 +16,7 @@ import type { MainFileControlData, MainQuitData, MainUpdateRobotModeData, + MainRobotInputData, } from '../common/IpcEventTypes'; import Config, { coerceToConfig } from './Config'; import CodeTransfer from './network/CodeTransfer'; @@ -85,6 +86,16 @@ function addRendererListener( func: (data: MainUpdateRobotModeData) => void, ): void; +/** + * Adds a listener for the main-robot-input IPC event fired by the renderer. + * @param channel - the event channel to listen to + * @param func - the listener to attach + */ +function addRendererListener( + channel: 'main-robot-input', + func: (data: MainRobotInputData) => void, +): void; + /** * Typed wrapper function to listen to IPC events from the renderer. * @param channel - the event channel to listen to @@ -201,6 +212,10 @@ export default class MainApp implements MenuHandler, RuntimeCommsListener { addRendererListener('main-update-robot-mode', (mode) => { this.#runtimeComms.sendRunMode({ mode }); }); + // eslint-disable-next-line no-unused + addRendererListener('main-robot-input', (inputs) => { + // TODO: send inputs to robot + }); try { this.#config = coerceToConfig( diff --git a/src/main/main.ts b/src/main/main.ts index ac16122..caef645 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -104,7 +104,7 @@ const createWindow = async () => { }); // Remove this if your app does not use auto updates - // eslint-disable-next-line + // eslint-disable-next-line no-new new AppUpdater(); }; diff --git a/src/main/preload.ts b/src/main/preload.ts index bdcc80b..53b9682 100644 --- a/src/main/preload.ts +++ b/src/main/preload.ts @@ -13,6 +13,7 @@ import type { RendererPostConsoleData, RendererFileControlData, MainQuitData, + MainRobotInputData, } from '../common/IpcEventTypes'; function sendMessage(channel: 'main-quit', data: MainQuitData): void; @@ -24,6 +25,10 @@ function sendMessage( channel: 'main-update-robot-mode', data: MainUpdateRobotModeData, ): void; +function sendMessage( + channel: 'main-robot-input', + data: MainRobotInputData, +): void; function sendMessage(channel: MainChannels, data?: any): void { ipcRenderer.send(channel, data); } diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index b6d9769..67adbc5 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -18,7 +18,12 @@ import ConnectionConfigModal, { } from './modals/ConnectionConfigModal'; import GamepadInfoModal from './modals/GamepadInfoModal'; import ResizeBar from './ResizeBar'; -import { Mode as RobotRunMode } from '../../protos-main/protos'; +import { + Mode as RobotRunMode, + Source as RobotInputSource, + Input as RobotInput, +} from '../../protos-main/protos'; +import robotKeyNumberMap from './robotKeyNumberMap'; import './App.css'; const INITIAL_EDITOR_WIDTH_PERCENT = 0.7; @@ -27,6 +32,7 @@ const MAX_EDITOR_WIDTH_PERCENT = 0.9; const MAX_CONSOLE_HEIGHT_PERCENT = 0.6; const MIN_EDITOR_WIDTH_PERCENT = 0.6; const MIN_CONSOLE_HEIGHT_PERCENT = 0.3; +const GAMEPAD_UPDATE_PERIOD_MS = 50; /** * Top-level component that communicates with main process and contains most renderer state. @@ -66,7 +72,7 @@ export default function App() { // closed) const [consoleIsAlerted, setConsoleIsAlerted] = useState(false); // Whether keyboard controls are enabled - const [kbCtrlEnabled, setKbCtrlEnabled] = useState(false); + const [keyboardControlsEnabled, setKeyboardControlsEnabled] = useState(false); // Whether the robot is running student code const [robotRunning, setRobotRunning] = useState(false); // Most recent window.innerWidth/Height needed to clamp editor and col size @@ -238,6 +244,71 @@ export default function App() { onResize(); return () => window.removeEventListener('resize', onResize); }, []); + useEffect(() => { + // Storing bitmap as closure useful side effect of resetting bitmap to 0 when kb ctrl toggled + // Also using bigint so bitwise operators don't wrap to 32 bits + let keyboardBitmap = 0n; + const onKeyChange = (key: string, down: boolean) => { + if (key in robotKeyNumberMap) { + const bit = 1n << BigInt(robotKeyNumberMap[key]); + if (down) { + keyboardBitmap |= bit; + } else { + keyboardBitmap &= ~bit; + } + } + }; + const onKeyDown = ({ key }: { key: string }) => onKeyChange(key, true); + window.addEventListener('keydown', onKeyDown); + const onKeyUp = ({ key }: { key: string }) => onKeyChange(key, false); + window.addEventListener('keyup', onKeyUp); + const gamepadUpdateInterval = setInterval(() => { + const gamepadInputs = navigator + .getGamepads() + // Preserve indices before filtering: + .map((gp, idx) => ({ gp, idx })) + // Filter null and 'ghost' gamepads: + .filter( + (obj): obj is { gp: Gamepad; idx: number } => + obj.gp !== null && obj.gp.mapping === 'standard', + ) + .map(({ gp, idx }: { gp: Gamepad; idx: number }) => { + let buttonBitmap: number = 0; + gp.buttons.forEach((button, buttonIdx) => { + if (button.pressed) { + buttonBitmap |= 1 << buttonIdx; + } + }); + return { + gamepadIndex: idx, + input: new RobotInput({ + connected: gp.connected, + axes: gp.axes.slice(), + buttons: buttonBitmap, + source: RobotInputSource.GAMEPAD, + }), + }; + }); + const keyboardInput = { + gamepadIndex: null, + input: new RobotInput({ + connected: keyboardControlsEnabled, + axes: [], + buttons: keyboardControlsEnabled ? Number(keyboardBitmap) : 0, + source: RobotInputSource.KEYBOARD, + }), + }; + window.electron.ipcRenderer.sendMessage('main-robot-input', [ + ...gamepadInputs, + keyboardInput, + ]); + }, GAMEPAD_UPDATE_PERIOD_MS); + return () => { + clearInterval(gamepadUpdateInterval); + window.removeEventListener('keydown', onKeyDown); + window.removeEventListener('keyup', onKeyUp); + }; + }, [keyboardControlsEnabled]); useEffect(() => { // Tests won't run main/preload.ts if (window.electron) { @@ -349,7 +420,7 @@ export default function App() { content={editorContent} consoleAlert={consoleIsAlerted} consoleIsOpen={consoleIsOpen} - keyboardControlsEnabled={kbCtrlEnabled} + keyboardControlsEnabled={keyboardControlsEnabled} robotConnected={robotLatencyMs !== -1} robotRunning={robotRunning} onOpen={loadFile} @@ -372,7 +443,7 @@ export default function App() { setConsoleIsAlerted(false); }} onToggleKeyboardControls={() => { - setKbCtrlEnabled((v) => !v); + setKeyboardControlsEnabled((v) => !v); }} /> ;