diff --git a/main/networking/Runtime.ts b/main/networking/Runtime.ts index 44f18237..55494d69 100644 --- a/main/networking/Runtime.ts +++ b/main/networking/Runtime.ts @@ -1,11 +1,10 @@ -import { createSocket, Socket as UDPSocket } from 'dgram'; import { Socket as TCPSocket } from 'net'; import { ipcMain, IpcMainEvent } from 'electron'; import * as protos from '../../protos-main'; import RendererBridge from '../RendererBridge'; import { updateConsole } from '../../renderer/actions/ConsoleActions'; -import { runtimeDisconnect, infoPerMessage } from '../../renderer/actions/InfoActions'; +import { infoPerMessage } from '../../renderer/actions/InfoActions'; import { updatePeripherals } from '../../renderer/actions/PeripheralActions'; import { Logger, defaults } from '../../renderer/utils/utils'; import { Peripheral } from '../../renderer/types'; @@ -14,8 +13,6 @@ import { setLatencyValue } from '../../renderer/actions/EditorActions'; /** * Define port constants, which must match with Runtime */ -const UDP_SEND_PORT = 9000; -const UDP_LISTEN_PORT = 9001; const DEFAULT_TCP_PORT = 8101; /** @@ -29,12 +26,11 @@ let runtimeIP = defaults.IPADDRESS; enum MsgType { RUN_MODE = 0, START_POS = 1, - CHALLENGE_DATA = 2, - LOG = 3, - DEVICE_DATA = 4, - // 5 reserved for some Shepherd msg type - INPUTS = 6, - TIME_STAMPS = 7 + LOG = 2, + DEVICE_DATA = 3, + // 4 reserved for some Shepherd msg type + INPUTS = 5, + TIME_STAMPS = 6 } interface TCPPacket { @@ -46,29 +42,29 @@ interface TCPPacket { /** Given a data buffer, read as many TCP Packets as possible. * If there are leftover bytes, return them so that they can be used in the next cycle of data. */ -function readPackets( - data: Buffer, - previousLeftoverBytes?: Buffer -): { leftoverBytes: Buffer | undefined; processedTCPPackets: TCPPacket[] } { +function readPackets(data: Buffer, previousLeftoverBytes?: Buffer): { leftoverBytes?: Buffer; processedTCPPackets: TCPPacket[] } { + const HEADER_NUM_BYTES = 3; + const bytesToRead = Buffer.concat([previousLeftoverBytes ?? new Uint8Array(), data]); - let leftoverBytes; - let i = 0; const processedTCPPackets: TCPPacket[] = []; - while (i < bytesToRead.length) { - let header; - let msgType; - let msgLength; + let leftoverBytes; + let currentPos = 0; + + while (currentPos < bytesToRead.length) { + let header: Buffer; + let msgType: number; + let msgLength: number; let payload: Buffer; - if (i + 3 <= bytesToRead.length) { + if (currentPos + HEADER_NUM_BYTES <= bytesToRead.length) { // Have enough bytes to read in 3 byte header - header = bytesToRead.slice(i, i + 3); + header = bytesToRead.slice(currentPos, currentPos + HEADER_NUM_BYTES); msgType = header[0]; msgLength = (header[2] << 8) | header[1]; } else { // Don't have enough bytes to read 3 byte header so we save the bytes for the next data cycle - leftoverBytes = bytesToRead.slice(i); + leftoverBytes = bytesToRead.slice(currentPos); return { leftoverBytes, @@ -76,14 +72,14 @@ function readPackets( }; } - i += 3; + currentPos += HEADER_NUM_BYTES; - if (i + msgLength <= bytesToRead.length) { + if (currentPos + msgLength <= bytesToRead.length) { // Have enough bytes to read entire payload from 1 TCP packet - payload = bytesToRead.slice(i, i + msgLength); + payload = bytesToRead.slice(currentPos, currentPos + msgLength); } else { // Don't have enough bytes to read entire payload - leftoverBytes = bytesToRead.slice(i); + leftoverBytes = bytesToRead.slice(currentPos); return { // Note: Need to save header so we know how many bytes to read for this packet in the next data cycle @@ -92,10 +88,10 @@ function readPackets( }; } - const newTCPPacket = { type: msgType, length: msgLength, payload: payload! }; + const newTCPPacket = { type: msgType, length: msgLength, payload }; processedTCPPackets.push(newTCPPacket); - i += msgLength; + currentPos += msgLength; } return { @@ -124,15 +120,11 @@ function createPacket(payload: unknown, messageType: MsgType): Buffer { case MsgType.TIME_STAMPS: encodedPayload = protos.TimeStamps.encode(payload as protos.ITimeStamps).finish(); break; - case MsgType.CHALLENGE_DATA: - encodedPayload = protos.Text.encode(payload as protos.IText).finish(); - break; case MsgType.INPUTS: - // Special case for 2021 competition where Input data is sent over tunneled TCP connection - encodedPayload = payload as Uint8Array; + encodedPayload = protos.UserInputs.encode({ inputs: payload as protos.Input[] }).finish(); break; default: - console.log('ERROR: trying to create TCP Packet with type LOG'); + console.log('ERROR: trying to create TCP Packet with unknown message type'); encodedPayload = new Uint8Array(); break; } @@ -144,102 +136,7 @@ function createPacket(payload: unknown, messageType: MsgType): Buffer { return Buffer.concat([msgTypeArr, msgLengthArr, encodedPayload], msgLength + 3); } -/** Uses TCP connection to tunnel UDP messages. */ -class UDPTunneledConn { - /* Leftover bytes from reading 1 cycle of the TCP data buffer. */ - leftoverBytes: Buffer | undefined; - logger: Logger; - tcpSocket: TCPSocket; - udpForwarder: UDPSocket; - ip: string; - port: number; - - constructor(logger: Logger) { - this.logger = logger; - this.ip = defaults.IPADDRESS; - - this.tcpSocket = new TCPSocket(); - - // Connect to most recent IP - setInterval(() => { - if (!this.tcpSocket.connecting && this.tcpSocket.pending) { - if (this.ip !== defaults.IPADDRESS) { - if (this.ip.includes(':')) { - const split = this.ip.split(':'); - this.ip = split[0]; - this.port = Number(split[1]); - } - console.log(`UDPTunneledConn: Trying to TCP connect to ${this.ip}:${this.port}`); - this.tcpSocket.connect(this.port, this.ip); - } - } - }, 1000); - - this.tcpSocket.on('connect', () => { - this.logger.log(`UDPTunneledConn connected`); - }); - - this.tcpSocket.on('end', () => { - this.logger.log(`UDPTunneledConn disconnected`); - }); - - this.tcpSocket.on('error', (err: string) => { - this.logger.log(err); - }); - - this.tcpSocket.on('data', (data) => { - const { leftoverBytes, processedTCPPackets } = readPackets(data, this.leftoverBytes); - - try { - for (const packet of processedTCPPackets) { - // Send to UDP Connection - udpForwarder.send(packet.payload, 0, packet.payload.length, UDP_LISTEN_PORT, 'localhost'); - } - - this.leftoverBytes = leftoverBytes; - } catch (e) { - this.logger.log('UDPTunneledConn udpForwarder failed to send to UDP connection: ' + String(e)); - } - }); - - /* Bidirectional - Can send to and receive from UDP connection. */ - const udpForwarder = createSocket({ type: 'udp4', reuseAddr: true }); - - udpForwarder.bind(UDP_SEND_PORT, () => { - console.log(`UDP forwarder receives from port ${UDP_SEND_PORT}`); - }); - - // Received a new message from UDP connection - udpForwarder.on('message', (msg: Uint8Array) => { - const message = createPacket(msg, MsgType.INPUTS); - this.tcpSocket.write(message); - }); - - this.udpForwarder = udpForwarder; - - ipcMain.on('udpTunnelIpAddress', this.ipAddressListener); - } - - ipAddressListener = (_event: IpcMainEvent, ipAddress: string) => { - if (ipAddress != this.ip) { - console.log(`UDPTunneledConn - Switching IP from ${this.ip} to ${ipAddress}`); - if (this.tcpSocket.connecting || !this.tcpSocket.pending) { - this.tcpSocket.end(); - } - this.ip = ipAddress; - } - }; - - close = () => { - if (!this.tcpSocket.pending) { - this.tcpSocket.end(); - } - this.udpForwarder.close(); - ipcMain.removeListener('udpTunnelIpAddress', this.ipAddressListener); - }; -} - -class TCPConn { +class RuntimeConnection { logger: Logger; socket: TCPSocket; leftoverBytes: Buffer | undefined; @@ -259,7 +156,7 @@ class TCPConn { ip = split[0]; port = Number(split[1]); } - console.log(`TCPConn: Trying to TCP connect to ${ip}:${port}`); + console.log(`RuntimeConnection: Trying to TCP connect to ${ip}:${port}`); this.socket.connect(port, ip); } } @@ -271,6 +168,7 @@ class TCPConn { }); this.socket.on('end', () => { + // RendererBridge.reduxDispatch(runtimeDisconnect()); this.logger.log('Runtime disconnected'); }); @@ -294,17 +192,42 @@ class TCPConn { decoded = protos.Text.decode(packet.payload); RendererBridge.reduxDispatch(updateConsole(decoded.payload)); break; + case MsgType.TIME_STAMPS: decoded = protos.TimeStamps.decode(packet.payload); const oneWayLatency = (Date.now() - Number(decoded.dawnTimestamp)) / 2; // TODO: we can probably do an average of n timestamps so the display doesn't change too frequently - RendererBridge.reduxDispatch(setLatencyValue(oneWayLatency)); break; - case MsgType.CHALLENGE_DATA: - // TODO: Dispatch challenge outputs to redux + + case MsgType.DEVICE_DATA: + try { + RendererBridge.reduxDispatch(infoPerMessage()); + const sensorData: protos.Device[] = protos.DevData.decode(packet.payload).devices; + const peripherals: Peripheral[] = []; + + // Need to convert protos.Device to Peripheral here because when dispatching to the renderer over IPC, + // some of the inner properties (i.e. device.uid which is a Long) loses its prototype, which means any + // data we are sending over through IPC should be serializable. + // https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm + sensorData.forEach((device) => { + // There is a weird bug that happens with the protobufs decoding when device.type is specifically 0 + // where the property can be accessed but when trying to view object contents, the property doesn't exist. + // Below is a way to get around this problem. + if (device.type.toString() === '0') { + device.type = 0; + } + peripherals.push({ ...device, uid: device.uid.toString() }); + }); + RendererBridge.reduxDispatch(updatePeripherals(peripherals)); + } catch (err) { + this.logger.log(err); + } break; + + default: + this.logger.log(`Unsupported received message type: ${packet.type}`) } } @@ -314,19 +237,27 @@ class TCPConn { /** * TCP Socket IPC Connections */ - ipcMain.on('runModeUpdate', this.sendRunMode); + ipcMain.on('runModeUpdate', (event: IpcMainEvent, ...args: any[]) => this.whenConnectionEstablished(this.sendRunMode, event, ...args)); + ipcMain.on('initiateLatencyCheck', (event: IpcMainEvent, ...args: any[]) => + this.whenConnectionEstablished(this.initiateLatencyCheck, event, ...args) + ); + ipcMain.on('stateUpdate', (event: IpcMainEvent, ...args: any[]) => this.whenConnectionEstablished(this.sendInputs, event, ...args)); + ipcMain.on('ipAddress', this.ipAddressListener); - ipcMain.on('initiateLatencyCheck', this.initiateLatencyCheck); } - /** - * Initiates latency check by sending first packet to Runtime - */ - initiateLatencyCheck = (_event: IpcMainEvent, data: protos.ITimeStamps) => { + whenConnectionEstablished = (cb: (event: IpcMainEvent, ...args: any[]) => void, event: IpcMainEvent, ...args: any[]) => { if (this.socket.pending) { return; } + return cb(event, ...args); + }; + + /** + * Initiates latency check by sending first packet to Runtime + */ + initiateLatencyCheck = (_event: IpcMainEvent, data: protos.ITimeStamps) => { const message = createPacket(data, MsgType.TIME_STAMPS); this.socket.write(message, () => { this.logger.log(`Sent timestamp data to runtime: ${JSON.stringify(data)}`); @@ -339,7 +270,7 @@ class TCPConn { */ ipAddressListener = (_event: IpcMainEvent, ipAddress: string) => { if (ipAddress != runtimeIP) { - console.log(`TCPConn - Switching IP from ${runtimeIP} to ${ipAddress}`); + console.log(`RuntimeConnection - Switching IP from ${runtimeIP} to ${ipAddress}`); if (this.socket.connecting || !this.socket.pending) { this.socket.end(); } @@ -354,10 +285,6 @@ class TCPConn { * Receives new run mode to send to Runtime */ sendRunMode = (_event: IpcMainEvent, runModeData: protos.IRunMode) => { - if (this.socket.pending) { - return; - } - const message = createPacket(runModeData, MsgType.RUN_MODE); this.socket.write(message, () => { this.logger.log(`Run Mode message sent: ${JSON.stringify(runModeData)}`); @@ -365,11 +292,6 @@ class TCPConn { }; sendDevicePreferences = (_event: IpcMainEvent, deviceData: protos.IDevData) => { - // TODO: Get device preference filter from UI components, then sagas - if (this.socket.pending) { - return; - } - // TODO: Serialize uid from string -> Long type const message = createPacket(deviceData, MsgType.DEVICE_DATA); this.socket.write(message, () => { @@ -377,108 +299,14 @@ class TCPConn { }); }; - sendChallengeInputs = (_event: IpcMainEvent, textData: protos.IText) => { - // TODO: Get challenge inputs from UI components, then sagas - if (this.socket.pending) { - return; - } - - const message = createPacket(textData, MsgType.CHALLENGE_DATA); - this.socket.write(message, () => { - this.logger.log(`Challenge inputs sent: ${textData.toString()}`); - }); - }; - sendRobotStartPos = (_event: IpcMainEvent, startPosData: protos.IStartPos) => { // TODO: Get start pos from sagas - if (this.socket.pending) { - return; - } - const message = createPacket(startPosData, MsgType.START_POS); this.socket.write(message, () => { this.logger.log(`Start position sent: ${startPosData.toString()}`); }); }; - close = () => { - this.socket.end(); - ipcMain.removeListener('runModeUpdate', this.sendRunMode); - ipcMain.removeListener('ipAddress', this.ipAddressListener); - ipcMain.removeListener('initiateLatencyCheck', this.initiateLatencyCheck); - }; -} - -/** - * UDPConn contains socket methods for both sending to and receiving from Runtime. - */ -class UDPConn { - logger: Logger; - socket: UDPSocket; - - constructor(logger: Logger) { - this.logger = logger; - - this.socket = createSocket({ type: 'udp4', reuseAddr: true }); - - this.socket.on('error', (err: string) => { - this.logger.log('UDP connection error'); - this.logger.log(err); - }); - - this.socket.on('close', () => { - RendererBridge.reduxDispatch(runtimeDisconnect()); - this.logger.log('UDP connection closed'); - }); - - /** - * Runtime UDP Message Handler. - * In other words, this is where we handle data that we receive from Runtime. - * Sets runtime connection, decodes device message, cleans UIDs from uint64, and sends sensor data array to reducer. - */ - this.socket.on('message', (msg: Uint8Array) => { - try { - RendererBridge.reduxDispatch(infoPerMessage()); - const sensorData: protos.Device[] = protos.DevData.decode(msg).devices; - // Need to convert protos.Device to Peripheral here because when dispatching to the renderer over IPC, - // some of the inner properties (i.e. device.uid which is a Long) loses its prototype, which means any - // data we are sending over through IPC should be serializable. - // https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm - const peripherals: Peripheral[] = []; - - sensorData.forEach((device) => { - // There is a weird bug that happens with the protobufs decoding when device.type is specifically 0 - // where the property can be accessed but when trying to view object contents, the property doesn't exist. - // Below is a way to get around this problem. - if (device.type.toString() === '0') { - device.type = 0; - } - - peripherals.push({ ...device, uid: device.uid.toString() }); - }); - - RendererBridge.reduxDispatch(updatePeripherals(peripherals)); - } catch (err) { - this.logger.log('Error decoding UDP'); - this.logger.log(err); - } - }); - - this.socket.bind(UDP_LISTEN_PORT, 'localhost', () => { - this.logger.log(`UDP connection bound`); - }); - - /** - * UDP Send Socket IPC Connections - */ - ipcMain.on('stateUpdate', this.sendInputs); - } - - /** - * IPC Connection with sagas.ts' runtimeGamepads() - * Sends messages when Gamepad information changes - * or when 100 ms has passed (with 50 ms cooldown) - */ sendInputs = (_event: IpcMainEvent, data: protos.Input[], source: protos.Source) => { if (data.length === 0) { data.push( @@ -488,27 +316,31 @@ class UDPConn { }) ); } - - const message = protos.UserInputs.encode({ inputs: data }).finish(); - - // Change IP to use runtimeIP again after 2021 Competition - this.socket.send(message, UDP_SEND_PORT, 'localhost'); + const message = createPacket(data, MsgType.INPUTS); + this.socket.write(message, (err?: Error) => { + if (err !== undefined) { + this.logger.log(`Error when sending inputs: ${JSON.stringify(err)}`); + } + }); }; - close() { - this.socket.close(); + close = () => { + this.socket.end(); + ipcMain.removeListener('runModeUpdate', this.sendRunMode); + ipcMain.removeListener('ipAddress', this.ipAddressListener); + ipcMain.removeListener('initiateLatencyCheck', this.initiateLatencyCheck); ipcMain.removeListener('stateUpdate', this.sendInputs); - } + }; } -const RuntimeConnections: Array = []; +const RuntimeConnections: Array = []; export const Runtime = { conns: RuntimeConnections, logger: new Logger('runtime', 'Runtime Debug'), setup() { - this.conns = [new UDPConn(this.logger), new TCPConn(this.logger), new UDPTunneledConn(this.logger)]; + this.conns = [new RuntimeConnection(this.logger)]; }, close() { diff --git a/renderer/actions/InfoActions.ts b/renderer/actions/InfoActions.ts index 2f7c7b63..9fe59e93 100644 --- a/renderer/actions/InfoActions.ts +++ b/renderer/actions/InfoActions.ts @@ -27,11 +27,6 @@ export const ipChange: InfoActions['ipChange'] = (ipAddress: string) => ({ ipAddress }); -export const udpTunnelIpChange = (ipAddress: string) => ({ - type: consts.InfoActionsTypes.UDP_TUNNEL_IP_CHANGE, - ipAddress -}); - export const sshIpChange = (ipAddress: string) => ({ type: consts.InfoActionsTypes.SSH_IP_CHANGE, ipAddress diff --git a/renderer/components/ConfigBox.tsx b/renderer/components/ConfigBox.tsx index 0235dca5..e3924dd7 100644 --- a/renderer/components/ConfigBox.tsx +++ b/renderer/components/ConfigBox.tsx @@ -6,7 +6,7 @@ import { Dispatch } from 'redux'; import _ from 'lodash'; import { defaults, getValidationState, logging, isValidationState } from '../utils/utils'; import { updateFieldControl } from '../actions/FieldActions'; -import { ipChange, udpTunnelIpChange, sshIpChange } from '../actions/InfoActions'; +import { ipChange, sshIpChange } from '../actions/InfoActions'; import storage from 'electron-json-storage'; import { Formik } from 'formik'; @@ -18,13 +18,11 @@ interface Config { interface StateProps { stationNumber: number; ipAddress: string; - udpTunnelAddress: string; sshAddress: string; } interface DispatchProps { onIPChange: (ipAddress: string) => void; - onUDPTunnelingIpAddressChange: (ipAddress: string) => void; onSSHAddressChange: (ipAddress: string) => void; onFCUpdate: (config: Config) => void; } @@ -41,12 +39,10 @@ type FormControlElement = HTMLInputElement | HTMLSelectElement | HTMLTextAreaEle export const ConfigBoxComponent = (props: Props) => { const [ipAddress, setIPAddress] = useState(props.ipAddress); - const [udpTunnelIpAddress, setUDPTunnelIpAddress] = useState(props.udpTunnelAddress); const [sshAddress, setSSHAddress] = useState(props.sshAddress); const [fcAddress, setFCAddress] = useState(props.fcAddress); const [stationNumber, setStationNumber] = useState(props.stationNumber); const [originalIPAddress, setOriginalIPAddress] = useState(props.ipAddress); - const [originalUDPTunnelIpAddress, setOriginalUDPTunnelIpAddress] = useState(props.udpTunnelAddress); const [originalSSHAddress, setOriginalSSHAddress] = useState(props.sshAddress); const [originalStationNumber, setOriginalStationNumber] = useState(props.stationNumber); const [originalFCAddress, setOriginalFCAddress] = useState(props.fcAddress); @@ -54,12 +50,6 @@ export const ConfigBoxComponent = (props: Props) => { const saveChanges = (e: React.FormEvent) => { e.preventDefault(); - props.onUDPTunnelingIpAddressChange(udpTunnelIpAddress); - setOriginalFCAddress(udpTunnelIpAddress); - storage.set('udpTunnelIpAddress', { udpTunnelIpAddress }, (err: any) => { - if (err) logging.log(err); - }); - props.onIPChange(ipAddress); setOriginalIPAddress(ipAddress); storage.set('ipAddress', { ipAddress }, (err: any) => { @@ -94,10 +84,6 @@ export const ConfigBoxComponent = (props: Props) => { setIPAddress(e.currentTarget.value); }; - const handleUDPTunnelIpChange = (e: React.FormEvent) => { - setUDPTunnelIpAddress(e.currentTarget.value); - }; - const handleSSHIpChange = (e: React.FormEvent) => { setSSHAddress(e.currentTarget.value); } @@ -114,7 +100,6 @@ export const ConfigBoxComponent = (props: Props) => { setIPAddress(originalIPAddress); setStationNumber(originalStationNumber); setFCAddress(originalFCAddress); - setUDPTunnelIpAddress(originalUDPTunnelIpAddress); setSSHAddress(originalSSHAddress); props.hide(); }; @@ -139,18 +124,6 @@ export const ConfigBoxComponent = (props: Props) => { } }); - storage.get('udpTunnelIpAddress', (err: any, data: object) => { - if (err) { - logging.log(err); - } else if (!_.isEmpty(data)) { - const udpTunnelIpAddress = (data as { udpTunnelIpAddress: string | undefined }).udpTunnelIpAddress ?? defaults.IPADDRESS; - - props.onUDPTunnelingIpAddressChange(udpTunnelIpAddress); - setUDPTunnelIpAddress(udpTunnelIpAddress); - setOriginalUDPTunnelIpAddress(udpTunnelIpAddress); - } - }); - storage.get('sshAddress', (err: any, data: object) => { if (err) { logging.log(err); @@ -203,12 +176,6 @@ export const ConfigBoxComponent = (props: Props) => { - - UDP Tunneling - - - - SSH Address @@ -243,9 +210,6 @@ const mapDispatchToProps = (dispatch: Dispatch) => ({ onIPChange: (ipAddress: string) => { dispatch(ipChange(ipAddress)); }, - onUDPTunnelingIpAddressChange: (ipAddress: string) => { - dispatch(udpTunnelIpChange(ipAddress)); - }, onSSHAddressChange: (ipAddress: string) => { dispatch(sshIpChange(ipAddress)); }, diff --git a/renderer/components/DNav.tsx b/renderer/components/DNav.tsx index 55cdb91c..873dace4 100644 --- a/renderer/components/DNav.tsx +++ b/renderer/components/DNav.tsx @@ -15,7 +15,6 @@ interface StateProps { blueMasterTeamNumber: number; goldMasterTeamNumber: number; ipAddress: string; - udpTunnelAddress: string; sshAddress: string; fieldControlStatus: boolean; latencyValue: number; @@ -77,7 +76,6 @@ const DNavComponent = (props: Props) => { masterStatus, isRunningCode, ipAddress, - udpTunnelAddress, sshAddress, runtimeVersion, codeStatus, @@ -101,7 +99,6 @@ const DNavComponent = (props: Props) => { toggleConfigModal(!showConfigModal)} /> @@ -117,6 +114,7 @@ const DNavComponent = (props: Props) => { ) : ( '' )} +
{ fieldControlStatus={fieldControlStatus} /> +
{`Latency: ${formatLatencyValue(props.latencyValue)}`} @@ -176,7 +175,6 @@ const mapStateToProps = (state: ApplicationState) => ({ codeStatus: state.info.studentCodeStatus, heart: state.fieldStore.heart, ipAddress: state.info.ipAddress, - udpTunnelAddress: state.info.udpTunnelIpAddress, sshAddress: state.info.sshAddress, fieldControlStatus: state.fieldStore.fieldControl, runtimeVersion: state.peripherals.runtimeVersion, diff --git a/renderer/components/Gamepad.tsx b/renderer/components/Gamepad.tsx index e48ec571..f069f3ba 100644 --- a/renderer/components/Gamepad.tsx +++ b/renderer/components/Gamepad.tsx @@ -89,7 +89,7 @@ export const Gamepad = (props: Props) => { Value {_.range(NUM_GAMEPAD_AXES).map((gamepadButtonAxis: number) => ( - {values.buttons[gamepadButtonAxis]} + {values.axes[gamepadButtonAxis]} ))} diff --git a/renderer/components/PeripheralGroup.tsx b/renderer/components/PeripheralGroup.tsx index 979949a3..92843a8c 100644 --- a/renderer/components/PeripheralGroup.tsx +++ b/renderer/components/PeripheralGroup.tsx @@ -21,39 +21,39 @@ cleanerNames[PeripheralTypes.GameValues] = 'Game Values'; cleanerNames[PeripheralTypes.PolarBear] = 'Polar Bear'; cleanerNames[PeripheralTypes.KoalaBear] = 'Koala Bear'; -interface PGProps { - peripherals: Peripheral[], - groupName: string, +interface PeripheralGroupProps { + peripherals: Peripheral[]; + groupName: string; } -const PeripheralGroup = (props: PGProps) => { - const [out, setOut] = useState(true); // controls toggle +const PeripheralGroup = (props: PeripheralGroupProps) => { + const [out, setOut] = useState(false); // controls toggle - const { peripherals, groupName } = props; - const groupNameCleaned = groupName; //cleanerNames[groupName] as string; + const { peripherals, groupName } = props; + const groupNameCleaned = groupName; //cleanerNames[groupName] as string; - return ( - - - setOut(!out)} style={{ fontWeight: 'bold' }}> - {groupName || 'Generic'} - - - - - {_.map(peripherals, (peripheral: Peripheral) => ( - - ))} - - - - ) -} + return ( + + + setOut(!out)} style={{ fontWeight: 'bold' }}> + {groupName || 'Generic'} + + + + + {_.map(peripherals, (peripheral: Peripheral) => ( + + ))} + + + + ); +}; -export default PeripheralGroup; \ No newline at end of file +export default PeripheralGroup; diff --git a/renderer/hooks/editor/useKeyboardMode.tsx b/renderer/hooks/editor/useKeyboardMode.tsx index 2e16f017..7252c3ff 100644 --- a/renderer/hooks/editor/useKeyboardMode.tsx +++ b/renderer/hooks/editor/useKeyboardMode.tsx @@ -1,5 +1,5 @@ import _ from 'lodash'; -import { useCallback, useEffect, useState } from 'react'; +import { useCallback, useEffect, useRef, useState } from 'react'; import { keyboardButtons } from '../../consts'; interface Props { @@ -10,6 +10,8 @@ interface Props { export const useKeyboardMode = (props: Props) => { const [isKeyboardModeToggled, setIsKeyboardModeToggled] = useState(false); const [keyboardBitmap, setKeyboardBitmap] = useState(0); + const keyboardBitmapRef = useRef(0); + keyboardBitmapRef.current = keyboardBitmap; // toggle keyboard control and add/remove listening for key presses to control robot const toggleKeyboardControl = () => { @@ -25,6 +27,7 @@ export const useKeyboardMode = (props: Props) => { } else { window.removeEventListener('keydown', turnCharacterOn); window.removeEventListener('keyup', turnCharacterOff); + setKeyboardBitmap(0); } return () => { @@ -39,7 +42,7 @@ export const useKeyboardMode = (props: Props) => { const updateKeyboardBitmap = useCallback((currentCharacter: string, isKeyPressed: boolean) => { const keyboardNum = keyboardButtons[currentCharacter] as number; - let newKeyboardBitmap: number = keyboardBitmap; + let newKeyboardBitmap: number = keyboardBitmapRef.current; const shift = bitShiftLeft(1, keyboardNum); const MAX_INT32_BITS = 2147483648; // 2^31 @@ -56,7 +59,7 @@ export const useKeyboardMode = (props: Props) => { } setKeyboardBitmap(newKeyboardBitmap); - props.onUpdateKeyboardBitmap(keyboardBitmap); + props.onUpdateKeyboardBitmap(newKeyboardBitmap); }, []); const turnCharacterOff = useCallback( @@ -68,7 +71,12 @@ export const useKeyboardMode = (props: Props) => { const turnCharacterOn = useCallback( (e: KeyboardEvent) => { - updateKeyboardBitmap(e.key, true); + // Handle special ctrl + q edge case + if (e.ctrlKey && e.key === 'q') { + setIsKeyboardModeToggled(false); + } else { + updateKeyboardBitmap(e.key, true); + } }, [updateKeyboardBitmap] ); diff --git a/renderer/reducers/info.ts b/renderer/reducers/info.ts index 0395a5dc..88c04d27 100644 --- a/renderer/reducers/info.ts +++ b/renderer/reducers/info.ts @@ -8,7 +8,6 @@ import { RuntimeDisconnectAction, UpdateCodeStatusAction, IpChangeAction, - UDPTunnelIpChangeAction, SSHIpChangeAction, UpdateRobotAction, NotificationChangeAction, @@ -21,14 +20,12 @@ type Actions = | RuntimeDisconnectAction | UpdateCodeStatusAction | IpChangeAction - | UDPTunnelIpChangeAction | SSHIpChangeAction | UpdateRobotAction | NotificationChangeAction; interface InfoState { ipAddress: string; - udpTunnelIpAddress: string; sshAddress: string; studentCodeStatus: number; isRunningCode: boolean; @@ -42,7 +39,6 @@ interface InfoState { const initialInfoState = { ipAddress: defaults.IPADDRESS, - udpTunnelIpAddress: defaults.IPADDRESS, sshAddress: defaults.IPADDRESS, studentCodeStatus: robotState.IDLE, isRunningCode: false, @@ -95,12 +91,6 @@ export const info = (state: InfoState = initialInfoState, action: Actions): Info ...state, ipAddress: action.ipAddress, }; - case consts.InfoActionsTypes.UDP_TUNNEL_IP_CHANGE: - ipcRenderer.send('udpTunnelIpAddress', action.ipAddress); - return { - ...state, - udpTunnelIpAddress: action.ipAddress - }; case consts.InfoActionsTypes.SSH_IP_CHANGE: return { ...state, diff --git a/renderer/types/actions/index.ts b/renderer/types/actions/index.ts index a336139c..76bdb500 100644 --- a/renderer/types/actions/index.ts +++ b/renderer/types/actions/index.ts @@ -38,7 +38,6 @@ export { RuntimeDisconnectAction, UpdateCodeStatusAction, IpChangeAction, - UDPTunnelIpChangeAction, SSHIpChangeAction, NotificationChangeAction, } from './info-actions'; diff --git a/renderer/types/actions/info-actions.ts b/renderer/types/actions/info-actions.ts index 5e2a00ff..971acfb7 100644 --- a/renderer/types/actions/info-actions.ts +++ b/renderer/types/actions/info-actions.ts @@ -26,11 +26,6 @@ export interface IpChangeAction { ipAddress: string; } -export interface UDPTunnelIpChangeAction { - type: consts.InfoActionsTypes.UDP_TUNNEL_IP_CHANGE; - ipAddress: string; -} - export interface SSHIpChangeAction { type: consts.InfoActionsTypes.SSH_IP_CHANGE; ipAddress: string; @@ -54,8 +49,6 @@ export interface InfoActions { ipChange: (ipAddress: string) => IpChangeAction; - udpTunnelIpChange: (ipAddress: string) => UDPTunnelIpChangeAction; - sshIpChange: (ipAddress: string) => SSHIpChangeAction; notificationChange: () => NotificationChangeAction;