From f8cb79f465462f340b369895992fff33ae24fbff Mon Sep 17 00:00:00 2001 From: Eddy Meals Date: Sun, 11 Aug 2024 23:55:22 -0700 Subject: [PATCH 01/14] Show device info in sidebar Replace DeviceInfo placeholder component with something that probably works (there still isn't a way to get information about devices from Runtime yet so the assumptions the code makes about how the data is structured might be completely wrong). Add a border to the Topbar. --- src/renderer/App.css | 3 ++- src/renderer/App.tsx | 9 +++++-- src/renderer/DeviceInfo.css | 24 +++++++++++++++++ src/renderer/DeviceInfo.tsx | 52 +++++++++++++++++++++++++++++++++++-- src/renderer/Topbar.css | 1 + 5 files changed, 84 insertions(+), 5 deletions(-) diff --git a/src/renderer/App.css b/src/renderer/App.css index 4c03133..5f658d0 100644 --- a/src/renderer/App.css +++ b/src/renderer/App.css @@ -8,7 +8,7 @@ body { --window-accent-color: grey; --body-color: white; --body-text-color: black; - --body-accent-color: white; + --body-accent-color: lightgray; display: flex; flex-direction: column; @@ -19,6 +19,7 @@ body { display: flex; flex-direction: row; height: 100%; + min-height: 0; } .App-modal-container { position: fixed; diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index 09c38d4..e285148 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -8,7 +8,7 @@ import { } from 'react'; import Topbar from './Topbar'; import Editor, { EditorContentStatus } from './Editor'; -import DeviceInfo from './DeviceInfo'; +import DeviceInfo, { Device as DeviceInfoState } from './DeviceInfo'; import AppConsole from './AppConsole'; import type AppConsoleMessage from '../common/AppConsoleMessage'; // No crypto package on the renderer import ConfirmModal from './modals/ConfirmModal'; @@ -90,6 +90,8 @@ export default function App() { const [SSHAddress, setSSHAddress] = useState('192.168.0.100'); const [FieldIPAddress, setFieldIPAddress] = useState('localhost'); const [FieldStationNum, setFieldStationNum] = useState('4'); + // Information about periperhals connected to the robot + const [deviceInfoState, setDeviceInfoState] = useState([] as DeviceInfoState[]); const changeActiveModal = (newModalName: string) => { if (document.activeElement instanceof HTMLElement) { @@ -244,6 +246,9 @@ export default function App() { } if (data.robotLatencyMs !== undefined) { setRobotLatencyMs(data.robotLatencyMs); + if (data.robotLatencyMs === -1) { + setDeviceInfoState([]); // Disconnect everything + } } }), window.electron.ipcRenderer.on('renderer-post-console', (data) => { @@ -344,7 +349,7 @@ export default function App() { onEndResize={endEditorResize} axis="x" /> - + {consoleIsOpen ? ( <> diff --git a/src/renderer/DeviceInfo.css b/src/renderer/DeviceInfo.css index f9e2ff9..2e41b13 100644 --- a/src/renderer/DeviceInfo.css +++ b/src/renderer/DeviceInfo.css @@ -1,4 +1,28 @@ .DeviceInfo { flex-grow: 1; overflow: auto scroll; + padding: 10px; +} +.DeviceInfo-device { + border: 1px solid var(--body-accent-color); + border-radius: 20px; + padding: 10px; + margin-bottom: 10px; +} +.DeviceInfo-device-id { + font: bold 20px Verdana, sans-serif; + user-select: all; + margin-bottom: 2px; +} +.DeviceInfo-device-type { + font-style: italic; +} +.DeviceInfo-device-props { + font-family: Verdana, sans-serif; + margin: 5px 0 0 10px; + width: 75%; + user-select: text; +} +.DeviceInfo-disconnected { + font: 15px Verdana, sans-serif; } diff --git a/src/renderer/DeviceInfo.tsx b/src/renderer/DeviceInfo.tsx index c29fa29..8780d27 100644 --- a/src/renderer/DeviceInfo.tsx +++ b/src/renderer/DeviceInfo.tsx @@ -1,8 +1,56 @@ import './DeviceInfo.css'; +const DEVICE_TYPES = { + 0: 'Dummy device', + 1: 'Limit switch', + 2: 'Line follower', + 3: 'Battery buzzer', + 4: 'Servo controller', + 5: 'Polar bear motor controller', + 6: 'KoalaBear motor controller', + 7: 'Power distribution board', + 8: 'Distance sensor', +}; + +export interface Device { + id: string; + [string]: string; +} + /** * Component displaying information about input devices and peripherals connected to the robot. */ -export default function DeviceInfo() { - return
Device info
; +export default function DeviceInfo({ + deviceStates +}: { + deviceStates: Device[]; +}) { + return ( +
+ { + deviceStates.length > 0 ? deviceStates.map((device) => { + const deviceTypeNum = Number(device.id.split('_')[0]); + const deviceType = deviceTypeNum in DEVICE_TYPES ? DEVICE_TYPES[deviceTypeNum] : 'Unknown device'; + return ( +
+
{device.id}
+
{deviceType}
+ + + { + Object.entries(device).map(([key, value]) => key !== 'id' && ( + + + + + )) + } + +
{key}{value}
+
+ ); + }) :
Dawn is not connected to the robot.
+ } +
+ ); } diff --git a/src/renderer/Topbar.css b/src/renderer/Topbar.css index 94ac6cc..6ea4ed3 100644 --- a/src/renderer/Topbar.css +++ b/src/renderer/Topbar.css @@ -2,6 +2,7 @@ width: calc(100% - 20px); padding: 10px; font-family: Arial, sans-serif; + border-bottom: 1px solid var(--body-accent-color); } .Topbar-left-group { float: left; From e23547bfa4d391cec7da8b9f7d3f89b8c1404b61 Mon Sep 17 00:00:00 2001 From: Eddy Meals Date: Mon, 12 Aug 2024 00:12:19 -0700 Subject: [PATCH 02/14] Remember to lint and test everything --- src/renderer/App.tsx | 8 ++++-- src/renderer/DeviceInfo.tsx | 56 ++++++++++++++++++++++++++----------- 2 files changed, 45 insertions(+), 19 deletions(-) diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index e285148..2ada479 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -91,7 +91,9 @@ export default function App() { const [FieldIPAddress, setFieldIPAddress] = useState('localhost'); const [FieldStationNum, setFieldStationNum] = useState('4'); // Information about periperhals connected to the robot - const [deviceInfoState, setDeviceInfoState] = useState([] as DeviceInfoState[]); + const [deviceInfoState, setDeviceInfoState] = useState( + [] as DeviceInfoState[], + ); const changeActiveModal = (newModalName: string) => { if (document.activeElement instanceof HTMLElement) { @@ -351,7 +353,7 @@ export default function App() { /> - {consoleIsOpen ? ( + {consoleIsOpen && ( <> - ) : undefined} + )}
- { - deviceStates.length > 0 ? deviceStates.map((device) => { + {deviceStates.length > 0 ? ( + deviceStates.map((device) => { const deviceTypeNum = Number(device.id.split('_')[0]); - const deviceType = deviceTypeNum in DEVICE_TYPES ? DEVICE_TYPES[deviceTypeNum] : 'Unknown device'; + const deviceType = + deviceTypeNum in DEVICE_TYPES + ? DEVICE_TYPES[deviceTypeNum] + : 'Unknown device'; return (
{device.id}
{deviceType}
- { - Object.entries(device).map(([key, value]) => key !== 'id' && ( - - - - - )) - } + {Object.entries(device).map( + ([key, value]) => + key !== 'id' && ( + + + + + ), + )}
{key}{value}
{key}{value}
); - }) :
Dawn is not connected to the robot.
- } + }) + ) : ( +
+ Dawn is not connected to the robot. +
+ )}
); } From 0340ae2c8ac4fc3a16a3c69a556612871b96495d Mon Sep 17 00:00:00 2001 From: Eddy Meals Date: Mon, 12 Aug 2024 18:41:09 -0700 Subject: [PATCH 03/14] Remove "resistance" when resizing DeviceInfo --- src/renderer/DeviceInfo.css | 1 + 1 file changed, 1 insertion(+) diff --git a/src/renderer/DeviceInfo.css b/src/renderer/DeviceInfo.css index 2e41b13..451c939 100644 --- a/src/renderer/DeviceInfo.css +++ b/src/renderer/DeviceInfo.css @@ -1,5 +1,6 @@ .DeviceInfo { flex-grow: 1; + flex-basis: 0; overflow: auto scroll; padding: 10px; } From 776e1f58fb3f9bb2da79e2058fbd21788577c5fc Mon Sep 17 00:00:00 2001 From: Eddy Meals Date: Tue, 10 Sep 2024 01:02:59 -0700 Subject: [PATCH 04/14] Communicate with Runtime Extract Device interface and DeviceTypes dictionary from DeviceInfo component to common/DeviceInfoState. Split renderer-robot-update into battery, latency, and devices updates; there is no equivalent runtime version update because it turns out runtime doesn't actually ever send its version, and the version indicator in the old Dawn's interface is vestigial. Change renderer listeners to reflect changed events. Move addRendererListener into MainApp class. Decouple MainApp from menu using MenuHandler interface. Add PacketStream to split chunks received from a TCP socket into Dawn packets. Add RuntimeComms class to handle all communications with Runtime. --- src/common/DeviceInfoState.ts | 28 +++ src/common/IpcEventTypes.ts | 38 ++- src/main/MainApp.ts | 145 +++++++++--- src/main/menu.ts | 56 +++-- src/main/network/PacketStream.ts | 64 +++++ src/main/network/RuntimeComms.ts | 389 +++++++++++++++++++++++++++++++ src/main/preload.ts | 28 ++- src/renderer/App.tsx | 21 +- src/renderer/DeviceInfo.tsx | 35 +-- 9 files changed, 685 insertions(+), 119 deletions(-) create mode 100644 src/common/DeviceInfoState.ts create mode 100644 src/main/network/PacketStream.ts create mode 100644 src/main/network/RuntimeComms.ts diff --git a/src/common/DeviceInfoState.ts b/src/common/DeviceInfoState.ts new file mode 100644 index 0000000..c68f128 --- /dev/null +++ b/src/common/DeviceInfoState.ts @@ -0,0 +1,28 @@ +/** + * Maps device types to user-friendly names. + */ +export const DeviceTypes: { [type: number]: string } = { + 0: 'Dummy device', + 1: 'Limit switch', + 2: 'Line follower', + 3: 'Battery buzzer', + 4: 'Servo controller', + 5: 'Polar bear motor controller', + 6: 'KoalaBear motor controller', + 7: 'Power distribution board', + 8: 'Distance sensor', +}; + +/** + * Represents one lowcar device and its reported data. + */ +export default interface DeviceInfoState { + /** + * The device id: the device type and uid separated by an underscore. + */ + id: string; + /** + * Human-presentable data reported by the device, by the keys used by Robot.get_value. + */ + [key: string]: string; +} diff --git a/src/common/IpcEventTypes.ts b/src/common/IpcEventTypes.ts index fa274bb..29a8a8a 100644 --- a/src/common/IpcEventTypes.ts +++ b/src/common/IpcEventTypes.ts @@ -1,11 +1,14 @@ import type AppConsoleMessage from './AppConsoleMessage'; +import type DeviceInfoState from './DeviceInfoState'; /** * IPC event channels used for communication from the main process to the renderer. */ export type RendererChannels = | 'renderer-init' - | 'renderer-robot-update' + | 'renderer-battery-update' + | 'renderer-latency-update' + | 'renderer-devices-update' | 'renderer-post-console' | 'renderer-file-control' | 'renderer-quit-request'; @@ -174,27 +177,20 @@ export type RendererFileControlData = */ export type RendererPostConsoleData = AppConsoleMessage; /** - * Data for the renderer-robot-update event when some info related to the robot or its connection - * changes. + * Data for the renderer-battery-update event sent by the main process to update the robot's battery + * voltage. */ -export interface RendererRobotUpdateData { - /** - * User-presentable runtime version string. May be omitted if the value has not changed since the - * last update. - */ - runtimeVersion?: string; - /** - * Robot battery voltage in volts. May be omitted if the value has not changed since the last - * update. - */ - robotBatteryVoltage?: number; - /** - * Robot connection latency in milliseconds. May be omitted if the value has not changed since the - * last update. - */ - robotLatencyMs?: number; -} - +export type RendererBatteryUpdateData = number; +/** + * Data for the renderer-latency-update event sent by the main process to update the displayed robot + * connection latency. + */ +export type RendererLatencyUpdateData = number; +/** + * Data for the renderer-devices-update event sent by the main process to update the state of + * connected lowcar devices. + */ +export type RendererDevicesUpdateData = DeviceInfoState[]; /** * Data for a specialization of the main-file-control event, sent by the renderer to * initiate/respond to a request to save the code. diff --git a/src/main/MainApp.ts b/src/main/MainApp.ts index 6c5381c..4586111 100644 --- a/src/main/MainApp.ts +++ b/src/main/MainApp.ts @@ -3,12 +3,15 @@ import type { BrowserWindow, FileFilter } from 'electron'; import fs from 'fs'; import { version as dawnVersion } from '../../package.json'; import AppConsoleMessage from '../common/AppConsoleMessage'; +import DeviceInfoState from '../common/DeviceInfoState'; import type { RendererChannels, RendererInitData, RendererFileControlData, RendererPostConsoleData, - RendererRobotUpdateData, + RendererBatteryUpdateData, + RendererLatencyUpdateData, + RendererDevicesUpdateData, MainChannels, MainFileControlData, MainQuitData, @@ -16,6 +19,8 @@ import type { import type Config from './Config'; import { coerceToConfig } from './Config'; import CodeTransfer from './network/CodeTransfer'; +import RuntimeComms, { RuntimeCommsListener } from './network/RuntimeComms'; +import type { MenuHandler } from './menu'; /** * Cooldown time in milliseconds to wait between sending didExternalChange messages to the renderer @@ -34,9 +39,9 @@ const CODE_FILE_FILTERS: FileFilter[] = [ */ const CONFIG_RELPATH = 'dawn-config.json'; /** - * Path on robot to upload student code to. $HOME is /home/${ROBOT_SSH_USER}. + * Path on robot to upload student code to. */ -const REMOTE_CODE_PATH = '/home/tmptestuser/runtime/executor/studentcode.py'; +const REMOTE_CODE_PATH = '/home/pi/runtime/executor/studentcode.py'; /** * Port to use when connecting to robot with SSH. */ @@ -50,30 +55,10 @@ const ROBOT_SSH_USER = 'pi'; */ const ROBOT_SSH_PASS = 'raspberry'; -function addRendererListener( - channel: 'main-quit', - func: (data: MainQuitData) => void, -): void; -function addRendererListener( - channel: 'main-file-control', - func: (data: MainFileControlData) => void, -): void; -/** - * Typed wrapper function to listen to IPC events from the renderer. - * @param channel - the event channel to listen to - * @param func - the listener to attach - */ -function addRendererListener( - channel: MainChannels, - func: (data: any) => void, -): void { - ipcMain.on(channel, (_event, data: any) => func(data)); -} - /** * Manages state owned by the main electron process. */ -export default class MainApp { +export default class MainApp implements MenuHandler, RuntimeCommsListener { /** * The BrowserWindow. */ @@ -113,6 +98,11 @@ export default class MainApp { */ readonly #codeTransfer: CodeTransfer; + /** + * Object used to communicate with Runtime. + */ + readonly #runtimeComms: RuntimeComms; + /** * @param mainWindow - the BrowserWindow. */ @@ -128,13 +118,15 @@ export default class MainApp { ROBOT_SSH_USER, ROBOT_SSH_PASS, ); + this.#runtimeComms = new RuntimeComms(this); + mainWindow.on('close', (e) => { if (this.#preventQuit) { e.preventDefault(); this.#sendToRenderer('renderer-quit-request'); } }); - addRendererListener('main-file-control', (data) => { + this.#addRendererListener('main-file-control', (data) => { if (data.type === 'save') { this.#saveCodeFile(data.content, data.forceDialog); } else if (data.type === 'load') { @@ -148,7 +140,8 @@ export default class MainApp { this.#watcher?.close(); } }); - addRendererListener('main-quit', (data) => { + this.#addRendererListener('main-quit', (data) => { + // Save config that may have been changed while the program was running this.#config.robotIPAddress = data.robotIPAddress; this.#config.robotSSHAddress = data.robotSSHAddress; this.#config.fieldIPAddress = data.fieldIPAddress; @@ -197,6 +190,40 @@ export default class MainApp { }); } + onReceiveRobotLogs(msgs: string[]) { + msgs.forEach((msg) => { + this.#sendToRenderer('renderer-post-console', new AppConsoleMessage('robot-info', msg)); + }); + } + + onReceiveLatency(latency: number) { + this.#sendToRenderer('renderer-latency-update', latency); + } + + onReceiveDevices(deviceInfoState: DeviceInfoState[]) { + this.#sendToRenderer('renderer-devices-update', deviceInfoState); + } + + onRuntimeTcpError(err: Error) { + this.#sendToRenderer('renderer-post-console', new AppConsoleMessage('dawn-err', + `Encountered TCP error when communicating with Runtime. ${err.toString()}`)) + } + + onRuntimeUdpError(err: Error) { + this.#sendToRenderer('renderer-post-console', new AppConsoleMessage('dawn-err', + `Encountered UDP error when communicating with Runtime. ${err.toString()}`)) + } + + onRuntimeError(err: Error) { + this.#sendToRenderer('renderer-post-console', new AppConsoleMessage('dawn-err', + `Encountered error when communicating with Runtime. ${err.toString()}`)) + } + + onRuntimeDisconnect() { + this.#sendToRenderer('renderer-post-console', new AppConsoleMessage('dawn-info', + 'Disconnected from robot.')); + } + /** * Requests that the renderer process start to save the code in the editor. * @param forceDialog - whether the save path selection dialog should be shown even if there is @@ -461,20 +488,50 @@ export default class MainApp { } /** - * Typed wrapper function for sending an event to the main window. + * Adds a listener for the main-quit IPC event fired by the renderer. + * @param channel - the event channel to listen to + * @param func - the listener to attach + */ + #addRendererListener( + channel: 'main-quit', + func: (data: MainQuitData) => void, + ): void; + /** + * Adds a listener for the main-file-control IPC event fired by the renderer. + * @param channel - the event channel to listen to + * @param func - the listener to attach + */ + #addRendererListener( + channel: 'main-file-control', + func: (data: MainFileControlData) => void, + ): void; + /** + * Typed wrapper function to listen to IPC events from the renderer. + * @param channel - the event channel to listen to + * @param func - the listener to attach + */ + #addRendererListener( + channel: MainChannels, + func: (data: any) => void, + ): void { + ipcMain.on(channel, (_event, data: any) => func(data)); + } + + /** + * Sends a renderer-quit-request IPC event to the renderer. * @param channel - the channel to send the event on */ #sendToRenderer(channel: 'renderer-quit-request'): void; /** - * Typed wrapper function for sending an event to the main window. + * Sends a renderer-init IPC event to the renderer. * @param channel - the channel to send the event on * @param data - a payload for the renderer-init event */ #sendToRenderer(channel: 'renderer-init', data: RendererInitData): void; /** - * Typed wrapper function for sending an event to the main window. + * Sends a renderer-file-control IPC event to the renderer. * @param channel - the channel to send the event on * @param data - a payload for the renderer-file-control event */ @@ -484,7 +541,7 @@ export default class MainApp { ): void; /** - * Typed wrapper function for sending an event to the main window. + * Sends a renderer-post-console IPC event to the renderer. * @param channel - the channel to send the event on * @param data - a payload for the renderer-post-console event */ @@ -494,13 +551,33 @@ export default class MainApp { ): void; /** - * Typed wrapper function for sending an event to the main window. + * Sends a renderer-battery-update IPC event to the renderer. + * @param channel - the channel to send the event on + * @param data - a payload for the renderer-battery-update event + */ + #sendToRenderer( + channel: 'renderer-battery-update', + data: RendererBatteryUpdateData, + ): void; + + /** + * Sends a renderer-latency-update IPC event to the renderer. + * @param channel - the channel to send the event on + * @param data - a payload for the renderer-latency-update event + */ + #sendToRenderer( + channel: 'renderer-latency-update', + data: RendererLatencyUpdateData, + ): void; + + /** + * Sends a renderer-devices-update IPC event to the renderer. * @param channel - the channel to send the event on - * @param data - a payload for the renderer-robot-update event + * @param data - a payload for the renderer-devices-update event */ #sendToRenderer( - channel: 'renderer-robot-update', - data: RendererRobotUpdateData, + channel: 'renderer-devices-update', + data: RendererDevicesUpdateData, ): void; /** diff --git a/src/main/menu.ts b/src/main/menu.ts index 938cb28..0b42d98 100644 --- a/src/main/menu.ts +++ b/src/main/menu.ts @@ -5,20 +5,44 @@ import { BrowserWindow, MenuItemConstructorOptions, } from 'electron'; -import type MainApp from './MainApp'; interface DarwinMenuItemConstructorOptions extends MenuItemConstructorOptions { selector?: string; submenu?: DarwinMenuItemConstructorOptions[] | Menu; } +export interface MenuHandler { + /** + * Requests that the file open in the editor be closed. + */ + promptCreateNewCodeFile: () => void; + /** + * Requests that a different file is opened in the editor. + */ + promptLoadCodeFile: () => void; + /** + * Requests that the contents of the editor are saved to a file. + * @param forceDialog - whether the user should be prompted for a save path even if the editor is + * already associated with an existing file. + */ + promptSaveCodeFile: (forceDialog: boolean) => void; + /** + * Requests that the open file be uploaded to the robot. + */ + promptUploadCodeFile: () => void; + /** + * Requests that student code on the robot be downloaded into the editor. + */ + promptDownloadCodeFile: () => void; +} + export default class MenuBuilder { - mainApp: MainApp; + menuHandler: MenuHandler; mainWindow: BrowserWindow; - constructor(mainApp: MainApp, mainWindow: BrowserWindow) { - this.mainApp = mainApp; + constructor(menuHandler: MenuHandler, mainWindow: BrowserWindow) { + this.menuHandler = menuHandler; this.mainWindow = mainWindow; } @@ -95,28 +119,28 @@ export default class MenuBuilder { label: 'New', accelerator: 'Command+N', click: () => { - this.mainApp.promptCreateNewCodeFile(); + this.menuHandler.promptCreateNewCodeFile(); }, }, { label: 'Open', accelerator: 'Command+O', click: () => { - this.mainApp.promptLoadCodeFile(); + this.menuHandler.promptLoadCodeFile(); }, }, { label: 'Save', accelerator: 'Command+S', click: () => { - this.mainApp.promptSaveCodeFile(false); + this.menuHandler.promptSaveCodeFile(false); }, }, { label: 'Save As', accelerator: 'Command+Shift+S', click: () => { - this.mainApp.promptSaveCodeFile(true); + this.menuHandler.promptSaveCodeFile(true); }, }, { type: 'separator' }, @@ -124,13 +148,13 @@ export default class MenuBuilder { label: 'Upload open file to robot', accelerator: 'Command+Shift+U', click: () => { - this.mainApp.promptUploadCodeFile(); + this.menuHandler.promptUploadCodeFile(); }, }, { label: 'Download code from robot', click: () => { - this.mainApp.promptDownloadCodeFile(); + this.menuHandler.promptDownloadCodeFile(); }, }, ], @@ -259,41 +283,41 @@ export default class MenuBuilder { label: '&New', accelerator: 'Ctrl+N', click: () => { - this.mainApp.promptCreateNewCodeFile(); + this.menuHandler.promptCreateNewCodeFile(); }, }, { label: '&Open', accelerator: 'Ctrl+O', click: () => { - this.mainApp.promptLoadCodeFile(); + this.menuHandler.promptLoadCodeFile(); }, }, { label: '&Save', accelerator: 'Ctrl+S', click: () => { - this.mainApp.promptSaveCodeFile(false); + this.menuHandler.promptSaveCodeFile(false); }, }, { label: 'Save &As', accelerator: 'Ctrl+Shift+S', click: () => { - this.mainApp.promptSaveCodeFile(true); + this.menuHandler.promptSaveCodeFile(true); }, }, { label: '&Upload open file to robot', accelerator: 'Ctrl+Alt+U', click: () => { - this.mainApp.promptUploadCodeFile(); + this.menuHandler.promptUploadCodeFile(); }, }, { label: 'Download code from robot', click: () => { - this.mainApp.promptDownloadCodeFile(); + this.menuHandler.promptDownloadCodeFile(); }, }, ], diff --git a/src/main/network/PacketStream.ts b/src/main/network/PacketStream.ts new file mode 100644 index 0000000..78285ea --- /dev/null +++ b/src/main/network/PacketStream.ts @@ -0,0 +1,64 @@ +import { Transform, TransformCallback, TransformOptions } from 'stream'; + +/** + * Length of a packet header in bytes: 1 byte type, 2 byte unsigned LE packet length. + */ +const HEADER_LENGTH = 3; + +/** + * Transform stream that splits a stream into Runtime packets: { type: number; data: Buffer }. + */ +export default class PacketStream extends Transform { + #buf: Buffer[]; + #bufLen: number; + + constructor(options?: TransformOptions) { + if (options && (options.writableObjectMode || options.objectMode)) { + throw new Error('PacketStream does not support writable object mode.'); + } + super({ + readableObjectMode: true, + ...(options || {}), + }); + this.#buf = []; + this.#bufLen = 0; + } + _transform(chunk: any, encoding: BufferEncoding, callback: TransformCallback) { + let chunkBuf: Buffer; + if (chunk instanceof Buffer) { + chunkBuf = chunk; + } else if (typeof chunk === 'string') { + chunkBuf = Buffer.from(chunk, encoding); + } else { + throw new Error('PacketStream does not support writable object mode.'); + } + this.#buf.push(chunkBuf); + let shouldConcatHeader = this.#bufLen < HEADER_LENGTH; + this.#bufLen += chunkBuf.byteLength; + while (this.#tryReadPacket(shouldConcatHeader)) { + shouldConcatHeader = false; + } + } + #tryReadPacket(shouldConcatHeader: boolean) { + if (this.#bufLen < HEADER_LENGTH) { + // Wait for complete header before reading packet + return false; + } + if (shouldConcatHeader) { + // Concat buffer in case the header is in multiple chunks, but only do this once per packet + this.#buf = [Buffer.concat(this.#buf)]; + } + const packetType = this.#buf[0].readUInt8(0); + const packetLength = this.#buf[0].readUInt16LE(1); + if (this.#bufLen < HEADER_LENGTH + packetLength) { + // Wait for complete packet data before reading packet + return false; + } + this.#buf[0] = this.#buf[0].subarray(HEADER_LENGTH); // Trim off header + this.#buf = [Buffer.concat(this.#buf)]; // Get packet data in one Buffer + this.push({ type: packetType, data: this.#buf[0].subarray(0, packetLength) }); + this.#buf[0] = this.#buf[0].subarray(packetLength); // Trim off packet data + this.#bufLen -= HEADER_LENGTH + packetLength; + return true; // Packet successfully read, more may follow + } +} diff --git a/src/main/network/RuntimeComms.ts b/src/main/network/RuntimeComms.ts new file mode 100644 index 0000000..df8f247 --- /dev/null +++ b/src/main/network/RuntimeComms.ts @@ -0,0 +1,389 @@ +import { Socket as TCPSocket, SocketAddress, createConnection as createTcpConnection } from 'net'; +import { Socket as UDPSocket, createSocket as createUdpSocket } from 'dgram'; +import PacketStream from './PacketStream'; +import DeviceInfoState from '../../common/DeviceInfoState'; +import * as protos from '../../../protos-main/protos'; + +const DEFAULT_RUNTIME_PORT = 8101; +const UDP_PORT = 9001; +const TCP_RECONNECT_DELAY = 2000; +const PING_INTERVAL = 5000; + +/** + * A type of packet. + */ +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 +} + +/** + * Describes a Runtime packet. + */ +interface Packet { + /** + * The type of packet. Might not be a valid MsgType. + */ + type: number; + /** + * The packet payload. + */ + data: Buffer; +} + +/** + * Handler interface for data received from the robot and transmission errors. + */ +export interface RuntimeCommsListener { + /** + * Called when the robot emits log messages. + * @param msgs - an array of new log messages. + */ + onReceiveRobotLogs: (msgs: string[]) => void; + /** + * Called when a latency check completes. + * @param latency - the measured robot connection latency. + */ + onReceiveLatency: (latency: number) => void; + /** + * Called when the robot sends lowcar device state. + * @param deviceInfoState - an array of the devices currently connected to the robot and their + * currently measured parameters. + */ + onReceiveDevices: (deviceInfoState: DeviceInfoState[]) => void; + /** + * Called when an error is encountered in the TCP connection to the robot. + * @param err - the error. + */ + onRuntimeTcpError: (err: Error) => void; + /** + * Called when the UDP socket encounters an error or data received by the UDP socket is malformed. + * @param err - the error. Protobuf ProtocolErrors are likely the result of a UDP transmission + * error. + */ + onRuntimeUdpError: (err: Error) => void; + /** + * Called when a generic Runtime communications error is encountered. + * @param err - the error. + */ + onRuntimeError: (err: Error) => void; + /** + * Called when the TCP connection to the robot is lost for any reason. + */ + onRuntimeDisconnect: () => void; +} + +/** + * Responsible for all communications with Runtime. + */ +export default class RuntimeComms { + readonly #commsListener: RuntimeCommsListener; + #runtimeAddr: string; + #runtimePort: number; + #tcpSock: TCPSocket | null; + #udpSock: UDPSocket | null; + #tcpDisconnected: boolean; + #pingInterval: NodeJS.Timeout | null; + + constructor(commsListener: RuntimeCommsListener) { + this.#commsListener = commsListener; + this.#runtimeAddr = ''; + this.#runtimePort = 0; + this.#tcpSock = null; + this.#udpSock = null; + this.#tcpDisconnected = false; + this.#pingInterval = null; + } + + /** + * Stops listening to the TCP and UDP sockets until resumed by setRobotIp. + */ + disconnect() { + this.#tcpDisconnected = true; // Don't reconnect + if (this.#pingInterval) { + clearInterval(this.#pingInterval); + } + this.#disconnectTcp(); + this.#disconnectUdp(); + } + /** + * Sets the host and address of the Runtime instance to connect to. + * @param ip - the address and optionally the port Runtime is listening on, separated by a colon. + * @returns Whether the given IP forms a valid address. + */ + setRobotIp(ip: string) { + this.#runtimeAddr = ip.split(':')[0]; + const portStr: string | undefined = ip.split(':')[1]; + this.#runtimePort = portStr ? Number(portStr) : DEFAULT_RUNTIME_PORT; + try { + new SocketAddress({ + address: this.#runtimeAddr, + port: this.#runtimePort, + }); + } catch { + return false; + } + this.#connectTcp(); // Reconnect TCP, UDP will just start sending to new host + return true; + } + /** + * Sends a new run mode. + * @param runMode - the new run mode. + */ + sendRunMode(runMode: protos.IRunMode) { + if (this.#tcpSock) { + this.#tcpSock.write(this.#createPacket(MsgType.RUN_MODE, runMode)); + } + } + /** + * Sends device preferences. + * @param deviceInfoState - device preferences to send. + */ + sendDevicePreferences(deviceData: protos.IDevData) { + if (this.#tcpSock) { + this.#tcpSock.write(this.#createPacket(MsgType.DEVICE_DATA, deviceData)); + } + } + /** + * Sends challenge data. + * @param data - the textual challenge data to send + */ + sendChallengeInputs(data: protos.IText) { + if (this.#tcpSock) { + this.#tcpSock.write(this.#createPacket(MsgType.CHALLENGE_DATA, data)); + } + } + /** + * Sends the robot's starting position. + */ + sendRobotStartPos(startPos: protos.IStartPos) { + if (this.#tcpSock) { + this.#tcpSock.write(this.#createPacket(MsgType.START_POS, startPos)); + } + } + sendInputs(inputs: protos.Input[], source: protos.Source) { + //if (this.#udpSock) { + // this.udpSock.send(protos.UserInputs.encode({ + // inputs: inputs.length ? inputs : [ + // protos.Input.create({ connected: false, source }) + // ], + // }), this.#runtimePort, this.#runtimeAddr); + //} + // Old Dawn sends inputs through TCP, though comments say this is just for 2021? + if (this.#tcpSock) { + this.#tcpSock.write(this.#createPacket(MsgType.INPUTS, { + inputs: inputs.length ? inputs : [ + protos.Input.create({ connected: false, source }) + ], + })); + } + } + + /** + * Sends a timestamped packet that Runtime will echo, updating the latency when we receive it + * again. + */ + #sendLatencyTest() { + if (this.#tcpSock) { + this.#tcpSock.write(this.#createPacket(MsgType.TIME_STAMPS, + new protos.TimeStamps({ dawnTimestamp: Date.now(), runtimeTimestamp: 0 }))); + } + } + /** + * Disconnects the old TCP socket if open, then makes a new one and connects it to the + * most recently known Runtime IP and port. + */ + #connectTcp() { + this.#tcpDisconnected = false; + this.#disconnectTcp(); + const tcpStream = new PacketStream() + .on('data', this.#handlePacket.bind(this)); + this.#tcpSock = createTcpConnection(this.#runtimePort, this.#runtimeAddr) + .on('connect', this.#handleTcpConnection.bind(this)) + .pipe(tcpStream) + .on('close', this.#handleTcpClose.bind(this)) + .on('error', this.#commsListener.onRuntimeTcpError.bind(this.#commsListener)); + this.#pingInterval = setInterval(this.#sendLatencyTest.bind(this), PING_INTERVAL); + } + /** + * Closes the old UDP socket if open, then makes and binds a new one. + */ + #connectUdp() { + this.#disconnectUdp(); + this.#udpSock = createUdpSocket({ + type: 'udp4', + reuseAddr: true, + }).on('error', this.#commsListener.onRuntimeUdpError.bind(this.#commsListener)) + .on('message', this.#handleUdpMessage.bind(this)) + .bind(UDP_PORT); + } + /** + * Ends and disconnects the TCP socket if open. + */ + #disconnectTcp() { + if (this.#tcpSock) { + this.#tcpSock.resetAndDestroy(); + this.#tcpSock = null; + } + } + /** + * Closes the UDP socket if open. + */ + #disconnectUdp() { + if (this.#udpSock) { + this.#udpSock.close(); + this.#udpSock = null; + } + } + /** + * Handler for TCP 'connect' event. + */ + #handleTcpConnection() { + if (this.#tcpSock) { + this.#tcpSock.write(new Uint8Array([1])); // Tell Runtime that we are Dawn, not Shepherd + } + } + /** + * Processes a received packet. + * @param packet - the received packet. + */ + #handlePacket({ type, data }: Packet) { + switch (type) { + case MsgType.LOG: + this.#commsListener.onReceiveRobotLogs(protos.Text.decode(data).payload); + break; + case MsgType.TIME_STAMPS: + this.#commsListener.onReceiveLatency( + (Date.now() - Number(protos.TimeStamps.decode(data))) / 2); + break; + case MsgType.CHALLENGE_DATA: + // TODO: ??? Not implemented in old Dawn + break; + case MsgType.DEVICE_DATA: + // Convert decoded Devices to DeviceInfoStates before passing to onReceiveDevices + this.#commsListener.onReceiveDevices(protos.DevData.decode(data).devices.map( + (device: protos.Device, _index: number, _array: protos.Device[]) => ({ + id: `${device.type.toString()}_${device.uid.toString()}`, + ...Object.fromEntries(device.params.map( + (param: protos.Param, _index: number, _array: protos.Param[]) => { + return param.val ? [param.name, param.val.toString()] : []; + }) + ), + } + ))); + break; + default: + this.#commsListener.onRuntimeError(new Error(`Received unexpected packet MsgType ${type}.`)); + } + } + /** + * Processes a Buffer assumed to be the payload of a device data packet with some error checking + * code because UDP packets might not be well-formed. + * @param packet - the received packet. + */ + #handleUdpMessage(data: Buffer) { + try { + this.#handlePacket({ type: MsgType.DEVICE_DATA, data }); + } catch (err) { + this.#commsListener.onRuntimeUdpError(err as Error); + } + } + /** + * Handles TCP 'close' event and tries to reconnect if we didn't cause the disconnection. + */ + #handleTcpClose() { + this.#commsListener.onRuntimeDisconnect(); + if (!this.#tcpDisconnected) { + setTimeout(this.#connectTcp, TCP_RECONNECT_DELAY); + } + } + /** + * Encodes a device state packet. + * @param type - the packet type. + * @param data - the packet payload. + * @returns The packet encoded in a Buffer + */ + #createPacket(type: MsgType.DEVICE_DATA, data: protos.IDevData): Buffer; + /** + * Encodes a run mode packet. + * @param type - the packet type. + * @param data - the packet payload. + * @returns The packet encoded in a Buffer + */ + #createPacket(type: MsgType.RUN_MODE, data: protos.IRunMode): Buffer; + /** + * Encodes a robot start position packet. + * @param type - the packet type. + * @param data - the packet payload. + * @returns The packet encoded in a Buffer + */ + #createPacket(type: MsgType.START_POS, data: protos.IStartPos): Buffer; + /** + * Encodes a ping packet. + * @param type - the packet type. + * @param data - the packet payload. + * @returns The packet encoded in a Buffer + */ + #createPacket(type: MsgType.TIME_STAMPS, data: protos.ITimeStamps): Buffer; + /** + * Encodes a challenge data packet. + * @param type - the packet type. + * @param data - the packet payload. + * @returns The packet encoded in a Buffer + */ + #createPacket(type: MsgType.CHALLENGE_DATA, data: protos.IText): Buffer; + /** + * Encodes an input state packet. + * @param type - the packet type. + * @param data - the packet payload. + * @returns The packet encoded in a Buffer + */ + #createPacket(type: MsgType.INPUTS, data: protos.IUserInputs): Buffer; + /** + * Encodes a packet. + * @param type - the packet type. + * @param data - the packet payload. + * @returns The packet encoded in a Buffer + */ + #createPacket(type: MsgType, data: unknown): Buffer { + let packetData: Uint8Array; + switch (type) { + case MsgType.DEVICE_DATA: + packetData = protos.DevData.encode(data as protos.IDevData).finish(); + break; + case MsgType.RUN_MODE: + packetData = protos.RunMode.encode(data as protos.IRunMode).finish(); + break; + case MsgType.START_POS: + packetData = protos.StartPos.encode(data as protos.IStartPos).finish(); + break; + case MsgType.TIME_STAMPS: + packetData = protos.TimeStamps.encode(data as protos.ITimeStamps).finish(); + break; + case MsgType.CHALLENGE_DATA: + packetData = protos.Text.encode(data as protos.IText).finish(); + break; + case MsgType.INPUTS: + // Source says input data isn't usually sent through TCP? What's that about? + packetData = protos.UserInputs.encode(data as protos.IUserInputs).finish(); + break; + default: + this.#commsListener.onRuntimeError(new Error(`Cannot create packet with type ${type}.`)); + packetData = Buffer.alloc(0); + break; + } + + const packet = Buffer.allocUnsafe(3 + packetData.byteLength); + packet.writeUInt8(type, 0); + packet.writeUInt16LE(packetData.byteLength, 1); + packetData.copy(packet, 3); + + return packet; + } +} diff --git a/src/main/preload.ts b/src/main/preload.ts index 67f65f2..0d5681a 100644 --- a/src/main/preload.ts +++ b/src/main/preload.ts @@ -6,7 +6,9 @@ import type { MainChannels, MainFileControlData, RendererInitData, - RendererRobotUpdateData, + RendererBatteryUpdateData, + RendererLatencyUpdateData, + RendererDevicesUpdateData, RendererPostConsoleData, RendererFileControlData, MainQuitData, @@ -26,8 +28,16 @@ function on( func: (arg: RendererInitData) => void, ): () => void; function on( - channel: 'renderer-robot-update', - func: (arg: RendererRobotUpdateData) => void, + channel: 'renderer-battery-update', + func: (arg: RendererBatteryUpdateData) => void, +): () => void; +function on( + channel: 'renderer-latency-update', + func: (arg: RendererLatencyUpdateData) => void, +): () => void; +function on( + channel: 'renderer-devices-update', + func: (arg: RendererDevicesUpdateData) => void, ): () => void; function on( channel: 'renderer-post-console', @@ -53,8 +63,16 @@ function once( func: (arg: RendererInitData) => void, ): void; function once( - channel: 'renderer-robot-update', - func: (arg: RendererRobotUpdateData) => void, + channel: 'renderer-battery-update', + func: (arg: RendererBatteryUpdateData) => void, +): void; +function once( + channel: 'renderer-latency-update', + func: (arg: RendererLatencyUpdateData) => void, +): void; +function once( + channel: 'renderer-devices-update', + func: (arg: RendererDevicesUpdateData) => void, ): void; function once( channel: 'renderer-post-console', diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index 6c37040..d299b4c 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -8,9 +8,10 @@ import { } from 'react'; import Topbar from './Topbar'; import Editor, { EditorContentStatus } from './Editor'; -import DeviceInfo, { Device as DeviceInfoState } from './DeviceInfo'; +import DeviceInfo from './DeviceInfo'; import AppConsole from './AppConsole'; import type AppConsoleMessage from '../common/AppConsoleMessage'; // No crypto package on the renderer +import type DeviceInfoState from '../common/DeviceInfoState'; import ConfirmModal from './modals/ConfirmModal'; import ConnectionConfigModal, { ConnectionConfigChangeEvent, @@ -244,20 +245,14 @@ export default function App() { setFieldStationNum(data.fieldStationNumber); setShowDirtyUploadWarning(data.showDirtyUploadWarning); }), - window.electron.ipcRenderer.on('renderer-robot-update', (data) => { - if (data.runtimeVersion !== undefined) { - setRuntimeVersion(data.runtimeVersion); - } - if (data.robotBatteryVoltage !== undefined) { - setRobotBatteryVoltage(data.robotBatteryVoltage); - } - if (data.robotLatencyMs !== undefined) { - setRobotLatencyMs(data.robotLatencyMs); - if (data.robotLatencyMs === -1) { - setDeviceInfoState([]); // Disconnect everything - } + window.electron.ipcRenderer.on('renderer-battery-update', setRobotBatteryVoltage), + window.electron.ipcRenderer.on('renderer-latency-update', (latency) => { + setRobotLatencyMs(latency); + if (latency === -1) { + setDeviceInfoState([]); // Disconnect everything } }), + window.electron.ipcRenderer.on('renderer-devices-update', setDeviceInfoState), ]; return () => listenerDestructors.forEach((destructor) => destructor()); } diff --git a/src/renderer/DeviceInfo.tsx b/src/renderer/DeviceInfo.tsx index 11d13cd..0e59842 100644 --- a/src/renderer/DeviceInfo.tsx +++ b/src/renderer/DeviceInfo.tsx @@ -1,42 +1,17 @@ +import DeviceInfoState, { DeviceTypes } from '../common/DeviceInfoState'; import './DeviceInfo.css'; -const DEVICE_TYPES: { [type: number]: string } = { - 0: 'Dummy device', - 1: 'Limit switch', - 2: 'Line follower', - 3: 'Battery buzzer', - 4: 'Servo controller', - 5: 'Polar bear motor controller', - 6: 'KoalaBear motor controller', - 7: 'Power distribution board', - 8: 'Distance sensor', -}; - -/** - * Represents one lowcar device and its reported data. - */ -export interface Device { - /** - * The device id: the device type and uid separated by an underscore. - */ - id: string; - /** - * Human-presentable data reported by the device, by the keys used by Robot.get_value. - */ - [key: string]: string; -} - /** * Component displaying information about input devices and peripherals connected to the robot. * @param props - props - * @param props.deviceStates - an array of Device objects describing the state of all devices + * @param props.deviceStates - an array of DeviceInfoState objects describing the state of all devices * connected to the robot. If empty, the robot is assumed to be disconnected (because the PDB should * always be there). */ export default function DeviceInfo({ deviceStates, }: { - deviceStates: Device[]; + deviceStates: DeviceInfoState[]; }) { return (
@@ -44,8 +19,8 @@ export default function DeviceInfo({ deviceStates.map((device) => { const deviceTypeNum = Number(device.id.split('_')[0]); const deviceType = - deviceTypeNum in DEVICE_TYPES - ? DEVICE_TYPES[deviceTypeNum] + deviceTypeNum in DeviceTypes + ? DeviceTypes[deviceTypeNum] : 'Unknown device'; return (
From 65c457a0171a7c0896ef6b364bad1dc046afb945 Mon Sep 17 00:00:00 2001 From: Eddy Meals Date: Wed, 11 Sep 2024 00:02:27 -0700 Subject: [PATCH 05/14] Fix tsc errors --- src/main/menu.ts | 2 +- src/main/network/RuntimeComms.ts | 24 ++++++++++++++---------- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/src/main/menu.ts b/src/main/menu.ts index 0b42d98..5a1fdd7 100644 --- a/src/main/menu.ts +++ b/src/main/menu.ts @@ -274,7 +274,7 @@ export default class MenuBuilder { ]; } - buildDefaultTemplate() { + buildDefaultTemplate(): MenuItemConstructorOptions[] { const templateDefault = [ { label: '&File', diff --git a/src/main/network/RuntimeComms.ts b/src/main/network/RuntimeComms.ts index df8f247..f1c5005 100644 --- a/src/main/network/RuntimeComms.ts +++ b/src/main/network/RuntimeComms.ts @@ -206,9 +206,9 @@ export default class RuntimeComms { .on('data', this.#handlePacket.bind(this)); this.#tcpSock = createTcpConnection(this.#runtimePort, this.#runtimeAddr) .on('connect', this.#handleTcpConnection.bind(this)) - .pipe(tcpStream) .on('close', this.#handleTcpClose.bind(this)) .on('error', this.#commsListener.onRuntimeTcpError.bind(this.#commsListener)); + this.#tcpSock.pipe(tcpStream); this.#pingInterval = setInterval(this.#sendLatencyTest.bind(this), PING_INTERVAL); } /** @@ -268,15 +268,19 @@ export default class RuntimeComms { case MsgType.DEVICE_DATA: // Convert decoded Devices to DeviceInfoStates before passing to onReceiveDevices this.#commsListener.onReceiveDevices(protos.DevData.decode(data).devices.map( - (device: protos.Device, _index: number, _array: protos.Device[]) => ({ - id: `${device.type.toString()}_${device.uid.toString()}`, - ...Object.fromEntries(device.params.map( - (param: protos.Param, _index: number, _array: protos.Param[]) => { - return param.val ? [param.name, param.val.toString()] : []; - }) - ), + (deviceProps: protos.IDevice, _index: number, _array: protos.IDevice[]) => { + const device = new protos.Device(deviceProps); + return { + id: `${device.type.toString()}_${device.uid.toString()}`, + ...Object.fromEntries(device.params.map( + (paramProps: protos.IParam, _index: number, _array: protos.IParam[]) => { + const param = new protos.Param(paramProps); + return param.val ? [param.name, param.val.toString()] : []; + }) + ), + }; } - ))); + )); break; default: this.#commsListener.onRuntimeError(new Error(`Received unexpected packet MsgType ${type}.`)); @@ -382,7 +386,7 @@ export default class RuntimeComms { const packet = Buffer.allocUnsafe(3 + packetData.byteLength); packet.writeUInt8(type, 0); packet.writeUInt16LE(packetData.byteLength, 1); - packetData.copy(packet, 3); + Buffer.from(packetData.buffer, packetData.byteOffset, packetData.byteLength) .copy(packet, 3); return packet; } From 793a2e95900f870220dde02f7404feafe8d0fb18 Mon Sep 17 00:00:00 2001 From: Eddy Meals Date: Wed, 11 Sep 2024 00:37:18 -0700 Subject: [PATCH 06/14] Remove all but one unfixable TypeDoc warning --- src/main/menu.ts | 28 +++++++++++++++++++++ src/main/network/PacketStream.ts | 29 +++++++++++++++++++++- src/main/network/RuntimeComms.ts | 42 +++++++++++++++++++++++++++++--- src/main/preload.ts | 18 ++++++++++++++ src/main/util.ts | 6 +++++ src/renderer/preload.d.ts | 7 ++++++ 6 files changed, 125 insertions(+), 5 deletions(-) diff --git a/src/main/menu.ts b/src/main/menu.ts index 5a1fdd7..a683c79 100644 --- a/src/main/menu.ts +++ b/src/main/menu.ts @@ -11,6 +11,9 @@ interface DarwinMenuItemConstructorOptions extends MenuItemConstructorOptions { submenu?: DarwinMenuItemConstructorOptions[] | Menu; } +/** + * Interface for objects that can handle user input from the menu. + */ export interface MenuHandler { /** * Requests that the file open in the editor be closed. @@ -36,16 +39,32 @@ export interface MenuHandler { promptDownloadCodeFile: () => void; } +/** + * Creates an Electron Menu for the appropriate platform and build type. + */ export default class MenuBuilder { + /** + * The handler for menu options related to app logic. + */ menuHandler: MenuHandler; + /** + * The target window for view-controlling menu options. + */ mainWindow: BrowserWindow; + /** + * @param menuHandler - menu option handler for options related to app logic. + * @param mainWindow - target window for view-controlling menu options. + */ constructor(menuHandler: MenuHandler, mainWindow: BrowserWindow) { this.menuHandler = menuHandler; this.mainWindow = mainWindow; } + /** + * Creates an appropriate Electron menu for the current platform and build type. + */ buildMenu(): Menu { if ( process.env.NODE_ENV === 'development' || @@ -65,6 +84,9 @@ export default class MenuBuilder { return menu; } + /** + * Called when menu is built and debug tools are enabled. + */ setupDevelopmentEnvironment(): void { this.mainWindow.webContents.on('context-menu', (_, props) => { const { x, y } = props; @@ -80,6 +102,9 @@ export default class MenuBuilder { }); } + /** + * Returns a menu template for the OSX Darwin environment. + */ buildDarwinTemplate(): MenuItemConstructorOptions[] { const subMenuAbout: DarwinMenuItemConstructorOptions = { label: 'Electron', @@ -274,6 +299,9 @@ export default class MenuBuilder { ]; } + /** + * Returns a menu template used for environments besides Darwin. + */ buildDefaultTemplate(): MenuItemConstructorOptions[] { const templateDefault = [ { diff --git a/src/main/network/PacketStream.ts b/src/main/network/PacketStream.ts index 78285ea..0e97fc4 100644 --- a/src/main/network/PacketStream.ts +++ b/src/main/network/PacketStream.ts @@ -9,7 +9,14 @@ const HEADER_LENGTH = 3; * Transform stream that splits a stream into Runtime packets: { type: number; data: Buffer }. */ export default class PacketStream extends Transform { + /** + * An array of buffers that together contain all the data yet to be output as packets. + */ #buf: Buffer[]; + + /** + * The current number of unoutput bytes. + */ #bufLen: number; constructor(options?: TransformOptions) { @@ -23,6 +30,14 @@ export default class PacketStream extends Transform { this.#buf = []; this.#bufLen = 0; } + + /** + * Handles input data and outputs packets if possible. + * @param chunk - the input data. + * @param encoding - the encoding of the input data. + * @param callback - a callback function to be called with the consumed chunk or an error + * following processing. + */ _transform(chunk: any, encoding: BufferEncoding, callback: TransformCallback) { let chunkBuf: Buffer; if (chunk instanceof Buffer) { @@ -30,7 +45,8 @@ export default class PacketStream extends Transform { } else if (typeof chunk === 'string') { chunkBuf = Buffer.from(chunk, encoding); } else { - throw new Error('PacketStream does not support writable object mode.'); + callback(new Error('PacketStream does not support writable object mode.'), null); + return; } this.#buf.push(chunkBuf); let shouldConcatHeader = this.#bufLen < HEADER_LENGTH; @@ -38,7 +54,18 @@ export default class PacketStream extends Transform { while (this.#tryReadPacket(shouldConcatHeader)) { shouldConcatHeader = false; } + callback(null, chunk); } + + /** + * Tries to output a packet from the data currently in the buffer. Regardless of whether data from + * the buffer is consumed, the buffer will always either be left empty or start with a packet + * header. + * @param shouldConcatHeader - whether the 3 byte packet header at the beginning of the buffer may + * be split into multiple Buffers. + * @returns Whether a full packet was read and output and the next packet is ready to be attempted + * to be read. + */ #tryReadPacket(shouldConcatHeader: boolean) { if (this.#bufLen < HEADER_LENGTH) { // Wait for complete header before reading packet diff --git a/src/main/network/RuntimeComms.ts b/src/main/network/RuntimeComms.ts index f1c5005..c9a1e35 100644 --- a/src/main/network/RuntimeComms.ts +++ b/src/main/network/RuntimeComms.ts @@ -83,12 +83,39 @@ export interface RuntimeCommsListener { * Responsible for all communications with Runtime. */ export default class RuntimeComms { + /** + * Handler for robot data and transmission errors. + */ readonly #commsListener: RuntimeCommsListener; + + /** + * IP address of robot running Runtime to connect to. + */ #runtimeAddr: string; + + /** + * Port Runtime is listening on on the connected robot. + */ #runtimePort: number; + + /** + * The TCP connection used to send and receive transmission-guarenteed data from Runtime. + */ #tcpSock: TCPSocket | null; + + /** + * The UDP socket listening for realtime data from Runtime. + */ #udpSock: UDPSocket | null; + + /** + * Whether communications are paused and reconnection should not be attempted automatically. + */ #tcpDisconnected: boolean; + + /** + * The JavaScript interval id for the periodic ping. + */ #pingInterval: NodeJS.Timeout | null; constructor(commsListener: RuntimeCommsListener) { @@ -143,7 +170,7 @@ export default class RuntimeComms { } /** * Sends device preferences. - * @param deviceInfoState - device preferences to send. + * @param deviceData - device preferences to send. */ sendDevicePreferences(deviceData: protos.IDevData) { if (this.#tcpSock) { @@ -152,7 +179,7 @@ export default class RuntimeComms { } /** * Sends challenge data. - * @param data - the textual challenge data to send + * @param data - the textual challenge data to send. */ sendChallengeInputs(data: protos.IText) { if (this.#tcpSock) { @@ -161,12 +188,18 @@ export default class RuntimeComms { } /** * Sends the robot's starting position. + * @param startPos - the robot's starting position index to send. */ sendRobotStartPos(startPos: protos.IStartPos) { if (this.#tcpSock) { this.#tcpSock.write(this.#createPacket(MsgType.START_POS, startPos)); } } + /** + * Sends control inputs to the robot. + * @param inputs - the inputs to send. + * @param source - the device that is the source of the inputs. + */ sendInputs(inputs: protos.Input[], source: protos.Source) { //if (this.#udpSock) { // this.udpSock.send(protos.UserInputs.encode({ @@ -253,7 +286,8 @@ export default class RuntimeComms { * Processes a received packet. * @param packet - the received packet. */ - #handlePacket({ type, data }: Packet) { + #handlePacket(packet: Packet) { + const {type, data} = packet; switch (type) { case MsgType.LOG: this.#commsListener.onReceiveRobotLogs(protos.Text.decode(data).payload); @@ -289,7 +323,7 @@ export default class RuntimeComms { /** * Processes a Buffer assumed to be the payload of a device data packet with some error checking * code because UDP packets might not be well-formed. - * @param packet - the received packet. + * @param data - the payload of the received packet. */ #handleUdpMessage(data: Buffer) { try { diff --git a/src/main/preload.ts b/src/main/preload.ts index 0d5681a..9eb4ee5 100644 --- a/src/main/preload.ts +++ b/src/main/preload.ts @@ -87,14 +87,32 @@ function once(channel: RendererChannels, func: (arg?: any) => void): void { ipcRenderer.once(channel, (_event, ...args) => func(...args)); } +/** + * Object whose contents are injected into renderer global contexts. + */ const electronHandler = { + /** + * Object containing main process functions exposed to the renderer processes. + */ ipcRenderer: { + /** + * Sends an IPC event to the main process. + */ sendMessage, + /** + * Listens to IPC events from the main process. + */ on, + /** + * Listens for the first firing of an IPC event from the main process. + */ once, }, }; contextBridge.exposeInMainWorld('electron', electronHandler); +/** + * The interface through which Electron renderer processes may communicate with the main process. + */ export type ElectronHandler = typeof electronHandler; diff --git a/src/main/util.ts b/src/main/util.ts index 7775eda..7a57162 100644 --- a/src/main/util.ts +++ b/src/main/util.ts @@ -2,6 +2,12 @@ import { URL } from 'url'; import path from 'path'; +/** + * Converts an absolute HTML path to a loopback or file:// path, respecting the build type and + * configuration. + * @param htmlFileName - the HTML path to convert. + * @returns The HTML path converted to a path suitable for the build type. + */ export function resolveHtmlPath(htmlFileName: string) { if (process.env.NODE_ENV === 'development') { const port = process.env.PORT || 1212; diff --git a/src/renderer/preload.d.ts b/src/renderer/preload.d.ts index 53cc2d7..99d3be0 100644 --- a/src/renderer/preload.d.ts +++ b/src/renderer/preload.d.ts @@ -1,8 +1,15 @@ import { ElectronHandler } from '../main/preload'; declare global { + /** + * Electron injections to the renderer global context. + */ // eslint-disable-next-line no-unused-vars interface Window { + /** + * The interface through which Electron renderer processes may communicate with the main + * process. + */ electron: ElectronHandler; } } From 43fad6f424aa8574550c58a6f6eefd47ff17340e Mon Sep 17 00:00:00 2001 From: Eddy Meals Date: Wed, 11 Sep 2024 00:45:53 -0700 Subject: [PATCH 07/14] Lint for formatting --- src/main/MainApp.ts | 45 ++++++--- src/main/network/PacketStream.ts | 16 +++- src/main/network/RuntimeComms.ts | 151 +++++++++++++++++++++++-------- src/renderer/App.tsx | 10 +- 4 files changed, 166 insertions(+), 56 deletions(-) diff --git a/src/main/MainApp.ts b/src/main/MainApp.ts index 4586111..3fa7e76 100644 --- a/src/main/MainApp.ts +++ b/src/main/MainApp.ts @@ -192,7 +192,10 @@ export default class MainApp implements MenuHandler, RuntimeCommsListener { onReceiveRobotLogs(msgs: string[]) { msgs.forEach((msg) => { - this.#sendToRenderer('renderer-post-console', new AppConsoleMessage('robot-info', msg)); + this.#sendToRenderer( + 'renderer-post-console', + new AppConsoleMessage('robot-info', msg), + ); }); } @@ -205,23 +208,40 @@ export default class MainApp implements MenuHandler, RuntimeCommsListener { } onRuntimeTcpError(err: Error) { - this.#sendToRenderer('renderer-post-console', new AppConsoleMessage('dawn-err', - `Encountered TCP error when communicating with Runtime. ${err.toString()}`)) + this.#sendToRenderer( + 'renderer-post-console', + new AppConsoleMessage( + 'dawn-err', + `Encountered TCP error when communicating with Runtime. ${err.toString()}`, + ), + ); } onRuntimeUdpError(err: Error) { - this.#sendToRenderer('renderer-post-console', new AppConsoleMessage('dawn-err', - `Encountered UDP error when communicating with Runtime. ${err.toString()}`)) + this.#sendToRenderer( + 'renderer-post-console', + new AppConsoleMessage( + 'dawn-err', + `Encountered UDP error when communicating with Runtime. ${err.toString()}`, + ), + ); } onRuntimeError(err: Error) { - this.#sendToRenderer('renderer-post-console', new AppConsoleMessage('dawn-err', - `Encountered error when communicating with Runtime. ${err.toString()}`)) + this.#sendToRenderer( + 'renderer-post-console', + new AppConsoleMessage( + 'dawn-err', + `Encountered error when communicating with Runtime. ${err.toString()}`, + ), + ); } onRuntimeDisconnect() { - this.#sendToRenderer('renderer-post-console', new AppConsoleMessage('dawn-info', - 'Disconnected from robot.')); + this.#sendToRenderer( + 'renderer-post-console', + new AppConsoleMessage('dawn-info', 'Disconnected from robot.'), + ); } /** @@ -496,6 +516,7 @@ export default class MainApp implements MenuHandler, RuntimeCommsListener { channel: 'main-quit', func: (data: MainQuitData) => void, ): void; + /** * Adds a listener for the main-file-control IPC event fired by the renderer. * @param channel - the event channel to listen to @@ -505,15 +526,13 @@ export default class MainApp implements MenuHandler, RuntimeCommsListener { channel: 'main-file-control', func: (data: MainFileControlData) => void, ): void; + /** * Typed wrapper function to listen to IPC events from the renderer. * @param channel - the event channel to listen to * @param func - the listener to attach */ - #addRendererListener( - channel: MainChannels, - func: (data: any) => void, - ): void { + #addRendererListener(channel: MainChannels, func: (data: any) => void): void { ipcMain.on(channel, (_event, data: any) => func(data)); } diff --git a/src/main/network/PacketStream.ts b/src/main/network/PacketStream.ts index 0e97fc4..0a5333a 100644 --- a/src/main/network/PacketStream.ts +++ b/src/main/network/PacketStream.ts @@ -38,14 +38,21 @@ export default class PacketStream extends Transform { * @param callback - a callback function to be called with the consumed chunk or an error * following processing. */ - _transform(chunk: any, encoding: BufferEncoding, callback: TransformCallback) { + _transform( + chunk: any, + encoding: BufferEncoding, + callback: TransformCallback, + ) { let chunkBuf: Buffer; if (chunk instanceof Buffer) { chunkBuf = chunk; } else if (typeof chunk === 'string') { chunkBuf = Buffer.from(chunk, encoding); } else { - callback(new Error('PacketStream does not support writable object mode.'), null); + callback( + new Error('PacketStream does not support writable object mode.'), + null, + ); return; } this.#buf.push(chunkBuf); @@ -83,7 +90,10 @@ export default class PacketStream extends Transform { } this.#buf[0] = this.#buf[0].subarray(HEADER_LENGTH); // Trim off header this.#buf = [Buffer.concat(this.#buf)]; // Get packet data in one Buffer - this.push({ type: packetType, data: this.#buf[0].subarray(0, packetLength) }); + this.push({ + type: packetType, + data: this.#buf[0].subarray(0, packetLength), + }); this.#buf[0] = this.#buf[0].subarray(packetLength); // Trim off packet data this.#bufLen -= HEADER_LENGTH + packetLength; return true; // Packet successfully read, more may follow diff --git a/src/main/network/RuntimeComms.ts b/src/main/network/RuntimeComms.ts index c9a1e35..49b9e03 100644 --- a/src/main/network/RuntimeComms.ts +++ b/src/main/network/RuntimeComms.ts @@ -1,4 +1,8 @@ -import { Socket as TCPSocket, SocketAddress, createConnection as createTcpConnection } from 'net'; +import { + Socket as TCPSocket, + SocketAddress, + createConnection as createTcpConnection, +} from 'net'; import { Socket as UDPSocket, createSocket as createUdpSocket } from 'dgram'; import PacketStream from './PacketStream'; import DeviceInfoState from '../../common/DeviceInfoState'; @@ -20,7 +24,7 @@ enum MsgType { DEVICE_DATA = 4, // 5 reserved for some Shepherd msg type INPUTS = 6, - TIME_STAMPS = 7 + TIME_STAMPS = 7, } /** @@ -139,6 +143,7 @@ export default class RuntimeComms { this.#disconnectTcp(); this.#disconnectUdp(); } + /** * Sets the host and address of the Runtime instance to connect to. * @param ip - the address and optionally the port Runtime is listening on, separated by a colon. @@ -159,6 +164,7 @@ export default class RuntimeComms { this.#connectTcp(); // Reconnect TCP, UDP will just start sending to new host return true; } + /** * Sends a new run mode. * @param runMode - the new run mode. @@ -168,6 +174,7 @@ export default class RuntimeComms { this.#tcpSock.write(this.#createPacket(MsgType.RUN_MODE, runMode)); } } + /** * Sends device preferences. * @param deviceData - device preferences to send. @@ -177,6 +184,7 @@ export default class RuntimeComms { this.#tcpSock.write(this.#createPacket(MsgType.DEVICE_DATA, deviceData)); } } + /** * Sends challenge data. * @param data - the textual challenge data to send. @@ -186,6 +194,7 @@ export default class RuntimeComms { this.#tcpSock.write(this.#createPacket(MsgType.CHALLENGE_DATA, data)); } } + /** * Sends the robot's starting position. * @param startPos - the robot's starting position index to send. @@ -195,26 +204,29 @@ export default class RuntimeComms { this.#tcpSock.write(this.#createPacket(MsgType.START_POS, startPos)); } } + /** * Sends control inputs to the robot. * @param inputs - the inputs to send. * @param source - the device that is the source of the inputs. */ sendInputs(inputs: protos.Input[], source: protos.Source) { - //if (this.#udpSock) { + // if (this.#udpSock) { // this.udpSock.send(protos.UserInputs.encode({ // inputs: inputs.length ? inputs : [ // protos.Input.create({ connected: false, source }) // ], // }), this.#runtimePort, this.#runtimeAddr); - //} + // } // Old Dawn sends inputs through TCP, though comments say this is just for 2021? if (this.#tcpSock) { - this.#tcpSock.write(this.#createPacket(MsgType.INPUTS, { - inputs: inputs.length ? inputs : [ - protos.Input.create({ connected: false, source }) - ], - })); + this.#tcpSock.write( + this.#createPacket(MsgType.INPUTS, { + inputs: inputs.length + ? inputs + : [protos.Input.create({ connected: false, source })], + }), + ); } } @@ -224,10 +236,18 @@ export default class RuntimeComms { */ #sendLatencyTest() { if (this.#tcpSock) { - this.#tcpSock.write(this.#createPacket(MsgType.TIME_STAMPS, - new protos.TimeStamps({ dawnTimestamp: Date.now(), runtimeTimestamp: 0 }))); + this.#tcpSock.write( + this.#createPacket( + MsgType.TIME_STAMPS, + new protos.TimeStamps({ + dawnTimestamp: Date.now(), + runtimeTimestamp: 0, + }), + ), + ); } } + /** * Disconnects the old TCP socket if open, then makes a new one and connects it to the * most recently known Runtime IP and port. @@ -235,15 +255,24 @@ export default class RuntimeComms { #connectTcp() { this.#tcpDisconnected = false; this.#disconnectTcp(); - const tcpStream = new PacketStream() - .on('data', this.#handlePacket.bind(this)); + const tcpStream = new PacketStream().on( + 'data', + this.#handlePacket.bind(this), + ); this.#tcpSock = createTcpConnection(this.#runtimePort, this.#runtimeAddr) .on('connect', this.#handleTcpConnection.bind(this)) .on('close', this.#handleTcpClose.bind(this)) - .on('error', this.#commsListener.onRuntimeTcpError.bind(this.#commsListener)); + .on( + 'error', + this.#commsListener.onRuntimeTcpError.bind(this.#commsListener), + ); this.#tcpSock.pipe(tcpStream); - this.#pingInterval = setInterval(this.#sendLatencyTest.bind(this), PING_INTERVAL); + this.#pingInterval = setInterval( + this.#sendLatencyTest.bind(this), + PING_INTERVAL, + ); } + /** * Closes the old UDP socket if open, then makes and binds a new one. */ @@ -252,10 +281,15 @@ export default class RuntimeComms { this.#udpSock = createUdpSocket({ type: 'udp4', reuseAddr: true, - }).on('error', this.#commsListener.onRuntimeUdpError.bind(this.#commsListener)) + }) + .on( + 'error', + this.#commsListener.onRuntimeUdpError.bind(this.#commsListener), + ) .on('message', this.#handleUdpMessage.bind(this)) .bind(UDP_PORT); } + /** * Ends and disconnects the TCP socket if open. */ @@ -265,6 +299,7 @@ export default class RuntimeComms { this.#tcpSock = null; } } + /** * Closes the UDP socket if open. */ @@ -274,6 +309,7 @@ export default class RuntimeComms { this.#udpSock = null; } } + /** * Handler for TCP 'connect' event. */ @@ -282,44 +318,65 @@ export default class RuntimeComms { this.#tcpSock.write(new Uint8Array([1])); // Tell Runtime that we are Dawn, not Shepherd } } + /** * Processes a received packet. * @param packet - the received packet. */ #handlePacket(packet: Packet) { - const {type, data} = packet; + const { type, data } = packet; switch (type) { case MsgType.LOG: - this.#commsListener.onReceiveRobotLogs(protos.Text.decode(data).payload); + this.#commsListener.onReceiveRobotLogs( + protos.Text.decode(data).payload, + ); break; case MsgType.TIME_STAMPS: this.#commsListener.onReceiveLatency( - (Date.now() - Number(protos.TimeStamps.decode(data))) / 2); + (Date.now() - Number(protos.TimeStamps.decode(data))) / 2, + ); break; case MsgType.CHALLENGE_DATA: // TODO: ??? Not implemented in old Dawn break; case MsgType.DEVICE_DATA: // Convert decoded Devices to DeviceInfoStates before passing to onReceiveDevices - this.#commsListener.onReceiveDevices(protos.DevData.decode(data).devices.map( - (deviceProps: protos.IDevice, _index: number, _array: protos.IDevice[]) => { - const device = new protos.Device(deviceProps); - return { - id: `${device.type.toString()}_${device.uid.toString()}`, - ...Object.fromEntries(device.params.map( - (paramProps: protos.IParam, _index: number, _array: protos.IParam[]) => { - const param = new protos.Param(paramProps); - return param.val ? [param.name, param.val.toString()] : []; - }) - ), - }; - } - )); + this.#commsListener.onReceiveDevices( + protos.DevData.decode(data).devices.map( + ( + deviceProps: protos.IDevice, + _index: number, + _array: protos.IDevice[], + ) => { + const device = new protos.Device(deviceProps); + return { + id: `${device.type.toString()}_${device.uid.toString()}`, + ...Object.fromEntries( + device.params.map( + ( + paramProps: protos.IParam, + _index: number, + _array: protos.IParam[], + ) => { + const param = new protos.Param(paramProps); + return param.val + ? [param.name, param.val.toString()] + : []; + }, + ), + ), + }; + }, + ), + ); break; default: - this.#commsListener.onRuntimeError(new Error(`Received unexpected packet MsgType ${type}.`)); + this.#commsListener.onRuntimeError( + new Error(`Received unexpected packet MsgType ${type}.`), + ); } } + /** * Processes a Buffer assumed to be the payload of a device data packet with some error checking * code because UDP packets might not be well-formed. @@ -332,6 +389,7 @@ export default class RuntimeComms { this.#commsListener.onRuntimeUdpError(err as Error); } } + /** * Handles TCP 'close' event and tries to reconnect if we didn't cause the disconnection. */ @@ -341,6 +399,7 @@ export default class RuntimeComms { setTimeout(this.#connectTcp, TCP_RECONNECT_DELAY); } } + /** * Encodes a device state packet. * @param type - the packet type. @@ -348,6 +407,7 @@ export default class RuntimeComms { * @returns The packet encoded in a Buffer */ #createPacket(type: MsgType.DEVICE_DATA, data: protos.IDevData): Buffer; + /** * Encodes a run mode packet. * @param type - the packet type. @@ -355,6 +415,7 @@ export default class RuntimeComms { * @returns The packet encoded in a Buffer */ #createPacket(type: MsgType.RUN_MODE, data: protos.IRunMode): Buffer; + /** * Encodes a robot start position packet. * @param type - the packet type. @@ -362,6 +423,7 @@ export default class RuntimeComms { * @returns The packet encoded in a Buffer */ #createPacket(type: MsgType.START_POS, data: protos.IStartPos): Buffer; + /** * Encodes a ping packet. * @param type - the packet type. @@ -369,6 +431,7 @@ export default class RuntimeComms { * @returns The packet encoded in a Buffer */ #createPacket(type: MsgType.TIME_STAMPS, data: protos.ITimeStamps): Buffer; + /** * Encodes a challenge data packet. * @param type - the packet type. @@ -376,6 +439,7 @@ export default class RuntimeComms { * @returns The packet encoded in a Buffer */ #createPacket(type: MsgType.CHALLENGE_DATA, data: protos.IText): Buffer; + /** * Encodes an input state packet. * @param type - the packet type. @@ -383,6 +447,7 @@ export default class RuntimeComms { * @returns The packet encoded in a Buffer */ #createPacket(type: MsgType.INPUTS, data: protos.IUserInputs): Buffer; + /** * Encodes a packet. * @param type - the packet type. @@ -402,17 +467,23 @@ export default class RuntimeComms { packetData = protos.StartPos.encode(data as protos.IStartPos).finish(); break; case MsgType.TIME_STAMPS: - packetData = protos.TimeStamps.encode(data as protos.ITimeStamps).finish(); + packetData = protos.TimeStamps.encode( + data as protos.ITimeStamps, + ).finish(); break; case MsgType.CHALLENGE_DATA: packetData = protos.Text.encode(data as protos.IText).finish(); break; case MsgType.INPUTS: // Source says input data isn't usually sent through TCP? What's that about? - packetData = protos.UserInputs.encode(data as protos.IUserInputs).finish(); + packetData = protos.UserInputs.encode( + data as protos.IUserInputs, + ).finish(); break; default: - this.#commsListener.onRuntimeError(new Error(`Cannot create packet with type ${type}.`)); + this.#commsListener.onRuntimeError( + new Error(`Cannot create packet with type ${type}.`), + ); packetData = Buffer.alloc(0); break; } @@ -420,7 +491,11 @@ export default class RuntimeComms { const packet = Buffer.allocUnsafe(3 + packetData.byteLength); packet.writeUInt8(type, 0); packet.writeUInt16LE(packetData.byteLength, 1); - Buffer.from(packetData.buffer, packetData.byteOffset, packetData.byteLength) .copy(packet, 3); + Buffer.from( + packetData.buffer, + packetData.byteOffset, + packetData.byteLength, + ).copy(packet, 3); return packet; } diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index d299b4c..859e02b 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -245,14 +245,20 @@ export default function App() { setFieldStationNum(data.fieldStationNumber); setShowDirtyUploadWarning(data.showDirtyUploadWarning); }), - window.electron.ipcRenderer.on('renderer-battery-update', setRobotBatteryVoltage), + window.electron.ipcRenderer.on( + 'renderer-battery-update', + setRobotBatteryVoltage, + ), window.electron.ipcRenderer.on('renderer-latency-update', (latency) => { setRobotLatencyMs(latency); if (latency === -1) { setDeviceInfoState([]); // Disconnect everything } }), - window.electron.ipcRenderer.on('renderer-devices-update', setDeviceInfoState), + window.electron.ipcRenderer.on( + 'renderer-devices-update', + setDeviceInfoState, + ), ]; return () => listenerDestructors.forEach((destructor) => destructor()); } From f3d662972aaf8266ad5635e01d50c99771066fc5 Mon Sep 17 00:00:00 2001 From: Eddy Meals Date: Wed, 11 Sep 2024 01:15:49 -0700 Subject: [PATCH 08/14] Remove uses of runtime version from renderer --- src/renderer/App.tsx | 3 --- src/renderer/Topbar.tsx | 7 ------- 2 files changed, 10 deletions(-) diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index 859e02b..d562481 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -43,8 +43,6 @@ export default function App() { const [activeModal, setActiveModal] = useState(''); // Dawn version string, received from main process const [dawnVersion, setDawnVersion] = useState(''); - // Runtime version string, empty if disconnected from robot - const [runtimeVersion, setRuntimeVersion] = useState(''); // Robot battery voltage, -1 if disconnected from robot const [robotBatteryVoltage, setRobotBatteryVoltage] = useState(-1); // Robot latency, -1 if disconnected from robot @@ -331,7 +329,6 @@ export default function App() { changeActiveModal('ConnectionConfig') } dawnVersion={dawnVersion} - runtimeVersion={runtimeVersion} robotLatencyMs={robotLatencyMs} robotBatteryVoltage={robotBatteryVoltage} /> diff --git a/src/renderer/Topbar.tsx b/src/renderer/Topbar.tsx index 83c80c6..4360795 100644 --- a/src/renderer/Topbar.tsx +++ b/src/renderer/Topbar.tsx @@ -8,21 +8,17 @@ import './Topbar.css'; * be opened * @param props.robotLatencyMs - latency in milliseconds of the connection to the currently * connected robot, or -1 if there is no robot connected - * @param props.runtimeVersion - version string of runtime running on currently connected robot. The - * value is not used if robotLatencyMs is -1 * @param props.robotBatteryVoltage - battery voltage in volts of the currently connected robot. The * value is not used if robotLatencyMs is -1 * @param props.dawnVersion - version string of Dawn */ export default function Topbar({ onConnectionConfigModalOpen, - runtimeVersion, robotBatteryVoltage, robotLatencyMs, dawnVersion, }: { onConnectionConfigModalOpen: () => void; - runtimeVersion: string; robotBatteryVoltage: number; robotLatencyMs: number; dawnVersion: string; @@ -34,9 +30,6 @@ export default function Topbar({
) : ( <> -
- Runtime v{runtimeVersion} -
Battery: {robotBatteryVoltage} V
Latency: {robotLatencyMs} ms
From cc4266ff57a94950a13915a367decfe4ca1654f0 Mon Sep 17 00:00:00 2001 From: Eddy Meals Date: Wed, 11 Sep 2024 01:16:03 -0700 Subject: [PATCH 09/14] Lint everything --- .eslintrc.js | 8 +++- src/main/MainApp.ts | 65 +++++++++++++++++--------------- src/main/network/PacketStream.ts | 3 +- src/main/network/RuntimeComms.ts | 11 +++--- 4 files changed, 49 insertions(+), 38 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 58ad7ca..85089f7 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -12,8 +12,14 @@ module.exports = { 'no-shadow': 'off', '@typescript-eslint/no-shadow': 'error', 'no-unused-vars': 'off', - '@typescript-eslint/no-unused-vars': 'error', + '@typescript-eslint/no-unused-vars': [ + 'error', + { + argsIgnorePattern: '^_', + }, + ], 'no-redeclare': 'off', // False positives on typescript's function overloads + 'no-undef': 'off', // False positives and typescript won't build if actually undefined vars }, parserOptions: { ecmaVersion: 2022, diff --git a/src/main/MainApp.ts b/src/main/MainApp.ts index 3fa7e76..0302f41 100644 --- a/src/main/MainApp.ts +++ b/src/main/MainApp.ts @@ -55,6 +55,38 @@ const ROBOT_SSH_USER = 'pi'; */ const ROBOT_SSH_PASS = 'raspberry'; +/** + * Adds a listener for the main-quit IPC event fired by the renderer. + * @param channel - the event channel to listen to + * @param func - the listener to attach + */ +function addRendererListener( + channel: 'main-quit', + func: (data: MainQuitData) => void, +): void; + +/** + * Adds a listener for the main-file-control IPC event fired by the renderer. + * @param channel - the event channel to listen to + * @param func - the listener to attach + */ +function addRendererListener( + channel: 'main-file-control', + func: (data: MainFileControlData) => void, +): void; + +/** + * Typed wrapper function to listen to IPC events from the renderer. + * @param channel - the event channel to listen to + * @param func - the listener to attach + */ +function addRendererListener( + channel: MainChannels, + func: (data: any) => void, +): void { + ipcMain.on(channel, (_event, data: any) => func(data)); +} + /** * Manages state owned by the main electron process. */ @@ -126,7 +158,7 @@ export default class MainApp implements MenuHandler, RuntimeCommsListener { this.#sendToRenderer('renderer-quit-request'); } }); - this.#addRendererListener('main-file-control', (data) => { + addRendererListener('main-file-control', (data) => { if (data.type === 'save') { this.#saveCodeFile(data.content, data.forceDialog); } else if (data.type === 'load') { @@ -140,7 +172,7 @@ export default class MainApp implements MenuHandler, RuntimeCommsListener { this.#watcher?.close(); } }); - this.#addRendererListener('main-quit', (data) => { + addRendererListener('main-quit', (data) => { // Save config that may have been changed while the program was running this.#config.robotIPAddress = data.robotIPAddress; this.#config.robotSSHAddress = data.robotSSHAddress; @@ -507,35 +539,6 @@ export default class MainApp implements MenuHandler, RuntimeCommsListener { ); } - /** - * Adds a listener for the main-quit IPC event fired by the renderer. - * @param channel - the event channel to listen to - * @param func - the listener to attach - */ - #addRendererListener( - channel: 'main-quit', - func: (data: MainQuitData) => void, - ): void; - - /** - * Adds a listener for the main-file-control IPC event fired by the renderer. - * @param channel - the event channel to listen to - * @param func - the listener to attach - */ - #addRendererListener( - channel: 'main-file-control', - func: (data: MainFileControlData) => void, - ): void; - - /** - * Typed wrapper function to listen to IPC events from the renderer. - * @param channel - the event channel to listen to - * @param func - the listener to attach - */ - #addRendererListener(channel: MainChannels, func: (data: any) => void): void { - ipcMain.on(channel, (_event, data: any) => func(data)); - } - /** * Sends a renderer-quit-request IPC event to the renderer. * @param channel - the channel to send the event on diff --git a/src/main/network/PacketStream.ts b/src/main/network/PacketStream.ts index 0a5333a..29c3653 100644 --- a/src/main/network/PacketStream.ts +++ b/src/main/network/PacketStream.ts @@ -38,9 +38,10 @@ export default class PacketStream extends Transform { * @param callback - a callback function to be called with the consumed chunk or an error * following processing. */ + // eslint-disable-next-line no-underscore-dangle _transform( chunk: any, - encoding: BufferEncoding, + encoding: NodeJS.BufferEncoding, callback: TransformCallback, ) { let chunkBuf: Buffer; diff --git a/src/main/network/RuntimeComms.ts b/src/main/network/RuntimeComms.ts index 49b9e03..a1c042e 100644 --- a/src/main/network/RuntimeComms.ts +++ b/src/main/network/RuntimeComms.ts @@ -150,10 +150,11 @@ export default class RuntimeComms { * @returns Whether the given IP forms a valid address. */ setRobotIp(ip: string) { - this.#runtimeAddr = ip.split(':')[0]; + [this.#runtimeAddr] = ip.split(':'); const portStr: string | undefined = ip.split(':')[1]; this.#runtimePort = portStr ? Number(portStr) : DEFAULT_RUNTIME_PORT; try { + // eslint-disable-next-line no-new new SocketAddress({ address: this.#runtimeAddr, port: this.#runtimePort, @@ -345,8 +346,8 @@ export default class RuntimeComms { protos.DevData.decode(data).devices.map( ( deviceProps: protos.IDevice, - _index: number, - _array: protos.IDevice[], + _devIdx: number, + _devArr: protos.IDevice[], ) => { const device = new protos.Device(deviceProps); return { @@ -355,8 +356,8 @@ export default class RuntimeComms { device.params.map( ( paramProps: protos.IParam, - _index: number, - _array: protos.IParam[], + _paramIdx: number, + _paramArr: protos.IParam[], ) => { const param = new protos.Param(paramProps); return param.val From 10e63652632af5ae7b3e33e8fb143473991f7293 Mon Sep 17 00:00:00 2001 From: Eddy Meals Date: Fri, 13 Sep 2024 00:42:58 -0700 Subject: [PATCH 10/14] Have start/stop buttons in Editor send run mode --- src/common/IpcEventTypes.ts | 11 ++++++++++- src/main/MainApp.ts | 17 +++++++++++++++-- src/main/preload.ts | 5 +++++ src/renderer/App.tsx | 13 ++++++++----- 4 files changed, 38 insertions(+), 8 deletions(-) diff --git a/src/common/IpcEventTypes.ts b/src/common/IpcEventTypes.ts index 29a8a8a..2d52295 100644 --- a/src/common/IpcEventTypes.ts +++ b/src/common/IpcEventTypes.ts @@ -1,5 +1,6 @@ import type AppConsoleMessage from './AppConsoleMessage'; import type DeviceInfoState from './DeviceInfoState'; +import { Mode as RobotRunMode } from '../../protos-main/protos'; /** * IPC event channels used for communication from the main process to the renderer. @@ -15,7 +16,10 @@ export type RendererChannels = /** * IPC event channels used for communication from the renderer to the main process. */ -export type MainChannels = 'main-file-control' | 'main-quit'; +export type MainChannels = + | 'main-file-control' + | 'main-quit' + | 'main-update-robot-mode'; /** * Data for the renderer-init event, sent when the renderer process has finished initializing and is @@ -296,3 +300,8 @@ export interface MainQuitData { */ showDirtyUploadWarning: boolean; } +/** + * Data for the main-update-robot-mode event sent by the renderer to stop the robot or start it with + * a specified opmode. + */ +export type MainUpdateRobotModeData = RobotRunMode; diff --git a/src/main/MainApp.ts b/src/main/MainApp.ts index 0302f41..9f28789 100644 --- a/src/main/MainApp.ts +++ b/src/main/MainApp.ts @@ -15,9 +15,9 @@ import type { MainChannels, MainFileControlData, MainQuitData, + MainUpdateRobotModeData, } from '../common/IpcEventTypes'; -import type Config from './Config'; -import { coerceToConfig } from './Config'; +import Config, { coerceToConfig } from './Config'; import CodeTransfer from './network/CodeTransfer'; import RuntimeComms, { RuntimeCommsListener } from './network/RuntimeComms'; import type { MenuHandler } from './menu'; @@ -75,6 +75,16 @@ function addRendererListener( func: (data: MainFileControlData) => void, ): void; +/** + * Adds a listener for the main-update-robot-mode IPC event fired by the renderer. + * @param channel - the event channel to listen to + * @param func - the listener to attach + */ +function addRendererListener( + channel: 'main-update-robot-mode', + func: (data: MainUpdateRobotModeData) => void, +): void; + /** * Typed wrapper function to listen to IPC events from the renderer. * @param channel - the event channel to listen to @@ -188,6 +198,9 @@ export default class MainApp implements MenuHandler, RuntimeCommsListener { this.#preventQuit = false; this.#mainWindow.close(); }); + addRendererListener('main-update-robot-mode', (mode) => { + this.#runtimeComms.sendRunMode({ mode }); + }); try { this.#config = coerceToConfig( diff --git a/src/main/preload.ts b/src/main/preload.ts index 9eb4ee5..bdcc80b 100644 --- a/src/main/preload.ts +++ b/src/main/preload.ts @@ -5,6 +5,7 @@ import type { RendererChannels, MainChannels, MainFileControlData, + MainUpdateRobotModeData, RendererInitData, RendererBatteryUpdateData, RendererLatencyUpdateData, @@ -19,6 +20,10 @@ function sendMessage( channel: 'main-file-control', data: MainFileControlData, ): void; +function sendMessage( + channel: 'main-update-robot-mode', + data: MainUpdateRobotModeData, +): 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 d562481..76d2cdf 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -18,6 +18,7 @@ import ConnectionConfigModal, { } from './modals/ConnectionConfigModal'; import GamepadInfoModal from './modals/GamepadInfoModal'; import ResizeBar from './ResizeBar'; +import { Mode as RobotRunMode } from '../../protos-main/protos'; import './App.css'; const INITIAL_EDITOR_WIDTH_PERCENT = 0.7; @@ -159,6 +160,8 @@ export default function App() { return true; }; const endColsResize = () => setConsoleInitSize(-1); + const changeRunMode = (mode: RobotRunMode) => + window.electron.ipcRenderer.sendMessage('main-update-robot-mode', mode); const closeWindow = useCallback(() => { window.electron.ipcRenderer.sendMessage('main-quit', { @@ -347,12 +350,12 @@ export default function App() { onNewFile={createNewFile} onRobotUpload={() => uploadDownloadFile(true)} onRobotDownload={() => uploadDownloadFile(false)} - onStartRobot={() => { - throw new Error('Not implemented.'); - }} - onStopRobot={() => { - throw new Error('Not implemented.'); + onStartRobot={(opmode: 'auto' | 'teleop') => { + changeRunMode( + opmode === 'auto' ? RobotRunMode.AUTO : RobotRunMode.TELEOP, + ); }} + onStopRobot={() => changeRunMode(RobotRunMode.IDLE)} onToggleConsole={() => { setConsoleIsOpen((v) => !v); setConsoleIsAlerted(false); From 498fed204c622222e3849b897e7ef166dd8e39d3 Mon Sep 17 00:00:00 2001 From: Eddy Meals Date: Sat, 14 Sep 2024 16:47:34 -0700 Subject: [PATCH 11/14] Disable toolbar buttons depending on robot status --- assets/download.svg | 18 ++++-------------- assets/upload.svg | 18 ++++-------------- src/renderer/App.tsx | 9 ++++++++- src/renderer/Editor.css | 8 +++++++- src/renderer/Editor.tsx | 40 ++++++++++++++++++++++++++++++++++++---- 5 files changed, 59 insertions(+), 34 deletions(-) diff --git a/assets/download.svg b/assets/download.svg index eac7690..35ae8f6 100755 --- a/assets/download.svg +++ b/assets/download.svg @@ -9,18 +9,8 @@ xmlns:svg="http://www.w3.org/2000/svg"> - - - - + diff --git a/assets/upload.svg b/assets/upload.svg index cc00c52..ceeb9eb 100755 --- a/assets/upload.svg +++ b/assets/upload.svg @@ -9,18 +9,8 @@ xmlns:svg="http://www.w3.org/2000/svg"> - - - - + diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index 76d2cdf..b6d9769 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -67,6 +67,8 @@ export default function App() { const [consoleIsAlerted, setConsoleIsAlerted] = useState(false); // Whether keyboard controls are enabled const [kbCtrlEnabled, setKbCtrlEnabled] = 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 const [windowSize, setWindowSize] = useReducer( (oldSize: [number, number], newSize: [number, number]) => { @@ -160,8 +162,10 @@ export default function App() { return true; }; const endColsResize = () => setConsoleInitSize(-1); - const changeRunMode = (mode: RobotRunMode) => + const changeRunMode = (mode: RobotRunMode) => { window.electron.ipcRenderer.sendMessage('main-update-robot-mode', mode); + setRobotRunning(mode !== RobotRunMode.IDLE); + }; const closeWindow = useCallback(() => { window.electron.ipcRenderer.sendMessage('main-quit', { @@ -254,6 +258,7 @@ export default function App() { setRobotLatencyMs(latency); if (latency === -1) { setDeviceInfoState([]); // Disconnect everything + setRobotRunning(false); } }), window.electron.ipcRenderer.on( @@ -345,6 +350,8 @@ export default function App() { consoleAlert={consoleIsAlerted} consoleIsOpen={consoleIsOpen} keyboardControlsEnabled={kbCtrlEnabled} + robotConnected={robotLatencyMs !== -1} + robotRunning={robotRunning} onOpen={loadFile} onSave={saveFile} onNewFile={createNewFile} diff --git a/src/renderer/Editor.css b/src/renderer/Editor.css index 410d7fa..f0bcb58 100644 --- a/src/renderer/Editor.css +++ b/src/renderer/Editor.css @@ -59,12 +59,18 @@ height: 100%; transition: filter 0.25s; /* Fix other filter variables so transition is directly from black to blue: */ - filter: invert(0%) sepia(100%) hue-rotate(180deg) saturate(10); + filter: invert(0%) sepia(100%) hue-rotate(180deg) saturate(0); } .Editor-tbbtn-toggled img { /* CSS filter witchcraft producing #0088ff */ filter: invert(40%) sepia(100%) hue-rotate(180deg) saturate(10); } +.Editor-toolbar .Editor-tbbtn-disabled { + cursor: default; +} +.Editor-tbbtn-disabled img { + filter: invert(60%) hue-rotate(180deg); +} .Editor-tbbtn-alert { position: absolute; transform: translate(50%, 50%); diff --git a/src/renderer/Editor.tsx b/src/renderer/Editor.tsx index 58e2482..07a70e9 100644 --- a/src/renderer/Editor.tsx +++ b/src/renderer/Editor.tsx @@ -53,6 +53,10 @@ const STATUS_TEXT: { [k in EditorContentStatus]: string } = { * indicating the console is open * @param props.keyboardControlsEnabled - whether to show a different icon for the toggle keyboard * control button indicating keyboard control is enabled + * @param props.robotConnected - whether toolbar buttons requiring a connection to the robot should + * be enabled. + * @param props.robotRunning - whether the robot is running, which affects whether some toolbar + * buttons are enabled. * @param props.onOpen - handler called when the user wants to open a file in the editor * @param props.onNewFile - handler called when the user wants to close the current file * @param props.onRobotUpload - handler called when the user wants to upload the open file to the @@ -73,6 +77,8 @@ export default function Editor({ consoleAlert, consoleIsOpen, keyboardControlsEnabled, + robotConnected, + robotRunning, onOpen, onSave, onNewFile, @@ -96,6 +102,8 @@ export default function Editor({ consoleAlert: boolean; consoleIsOpen: boolean; keyboardControlsEnabled: boolean; + robotConnected: boolean; + robotRunning: boolean; onOpen: () => void; /** * handler called when the user wants to save the contents of the editor @@ -149,12 +157,18 @@ export default function Editor({
- -
From 512a327ae715cb1379f7890919f0ec57a3354a3e Mon Sep 17 00:00:00 2001 From: Victoria Lee Date: Sat, 28 Sep 2024 16:47:54 -0700 Subject: [PATCH 12/14] Test with staff robot and debug --- src/main/MainApp.ts | 6 ++++- src/main/network/RuntimeComms.ts | 41 ++++++++++++++++---------------- 2 files changed, 26 insertions(+), 21 deletions(-) diff --git a/src/main/MainApp.ts b/src/main/MainApp.ts index 9f28789..19a6310 100644 --- a/src/main/MainApp.ts +++ b/src/main/MainApp.ts @@ -215,6 +215,8 @@ export default class MainApp implements MenuHandler, RuntimeCommsListener { // Use all defaults if bad JSON or no config file this.#config = coerceToConfig({}); } + + this.#runtimeComms.setRobotIp(this.#config.robotIPAddress); } /** @@ -621,6 +623,8 @@ export default class MainApp implements MenuHandler, RuntimeCommsListener { * @param data - an optional payload for the event */ #sendToRenderer(channel: RendererChannels, data?: any): void { - this.#mainWindow.webContents.send(channel, data); + if (!this.#mainWindow.isDestroyed()) { + this.#mainWindow.webContents.send(channel, data); + } } } diff --git a/src/main/network/RuntimeComms.ts b/src/main/network/RuntimeComms.ts index a1c042e..60a3704 100644 --- a/src/main/network/RuntimeComms.ts +++ b/src/main/network/RuntimeComms.ts @@ -19,12 +19,11 @@ const PING_INTERVAL = 5000; 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, } /** @@ -192,7 +191,8 @@ export default class RuntimeComms { */ sendChallengeInputs(data: protos.IText) { if (this.#tcpSock) { - this.#tcpSock.write(this.#createPacket(MsgType.CHALLENGE_DATA, data)); + throw new Error('Not implemented.'); // MsgTypes from old dawn are inconsistent? + //this.#tcpSock.write(this.#createPacket(MsgType.CHALLENGE_DATA, data)); } } @@ -254,6 +254,7 @@ export default class RuntimeComms { * most recently known Runtime IP and port. */ #connectTcp() { + this.#commsListener.onRuntimeError(new Error("Attempting to connect tcp socket")); this.#tcpDisconnected = false; this.#disconnectTcp(); const tcpStream = new PacketStream().on( @@ -328,18 +329,18 @@ export default class RuntimeComms { const { type, data } = packet; switch (type) { case MsgType.LOG: - this.#commsListener.onReceiveRobotLogs( - protos.Text.decode(data).payload, - ); + //this.#commsListener.onReceiveRobotLogs( + // protos.Text.decode(data).payload, + //); break; case MsgType.TIME_STAMPS: this.#commsListener.onReceiveLatency( (Date.now() - Number(protos.TimeStamps.decode(data))) / 2, ); break; - case MsgType.CHALLENGE_DATA: + //case MsgType.CHALLENGE_DATA: // TODO: ??? Not implemented in old Dawn - break; + //break; case MsgType.DEVICE_DATA: // Convert decoded Devices to DeviceInfoStates before passing to onReceiveDevices this.#commsListener.onReceiveDevices( @@ -361,7 +362,7 @@ export default class RuntimeComms { ) => { const param = new protos.Param(paramProps); return param.val - ? [param.name, param.val.toString()] + ? [param.name, param[param.val].toString()] : []; }, ), @@ -373,7 +374,7 @@ export default class RuntimeComms { break; default: this.#commsListener.onRuntimeError( - new Error(`Received unexpected packet MsgType ${type}.`), + new Error(`Received unexpected packet MsgType ${type}.\nPacket: ${JSON.stringify(packet)}`), ); } } @@ -397,7 +398,7 @@ export default class RuntimeComms { #handleTcpClose() { this.#commsListener.onRuntimeDisconnect(); if (!this.#tcpDisconnected) { - setTimeout(this.#connectTcp, TCP_RECONNECT_DELAY); + setTimeout(this.#connectTcp.bind(this), TCP_RECONNECT_DELAY); } } @@ -433,13 +434,13 @@ export default class RuntimeComms { */ #createPacket(type: MsgType.TIME_STAMPS, data: protos.ITimeStamps): Buffer; - /** + /* * Encodes a challenge data packet. * @param type - the packet type. * @param data - the packet payload. * @returns The packet encoded in a Buffer */ - #createPacket(type: MsgType.CHALLENGE_DATA, data: protos.IText): Buffer; + //#createPacket(type: MsgType.CHALLENGE_DATA, data: protos.IText): Buffer; /** * Encodes an input state packet. @@ -472,9 +473,9 @@ export default class RuntimeComms { data as protos.ITimeStamps, ).finish(); break; - case MsgType.CHALLENGE_DATA: - packetData = protos.Text.encode(data as protos.IText).finish(); - break; + //case MsgType.CHALLENGE_DATA: + //packetData = protos.Text.encode(data as protos.IText).finish(); + //break; case MsgType.INPUTS: // Source says input data isn't usually sent through TCP? What's that about? packetData = protos.UserInputs.encode( From 7d4a7d84514c67cb7c7e7320a69ade9c154c3337 Mon Sep 17 00:00:00 2001 From: Victoria Lee Date: Sat, 28 Sep 2024 16:48:17 -0700 Subject: [PATCH 13/14] Fiddle with AppConsole styling --- src/renderer/AppConsole.css | 6 ++---- src/renderer/Editor.css | 1 + 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/renderer/AppConsole.css b/src/renderer/AppConsole.css index 20e7ffe..6b29421 100644 --- a/src/renderer/AppConsole.css +++ b/src/renderer/AppConsole.css @@ -10,8 +10,8 @@ .AppConsole-inner { height: 100%; background-color: #eee; - overflow: hidden scroll; - overflow-wrap: break-word; + overflow-y: scroll; + word-break: break-word; margin: 0; margin-bottom: 50px; min-height: 0; @@ -19,8 +19,6 @@ .AppConsole-message { padding: 2px; } -.AppConsole-message-dawn-info { -} .AppConsole-message-dawn-err { color: red; } diff --git a/src/renderer/Editor.css b/src/renderer/Editor.css index f0bcb58..7e113eb 100644 --- a/src/renderer/Editor.css +++ b/src/renderer/Editor.css @@ -79,6 +79,7 @@ width: 40%; height: 40%; background-color: red; + z-index: 5; } .Editor-tbopmode { font-family: Verdana, monospace; From f3d97ea7b54b17a439846054054bb8344cd6da7a Mon Sep 17 00:00:00 2001 From: Eddy Meals Date: Sat, 12 Oct 2024 12:31:37 -0700 Subject: [PATCH 14/14] Lint --- src/main/network/RuntimeComms.ts | 31 +++++++++++++++++++------------ 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/src/main/network/RuntimeComms.ts b/src/main/network/RuntimeComms.ts index 60a3704..48ec7e2 100644 --- a/src/main/network/RuntimeComms.ts +++ b/src/main/network/RuntimeComms.ts @@ -189,10 +189,11 @@ export default class RuntimeComms { * Sends challenge data. * @param data - the textual challenge data to send. */ + // eslint-disable-next-line @typescript-eslint/no-unused-vars sendChallengeInputs(data: protos.IText) { if (this.#tcpSock) { throw new Error('Not implemented.'); // MsgTypes from old dawn are inconsistent? - //this.#tcpSock.write(this.#createPacket(MsgType.CHALLENGE_DATA, data)); + // this.#tcpSock.write(this.#createPacket(MsgType.CHALLENGE_DATA, data)); } } @@ -254,7 +255,9 @@ export default class RuntimeComms { * most recently known Runtime IP and port. */ #connectTcp() { - this.#commsListener.onRuntimeError(new Error("Attempting to connect tcp socket")); + this.#commsListener.onRuntimeError( + new Error('Attempting to connect tcp socket'), + ); this.#tcpDisconnected = false; this.#disconnectTcp(); const tcpStream = new PacketStream().on( @@ -329,18 +332,18 @@ export default class RuntimeComms { const { type, data } = packet; switch (type) { case MsgType.LOG: - //this.#commsListener.onReceiveRobotLogs( + // this.#commsListener.onReceiveRobotLogs( // protos.Text.decode(data).payload, - //); + // ); break; case MsgType.TIME_STAMPS: this.#commsListener.onReceiveLatency( (Date.now() - Number(protos.TimeStamps.decode(data))) / 2, ); break; - //case MsgType.CHALLENGE_DATA: - // TODO: ??? Not implemented in old Dawn - //break; + // case MsgType.CHALLENGE_DATA: + // TODO: ??? Not implemented in old Dawn + // break; case MsgType.DEVICE_DATA: // Convert decoded Devices to DeviceInfoStates before passing to onReceiveDevices this.#commsListener.onReceiveDevices( @@ -374,7 +377,11 @@ export default class RuntimeComms { break; default: this.#commsListener.onRuntimeError( - new Error(`Received unexpected packet MsgType ${type}.\nPacket: ${JSON.stringify(packet)}`), + new Error( + `Received unexpected packet MsgType ${type}.\nPacket: ${JSON.stringify( + packet, + )}`, + ), ); } } @@ -440,7 +447,7 @@ export default class RuntimeComms { * @param data - the packet payload. * @returns The packet encoded in a Buffer */ - //#createPacket(type: MsgType.CHALLENGE_DATA, data: protos.IText): Buffer; + // #createPacket(type: MsgType.CHALLENGE_DATA, data: protos.IText): Buffer; /** * Encodes an input state packet. @@ -473,9 +480,9 @@ export default class RuntimeComms { data as protos.ITimeStamps, ).finish(); break; - //case MsgType.CHALLENGE_DATA: - //packetData = protos.Text.encode(data as protos.IText).finish(); - //break; + // case MsgType.CHALLENGE_DATA: + // packetData = protos.Text.encode(data as protos.IText).finish(); + // break; case MsgType.INPUTS: // Source says input data isn't usually sent through TCP? What's that about? packetData = protos.UserInputs.encode(