Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 98 additions & 0 deletions src/hub/dataSources/rev/REVDataSource.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { LiveDataSource, LiveDataSourceStatus } from "../LiveDataSource";
import Log from "../../../shared/log/Log";
import { REVTelemetryClient } from "./REVTelemetryClient";

/*
* The REV DataSource is a WebSocket hosted by an external server.
* The server will be running either on the user's desktop, or a
* SystemCore.
*
* The server listens for a message in the format:
* ```
* {
* "key": "an arbitrary key"
* }
* ```
*
* where the key identifies this AdvantageScope instance.
* The server then registers this key, which is used to manage
* the status frames and devices that are exposed to advantage scope.
*
* Once a key is registered, and any frames are enabled on the server,
* messages of the form:
*
* ```
* {
* "name": "NameOfDevice",
* "descriptor": "technical descriptor used by server to identify the device",
* "timestamp": 1.23,
* "data": {
* "status0": {
* "isPoweredOn": true,
* "position": 123.456,
* },
* "status1": {
* "aField": 7
* }
* }
* }
* ```
*
* are sent by the server asynchronously. This data source
* will receive these messages, and create fields named
* `${frame.name}.${frame.data.<name>}.${field.name}`,
* such as `NameOfDevice.status0.isPoweredOn` in the previous frame example.
*
* Fields can have type of number or boolean.
*
* Currently, the data structure enforces that there must be two
* layers of indirection from the data object (such that data.foo.bar is valid,
* and data.bar or data.foo.baz.bar are both invalid).
*
* Any fields that are null will not be updated or removed, instead keeping the
* last value, and the timestamp will not be updated.
*
* No assumptions are made about the format of name, or data fields, meaning
* they can have any value. Specifically, data fields do not need to be named
* status<index>, but it is a good convention.
*/

export default class REVDataSource extends LiveDataSource {
client: REVTelemetryClient | undefined;
liveZeroTime: number | undefined;

override connect(
address: string,
statusCallback: (status: LiveDataSourceStatus) => void,
outputCallback: (log: Log, timeSupplier: () => number) => void
) {
super.connect(address, statusCallback, outputCallback, false);
this.log = new Log();
this.client = new REVTelemetryClient(this.log, this.onMessage.bind(this));
let port = window.preferences?.revTelemetryPort ?? 8080;
this.client.connect(address, port);
}

onMessage() {
this.setStatus(LiveDataSourceStatus.Active);

if (this.liveZeroTime === undefined) {
this.liveZeroTime = new Date().getTime() / 1000;
}

if (this.log === null) {
return;
}

// Run output callback
if (this.outputCallback !== null) {
this.outputCallback(this.log, () => {
if (this.log && this.liveZeroTime !== undefined) {
return new Date().getTime() / 1000 - this.liveZeroTime + this.log.getTimestampRange()[0];
} else {
return 0;
}
});
}
}
}
64 changes: 64 additions & 0 deletions src/hub/dataSources/rev/REVTelemetryClient.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import Log from "../../../shared/log/Log";

export class REVTelemetryClient {
log: Log;
onMessage: () => void;

constructor(log: Log, onMessage: () => void) {
this.log = log;
this.onMessage = onMessage;
}

connect(address: string, port: number) {
let socket = new WebSocket(`ws://${address}:${port}/v1/ws/status`);

socket.addEventListener("open", event => {
let key = window.preferences?.revTelemetryKey ?? crypto.randomUUID();
console.log(key);

socket.send(JSON.stringify({ key: key }));
});

socket.addEventListener("message", event => {
let statusData = JSON.parse(event.data);

if(statusData !== undefined && statusData !== null) {
this.handleStatusFrame(statusData);
}
});
}

handleStatusFrame(frameData: any) {
this.onMessage();
let timestamp = frameData.timestamp;
let name = frameData.name;

for(let statusFrameKey in frameData.data) {
let statusFrame = frameData.data[statusFrameKey];

if(statusFrame === undefined || statusFrame === null) {
continue;
}

if(typeof statusFrame !== "object") {
continue;
}

for(let fieldKey in statusFrame) {
let value = statusFrame[fieldKey];
let fieldName = `${name}/${statusFrameKey}/${fieldKey}`;

if(value !== undefined && value !== null) {
switch(typeof value) {
case "number":
this.log.putNumber(fieldName, timestamp, value);
break;
case "boolean":
this.log.putBoolean(fieldName, timestamp, value);
break;
}
}
}
}
}
}
3 changes: 3 additions & 0 deletions src/hub/hub.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import FTCDashboardSource from "./dataSources/ftcdashboard/FTCDashboardSource";
import { NT4Publisher, NT4PublisherStatus } from "./dataSources/nt4/NT4Publisher";
import NT4Source from "./dataSources/nt4/NT4Source";
import RLOGServerSource from "./dataSources/rlog/RLOGServerSource";
import REVDataSource from "./dataSources/rev/REVDataSource";

// Constants
const STATE_SAVE_PERIOD_MS = 250;
Expand Down Expand Up @@ -440,6 +441,8 @@ function startLive(isSim = false) {
case "ftcdashboard":
liveSource = new FTCDashboardSource();
break;
case "rev":
liveSource = new REVDataSource();
}

let address = "";
Expand Down
8 changes: 4 additions & 4 deletions src/main/electron/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1898,7 +1898,7 @@ function setupMenu() {
}
},
{ type: "separator" },
...(["nt4", "nt4-akit", "phoenix", "rlog", "ftcdashboard"] as const).map((liveMode: LiveMode) => {
...(["nt4", "nt4-akit", "phoenix", "rlog", "ftcdashboard", "rev"] as const).map((liveMode: LiveMode) => {
let item: Electron.MenuItemConstructorOptions = {
label: getLiveModeName(liveMode),
click(_, baseWindow) {
Expand Down Expand Up @@ -1929,7 +1929,7 @@ function setupMenu() {
}
},
{ type: "separator" },
...(["nt4", "nt4-akit", "phoenix", "rlog", "ftcdashboard"] as const).map((liveMode: LiveMode) => {
...(["nt4", "nt4-akit", "phoenix", "rlog", "ftcdashboard", "rev"] as const).map((liveMode: LiveMode) => {
let item: Electron.MenuItemConstructorOptions = {
label: getLiveModeName(liveMode),
click(_, baseWindow) {
Expand Down Expand Up @@ -3050,8 +3050,8 @@ function openPreferences(parentWindow: Electron.BrowserWindow) {
}

const width = 400;
const optionRows = 12;
const titleRows = 2;
const optionRows = 14;
const titleRows = 3;
const height = optionRows * 27 + titleRows * 34 + 54;
prefsWindow = new BrowserWindow({
width: width,
Expand Down
4 changes: 2 additions & 2 deletions src/main/lite/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,8 +167,8 @@ function openSourceListHelp(config: SourceListConfig) {
/** Opens a popup window for preferences. */
function openPreferences() {
const width = 400;
const optionRows = 7;
const titleRows = 2;
const optionRows = 9;
const titleRows = 3;
const height = optionRows * 27 + titleRows * 34 + 54;
openPopupWindow("www/preferences.html", [width, height], "pixels", (message) => {
closePopupWindow();
Expand Down
13 changes: 12 additions & 1 deletion src/preferences.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ const FIELD_3D_ANTIALIASING = document.getElementById("field3dAntialiasing") as
const TBA_API_KEY = document.getElementById("tbaApiKey") as HTMLInputElement;
const EXIT_BUTTON = document.getElementById("exit") as HTMLInputElement;
const CONFIRM_BUTTON = document.getElementById("confirm") as HTMLInputElement;
const REV_PORT = document.getElementById("revPort") as HTMLInputElement;
const REV_KEY = document.getElementById("revKey") as HTMLInputElement;

window.addEventListener("message", (event) => {
if (event.data === "port") {
Expand Down Expand Up @@ -67,6 +69,8 @@ window.addEventListener("message", (event) => {
FIELD_3D_MODE_BATTERY.value = oldPrefs.field3dModeBattery;
FIELD_3D_ANTIALIASING.value = oldPrefs.field3dAntialiasing.toString();
TBA_API_KEY.value = oldPrefs.tbaApiKey;
REV_PORT.value = oldPrefs.revTelemetryPort.toString();
REV_KEY.value = oldPrefs.revTelemetryKey;

// Close function
function close(useNewPrefs: boolean) {
Expand Down Expand Up @@ -98,6 +102,11 @@ window.addEventListener("message", (event) => {
if (FIELD_3D_MODE_BATTERY.value === "standard") field3dModeBattery = "standard";
if (FIELD_3D_MODE_BATTERY.value === "low-power") field3dModeBattery = "low-power";

let revPort = parseInt(REV_PORT.value);
if(isNaN(revPort)) {
revPort = oldPrefs.revTelemetryPort;
}

let newPrefs: Preferences = {
theme: theme,
robotAddress: ROBOT_ADDRESS.value,
Expand All @@ -117,7 +126,9 @@ window.addEventListener("message", (event) => {
skipFrcLogFolderDefault: oldPrefs.skipFrcLogFolderDefault,
skipNumericArrayDeprecationWarning: oldPrefs.skipNumericArrayDeprecationWarning,
skipFTCExperimentalWarning: oldPrefs.skipFTCExperimentalWarning,
ctreLicenseAccepted: oldPrefs.ctreLicenseAccepted
ctreLicenseAccepted: oldPrefs.ctreLicenseAccepted,
revTelemetryKey: REV_KEY.value,
revTelemetryPort: revPort,
};
messagePort.postMessage(newPrefs);
} else {
Expand Down
21 changes: 17 additions & 4 deletions src/shared/Preferences.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ export default interface Preferences {
skipFrcLogFolderDefault: boolean;
ctreLicenseAccepted: boolean;
usb?: boolean;
revTelemetryPort: number;
revTelemetryKey: string;
}

export const DEFAULT_PREFS: Preferences = {
Expand All @@ -50,10 +52,12 @@ export const DEFAULT_PREFS: Preferences = {
skipFrcLogFolderDefault: false,
skipNumericArrayDeprecationWarning: false,
skipFTCExperimentalWarning: false,
ctreLicenseAccepted: false
ctreLicenseAccepted: false,
revTelemetryPort: 8080,
revTelemetryKey: "REV",
};

export type LiveMode = "nt4" | "nt4-akit" | "phoenix" | "rlog" | "ftcdashboard";
export type LiveMode = "nt4" | "nt4-akit" | "phoenix" | "rlog" | "ftcdashboard" | "rev";

export function getLiveModeName(mode: LiveMode): string {
switch (mode) {
Expand All @@ -67,12 +71,14 @@ export function getLiveModeName(mode: LiveMode): string {
return "RLOG Server";
case "ftcdashboard":
return "FTC Dashboard";
case "rev":
return "Rev Dashboard";
}
}

// Phoenix not possible due to cross origin restrictions
// RLOG not possible because it uses raw TCP
export const LITE_ALLOWED_LIVE_MODES: LiveMode[] = ["nt4", "nt4-akit", "ftcdashboard"];
export const LITE_ALLOWED_LIVE_MODES: LiveMode[] = ["nt4", "nt4-akit", "ftcdashboard", "rev"];

export function mergePreferences(basePrefs: Preferences, newPrefs: object) {
if ("theme" in newPrefs && (newPrefs.theme === "light" || newPrefs.theme === "dark" || newPrefs.theme === "system")) {
Expand Down Expand Up @@ -102,7 +108,8 @@ export function mergePreferences(basePrefs: Preferences, newPrefs: object) {
newPrefs.liveMode === "nt4-akit" ||
newPrefs.liveMode === "phoenix" ||
newPrefs.liveMode === "rlog" ||
newPrefs.liveMode === "ftcdashboard")
newPrefs.liveMode === "ftcdashboard" ||
newPrefs.liveMode === "rev")
) {
basePrefs.liveMode = newPrefs.liveMode;
}
Expand All @@ -121,6 +128,12 @@ export function mergePreferences(basePrefs: Preferences, newPrefs: object) {
if ("rlogPort" in newPrefs && typeof newPrefs.rlogPort === "number") {
basePrefs.rlogPort = newPrefs.rlogPort;
}
if ("revTelemetryPort" in newPrefs && typeof newPrefs.revTelemetryPort === "number") {
basePrefs.revTelemetryPort = newPrefs.revTelemetryPort;
}
if ("revTelemetryKey" in newPrefs && typeof newPrefs.revTelemetryKey === "string") {
basePrefs.revTelemetryKey = newPrefs.revTelemetryKey;
}
if (
"coordinateSystem" in newPrefs &&
(newPrefs.coordinateSystem === "automatic" ||
Expand Down
18 changes: 18 additions & 0 deletions www/preferences.html
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,24 @@
<input type="text" id="publishFilter" />
</td>
</tr>
<tr>
<td class="title" colspan="2">
<hr />
REV Options
</td>
</tr>
<tr>
<td class="label">REV Port</td>
<td class="input" tabindex="-1">
<input type="number" id="revPort" />
</td>
</tr>
<tr>
<td class="label">REV Key (used to identify connection)</td>
<td class="input" tabindex="-1">
<input type="text" id="revKey" />
</td>
</tr>
<tr>
<td class="title" colspan="2">
<hr />
Expand Down