Skip to content

Commit

Permalink
Send gamepad and keyboard inputs from renderer to main
Browse files Browse the repository at this point in the history
  • Loading branch information
Hal-9k1 committed Oct 17, 2024
1 parent 350e88a commit 74128b1
Show file tree
Hide file tree
Showing 7 changed files with 176 additions and 7 deletions.
1 change: 1 addition & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
27 changes: 25 additions & 2 deletions src/common/IpcEventTypes.ts
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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
Expand Down Expand Up @@ -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[];
15 changes: 15 additions & 0 deletions src/main/MainApp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import type {
MainFileControlData,
MainQuitData,
MainUpdateRobotModeData,
MainRobotInputData,
} from '../common/IpcEventTypes';
import Config, { coerceToConfig } from './Config';
import CodeTransfer from './network/CodeTransfer';
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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

Check failure on line 215 in src/main/MainApp.ts

View workflow job for this annotation

GitHub Actions / test (ubuntu-latest)

Definition for rule 'no-unused' was not found
addRendererListener('main-robot-input', (inputs) => {

Check failure on line 216 in src/main/MainApp.ts

View workflow job for this annotation

GitHub Actions / test (ubuntu-latest)

'inputs' is defined but never used. Allowed unused args must match /^_/u
// TODO: send inputs to robot
});

try {
this.#config = coerceToConfig(
Expand Down
2 changes: 1 addition & 1 deletion src/main/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
};

Expand Down
5 changes: 5 additions & 0 deletions src/main/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import type {
RendererPostConsoleData,
RendererFileControlData,
MainQuitData,
MainRobotInputData,
} from '../common/IpcEventTypes';

function sendMessage(channel: 'main-quit', data: MainQuitData): void;
Expand All @@ -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);
}
Expand Down
79 changes: 75 additions & 4 deletions src/renderer/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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}
Expand All @@ -372,7 +443,7 @@ export default function App() {
setConsoleIsAlerted(false);
}}
onToggleKeyboardControls={() => {
setKbCtrlEnabled((v) => !v);
setKeyboardControlsEnabled((v) => !v);
}}
/>
<ResizeBar
Expand Down
54 changes: 54 additions & 0 deletions src/renderer/robotKeyNumberMap.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/**
* Assigns a numeric identifier to each valid input key that may be sent to a robot. Char codes are
* unsuitable for this purpose because keys are sent as a bitmap of a 53 bit float (Javascript's
* native number type).
*/
export default Object.freeze({
a: 0,
b: 1,
c: 2,
d: 3,
e: 4,
f: 5,
g: 6,
h: 7,
i: 8,
j: 9,
k: 10,
l: 11,
m: 12,
n: 13,
o: 14,
p: 15,
q: 16,
r: 17,
s: 18,
t: 19,
u: 20,
v: 21,
w: 22,
x: 23,
y: 24,
z: 25,
'1': 26,
'2': 27,
'3': 28,
'4': 29,
'5': 30,
'6': 31,
'7': 32,
'8': 33,
'9': 34,
'0': 35,
',': 36,
'.': 37,
'/': 38,
';': 39,
"'": 40,
'[': 41,
']': 42,
ArrowLeft: 43,
ArrowRight: 44,
ArrowUp: 45,
ArrowDown: 46,
}) as Readonly<{ [key: string]: number }>;

0 comments on commit 74128b1

Please sign in to comment.