+ {
+ Object.keys(this.config.boards).map(boardName => (
+
+
+ Influx: {this.state.influxState === 0 && "Not Connected"}{this.state.influxState === 1 && this.state.influxDatabase}{this.state.influxState === 2 && "Error"}
+
+
+
+ {
+ locked && (
+
+ )
+ }
+ {
+ !locked && (
+
+ )
+ }
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+ }
+}
+
+export default withTheme(withStyles(styles)(Navbar));
diff --git a/telemetry/src/components/NineGrid.js b/telemetry/src/components/NineGrid.js
new file mode 100644
index 000000000..10125a4c0
--- /dev/null
+++ b/telemetry/src/components/NineGrid.js
@@ -0,0 +1,320 @@
+import React, { Component } from "react";
+import "@fontsource/roboto";
+import {withStyles, withTheme} from "@material-ui/core/styles";
+import {Container, Grid} from "@material-ui/core";
+
+import Graph from "./Graph";
+import SixValueSquare from "./SixValueSquare";
+
+import MessageDisplaySquare from "./MessageDisplaySquare";
+import ErrorSquare from "./ErrorSquare";
+import FourButtonSquare from "./FourButtonSquare";
+import LaunchButton from "./LaunchButton";
+import ProgressBarsSquare from "./ProgressBarsSquare";
+import RocketOrientation from "./RocketOrientation";
+import Map from "./Map";
+
+import { Responsive } from "react-grid-layout";
+import clsx from "clsx"
+import CreateSquare from "./CreateSquare";
+
+const ResponsiveGridLayout = NineGridWidthHeightProvideRGL(Responsive)
+
+const styles = (theme) => ({
+ root: {
+ flexGrow: 1,
+ height: "100vh",
+ },
+ container: {
+ flexGrow: 1,
+ position: "absolute",
+ top: theme.spacing(6),
+ bottom: "0px",
+ padding: theme.spacing(1),
+ },
+ row: {
+ height: "100%",
+ },
+ item: {
+ height: "33%",
+ }
+});
+
+class NineGrid extends Component {
+ constructor(props) {
+ super(props);
+ this.windowConfig = props.windowConfig;
+ this.config = props.config;
+
+ let slots = [];
+ for (let slot of this.windowConfig.slots) {
+ slots.push(slot);
+ }
+ for (let i = slots.length; i < 9; i ++) {
+ slots.push({});
+ }
+
+ this.state = {
+ slots: slots,
+ layout: [
+ { i: "0", x: 0, y: 0, w: 1, h: 1 },
+ { i: "1", x: 1, y: 0, w: 1, h: 1 },
+ { i: "2", x: 2, y: 0, w: 1, h: 1 },
+ { i: "3", x: 0, y: 1, w: 1, h: 1 },
+ { i: "4", x: 1, y: 1, w: 1, h: 1 },
+ { i: "5", x: 2, y: 1, w: 1, h: 1 },
+ { i: "6", x: 0, y: 2, w: 1, h: 1 },
+ { i: "7", x: 1, y: 2, w: 1, h: 1 },
+ { i: "8", x: 2, y: 2, w: 1, h: 1 }
+ ]
+ }
+
+ this.itemRefs = [];
+ for (let i = 0; i < 9; i ++) {
+ this.itemRefs[i] = React.createRef();
+ }
+
+ this.fixLayout = this.fixLayout.bind(this);
+ this.resetItem = this.resetItem.bind(this);
+ this.setSlotConfig = this.setSlotConfig.bind(this);
+ }
+
+ fixLayout(layout) {
+ const maxY = 2;
+ const maxRowXs = layout.map((item) => item.y === maxY ? item.x : null).filter((value) => value !== null);
+ const missingX = [0, 1, 2].find(value => maxRowXs.every(maxRowX => maxRowX !== value));
+ return layout.map(item => {
+ if (item.y > maxY) {
+ return {
+ ...item,
+ y: maxY,
+ x: missingX
+ }
+ }
+ return item;
+ });
+ }
+
+ resetItem(index) {
+ return () => {
+ let newSlots = [...this.state.slots];
+ newSlots[index] = {};
+ this.setState({slots: newSlots});
+ }
+ }
+
+ setSlotConfig(index) {
+ return (conf) => {
+ let newSlots = [...this.state.slots];
+ newSlots[index] = conf;
+ this.setState({slots: newSlots});
+ }
+ }
+
+ render() {
+ const { layout, slots } = this.state;
+ const { locked } = this.props;
+ return (
+
this.setState({layout: this.fixLayout(l)})}
+ isBounded={true}
+ draggableHandle=".handle"
+ >
+ {
+ slots.map((field, index) => (
+
+ {
+ (() => {
+ switch (field.type) {
+ case "logs":
+ return (
+
+ )
+ case "six-square":
+ return (
+
[
+ value.field,
+ value.name,
+ value.units,
+ null,
+ null,
+ null,
+ null,
+ value.func
+ ])
+ }
+ />
+ )
+ case "graph":
+ return (
+ ({
+ field: value.field,
+ name: value.name,
+ color: value.color,
+ unit: value.units
+ }))
+ }
+ />
+ )
+ case "four-button":
+ return (
+ [
+ value.id,
+ value.type,
+ value.name,
+ value.field,
+ value.actions,
+ value.safe || false,
+ value.green || []
+ ])
+ }
+ />
+ )
+ case "launch":
+ return (
+
+ )
+ case "progress":
+ return (
+ ({
+ field: value.field,
+ name: value.name,
+ units: value.units,
+ color: value.color,
+ minValue: value.minValue,
+ delta: value.delta
+ }))
+ }
+ />
+ )
+ case "orientation":
+ return (
+
+ )
+ case "gpsmap":
+ return (
+
+ )
+ case undefined:
+ return (
+
+ )
+ default:
+ return (
+
+ )
+ }
+ })()
+ }
+
+ ))
+ }
+
+ )
+ }
+}
+
+function NineGridWidthHeightProvideRGL(ComposedComponent) {
+ return class WidthHeightProvider extends React.Component {
+
+ static defaultProps = {
+ measureBeforeMount: false
+ };
+
+ state = {
+ width: 1280,
+ height: 720
+ };
+
+ elementRef = React.createRef();
+ mounted = false;
+
+ componentDidMount() {
+ this.mounted = true;
+ window.addEventListener("resize", this.onWindowResize);
+ this.onWindowResize();
+ }
+
+ componentWillUnmount() {
+ this.mounted = false;
+ window.removeEventListener("resize", this.onWindowResize);
+ }
+
+ onWindowResize = () => {
+ if (!this.mounted) return;
+ const node = this.elementRef.current;
+ if (node instanceof HTMLElement && node.offsetWidth && node.offsetHeight) {
+ this.setState({width: window.innerWidth, rowHeight: (window.innerHeight - node.offsetTop - 40) / 3});
+ }
+ };
+
+ render() {
+ const { measureBeforeMount, ...rest } = this.props;
+ if (measureBeforeMount && !this.mounted) {
+ return (
+
+ )
+ }
+ return (
+
+ )
+ }
+ }
+}
+
+export default withTheme(withStyles(styles)(NineGrid));
diff --git a/telemetry/src/components/ProgressBarsSquare.js b/telemetry/src/components/ProgressBarsSquare.js
new file mode 100644
index 000000000..8cc45fe17
--- /dev/null
+++ b/telemetry/src/components/ProgressBarsSquare.js
@@ -0,0 +1,145 @@
+import React, { Component } from "react";
+
+import { withStyles, withTheme } from "@material-ui/core/styles";
+import { Card, CardContent, Grid, LinearProgress, Typography, useTheme } from "@material-ui/core";
+
+import Field from "./Field";
+import comms from '../api/Comms';
+import SquareControls from "./SquareControls";
+
+const styles = (theme) => ({
+ root: {
+ height: "100%",
+ },
+ cardContent: {
+ height: "100%",
+ padding: "8px",
+ paddingBottom: "8px !important",
+ },
+ container: {
+ height: "100%",
+ },
+ item: {
+ height: "50%",
+ textAlign: "center",
+ },
+});
+
+class ProgressBarsSquare extends Component {
+ constructor(props) {
+ super(props);
+
+ this.state = {};
+
+ this.handleValueUpdate = this.handleValueUpdate.bind(this);
+ this.updateDisplay = this.updateDisplay.bind(this);
+
+ this.animationID = null;
+
+ this.fieldTextReferences = {};
+ this.fieldBarReferences = {};
+ this.fieldValueUpdateFunctions = {};
+ this.values = {};
+ this.minValues = {};
+ this.deltas = {};
+ this.names = {};
+ this.units = {};
+ for (let field of this.props.fields) {
+ this.fieldTextReferences[field.field] = React.createRef();
+ this.fieldBarReferences[field.field] = React.createRef();
+ this.fieldValueUpdateFunctions[field.field] = (timestamp, value) => {
+ this.handleValueUpdate(timestamp, value, field.field);
+ }
+ this.values[field.field] = 0.0;
+ this.minValues[field.field] = field.minValue;
+ this.deltas[field.field] = field.delta;
+ this.names[field.field] = field.name;
+ this.units[field.field] = field.units;
+
+ let stateUpdate = {};
+ stateUpdate[field.field] = field.value;
+ this.setState(stateUpdate);
+ }
+ }
+
+ handleValueUpdate(timestamp, value, field) {
+ const { modifyValue } = this.props;
+ this.values[field] = modifyValue ? modifyValue(value) : value;
+ if(this.animationID === null) {
+ this.animationID = requestAnimationFrame(() => {
+ for (let f of this.props.fields) {
+ this.updateDisplay(this.values[f.field], f.field);
+ }
+ });
+ }
+ }
+
+ updateDisplay(value, field) {
+ this.animationID = null;
+ let percentage = (value - this.minValues[field]) / this.deltas[field];
+ this.fieldTextReferences[field].current.innerHTML = this.names[field] + " - " + percentage.toFixed(2) + "% - " + this.values[field].toFixed(this.decimals) + this.units[field];
+ let stateUpdate = {};
+ stateUpdate[field] = percentage;
+ this.setState(stateUpdate);
+ // this.fieldBarReferences[field].current.value = percentage;
+ // if(this.value > this.props.threshold && this.props.threshold !== null) {
+ // this.colorRef.current.style.backgroundColor = this.props.thresholdColor;
+ // } else {
+ // this.colorRef.current.style.backgroundColor = '';
+ // }
+ }
+
+ componentDidMount() {
+ const { fields } = this.props;
+ for (let field of fields) {
+ comms.addSubscriber(field.field, this.fieldValueUpdateFunctions[field.field]);
+ }
+ }
+
+ componentWillUnmount() {
+ const { fields } = this.props;
+ for (let field of fields) {
+ comms.removeSubscriber(field.field, this.fieldValueUpdateFunctions[field.field]);
+ }
+ cancelAnimationFrame(this.animationID);
+ }
+
+ render() {
+ const { children, classes, fields } = this.props;
+ return (
+
+
+
+ {
+ fields.map(field => (
+
+
+ {field.name} - - 0.0{field.units}
+
+
+
+ ))
+ }
+ {/*
+ {fields.map((obj) => (
+
+
+
+ ))}
+ {children}
+ */}
+
+
+ );
+ }
+}
+
+export default withTheme(withStyles(styles)(ProgressBarsSquare));
diff --git a/telemetry/src/components/RocketOrientation.js b/telemetry/src/components/RocketOrientation.js
new file mode 100644
index 000000000..e86969ef1
--- /dev/null
+++ b/telemetry/src/components/RocketOrientation.js
@@ -0,0 +1,160 @@
+import React, { Component } from 'react';
+
+import * as THREE from 'three';
+import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls';
+import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
+
+import { withStyles, withTheme } from '@material-ui/core/styles';
+import Card from '@material-ui/core/Card';
+import CardContent from '@material-ui/core/CardContent';
+
+import comms from '../api/Comms';
+import SquareControls from './SquareControls';
+
+const styles = theme => ({
+ root: {
+ height: '100%'
+ },
+ cardContent: {
+ padding: 0,
+ paddingBottom: "0 !important",
+ height: '100%',
+ },
+ button: {
+ float: 'right',
+ minWidth: '0px',
+ zIndex: 1000
+ },
+ canvas: {
+ position: 'absolute',
+ width: '100%',
+ height: '100%'
+ },
+ sizeDetector: {
+ position: 'relative',
+ width: '100%',
+ height: '100%'
+ },
+ legend: {}
+});
+
+class RocketOrientation extends Component {
+ constructor(props) {
+ super(props);
+ this.state = {};
+ this.sizeDetector = React.createRef();
+ this.lad4gltf = null
+
+ this.qW = 0.0;
+ this.qX = 0.0;
+ this.qY = 0.0;
+ this.qZ = 0.0;
+
+ this.handleValueUpdate = this.handleValueUpdate.bind(this)
+ this.handleqWUpdate = this.handleqWUpdate.bind(this)
+ this.handleqXUpdate = this.handleqXUpdate.bind(this)
+ this.handleqYUpdate = this.handleqYUpdate.bind(this)
+ }
+
+ componentDidMount() {
+ const { fieldQW, fieldQX, fieldQY, fieldQZ } = this.props;
+
+ const width = this.sizeDetector.current.clientWidth;
+ const height = this.sizeDetector.current.clientHeight;
+ let scene = new THREE.Scene();
+ let camera = new THREE.PerspectiveCamera(50, width / height, 0.1, 1000);
+ let renderer = new THREE.WebGLRenderer({ alpha: true });
+ let controls = new OrbitControls(camera, renderer.domElement);
+ renderer.setSize(width, height);
+ renderer.setPixelRatio(window.devicePixelRatio);
+ renderer.setClearColor(0x000000, 0.9);
+ // document.body.appendChild( renderer.domElement );
+ // use ref as a mount point of the Three.js scene instead of the document.body
+ this.sizeDetector.current.appendChild(renderer.domElement);
+ const axesHelper = new THREE.AxesHelper(2);
+ scene.add(axesHelper);
+ var ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
+ scene.add(ambientLight);
+ const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
+ scene.add(directionalLight);
+
+ const loader = new GLTFLoader();
+ const that = this
+ loader.load('static/3d-models/LAD4.gltf', function (gltf) {
+ that.lad4gltf = gltf;
+ scene.add(gltf.scene);
+ gltf.scene.setRotationFromAxisAngle(new THREE.Vector3(0,1,0), Math.PI / 2)
+ }, undefined, function (error) {
+ console.error(error);
+ });
+
+ camera.position.x = 3;
+ camera.position.y = 3;
+ camera.position.z = 3;
+ camera.lookAt(0, 0, 0);
+ controls.update();
+
+ let animate = () => {
+ this.animationHandle = requestAnimationFrame(animate);
+
+ controls.update();
+
+ directionalLight.position.x = camera.position.x;
+ directionalLight.position.y = camera.position.y;
+ directionalLight.position.z = camera.position.z;
+
+ renderer.render(scene, camera);
+ };
+ animate();
+
+ comms.addSubscriber(fieldQW, this.handleqWUpdate);
+ comms.addSubscriber(fieldQX, this.handleqXUpdate);
+ comms.addSubscriber(fieldQY, this.handleqYUpdate);
+ comms.addSubscriber(fieldQZ, this.handleValueUpdate); // actuall update the model once QZ received
+ }
+
+ handleqWUpdate(timestamp, data) {this.qW = data}
+ handleqXUpdate(timestamp, data) {this.qX = data}
+ handleqYUpdate(timestamp, data) {this.qY = data}
+
+ handleValueUpdate(timestamp, data) {
+ this.qZ = data;
+ if (!this.lad4gltf) return
+
+ let quat = new THREE.Quaternion(this.qW, this.qX, this.qY, this.qZ);
+ // const rot1 = new THREE.Quaternion(-Math.sqrt(2) / 2, 0, 0, Math.sqrt(2) / 2);
+ // const rot2 = new THREE.Quaternion(0, 0, Math.sqrt(2) / 2, Math.sqrt(2) / 2);
+ // quat = quat.premultiply(new THREE.Quaternion(0, Math.sqrt(2)/2, 0, Math.sqrt(2)/2));
+ // quat = quat.multiply((new THREE.Quaternion(0, Math.sqrt(2)/2, 0, Math.sqrt(2)/2)).invert());
+ // quat = quat.premultiply(rot1);
+ // quat = quat.multiply(rot1.invert());
+ // quat = quat.premultiply(rot2);
+ // quat.multiply(rot2.invert());
+ this.lad4gltf.scene.setRotationFromQuaternion(quat);
+ // lad4gltf.scene.setRotationFromEuler(new THREE.Qua(Math.PI * data[0] / 180, Math.PI * data[1] / 180, Math.PI * data[2] / 180, "YXZ"));
+ }
+
+ componentWillUnmount() {
+ const { fieldQW, fieldQX, fieldQY, fieldQZ } = this.props;
+ comms.removeSubscriber(fieldQW, this.handleqWUpdate);
+ comms.removeSubscriber(fieldQX, this.handleqXUpdate);
+ comms.removeSubscriber(fieldQY, this.handleqYUpdate);
+ comms.removeSubscriber(fieldQZ, this.handleValueUpdate);
+ cancelAnimationFrame(this.animationHandle);
+ }
+
+ render() {
+ const { classes } = this.props;
+ return (
+
+
+
+
+
+
+
+ );
+ }
+}
+
+export default withTheme(withStyles(styles)(RocketOrientation));
\ No newline at end of file
diff --git a/telemetry/src/components/Settings.js b/telemetry/src/components/Settings.js
new file mode 100644
index 000000000..7cd8a3a76
--- /dev/null
+++ b/telemetry/src/components/Settings.js
@@ -0,0 +1,179 @@
+import React, { Component } from 'react';
+import { withTheme, withStyles } from '@material-ui/core/styles';
+
+import Dialog from '@material-ui/core/Dialog';
+import DialogTitle from '@material-ui/core/DialogTitle';
+import { DialogContent, DialogActions, Button, Typography, TextField, Select, MenuItem } from '@material-ui/core';
+import Comms from '../api/Comms';
+
+const styles = theme => ({
+ head: {
+ // display: 'inline',
+ // borderBottom: '0.5px solid gray'
+ },
+ fields: {
+ marginTop: '1rem',
+ marginRight: '1rem',
+ width: '18ch',
+ },
+ connectButton: {
+ marginTop: theme.spacing(2)
+ },
+ connectedButton: {
+ marginTop: theme.spacing(2),
+ backgroundColor: theme.palette.success.main + ' !important',
+ color: theme.palette.text.primary + ' !important'
+ },
+});
+
+class Settings extends Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ influxHost: '127.0.0.1',
+ influxPort: 8086,
+ influxProtocol: 'http',
+ influxUsername: '',
+ influxPassword: '',
+ influxConnecting: false,
+ influxDatabase: '',
+ influxDatabaseList: [],
+ };
+
+ this.updateInfluxHost = this.updateInfluxHost.bind(this);
+ this.updateInfluxPort = this.updateInfluxPort.bind(this);
+ this.updateInfluxProtocol = this.updateInfluxProtocol.bind(this);
+ this.updateInfluxUsername = this.updateInfluxUsername.bind(this);
+ this.updateInfluxPassword = this.updateInfluxPassword.bind(this);
+ this.updateInfluxDatabase = this.updateInfluxDatabase.bind(this);
+
+ this.connectToInflux = this.connectToInflux.bind(this);
+ this.setInfluxDatabase = this.setInfluxDatabase.bind(this);
+ }
+
+ updateInfluxHost(e) { this.setState({ influxHost: e.target.value }); }
+ updateInfluxPort(e) { this.setState({ influxPort: parseInt(e.target.value) }); }
+ updateInfluxProtocol(e) { this.setState({ influxProtocol: e.target.value }); }
+ updateInfluxUsername(e) { this.setState({ influxUsername: e.target.value }); }
+ updateInfluxPassword(e) { this.setState({ influxPassword: e.target.value }); }
+ updateInfluxDatabase(e) { this.setState({ influxDatabase: e.target.value }); }
+
+ async connectToInflux() {
+ this.setState({ influxConnecting: true });
+ const { influxHost,
+ influxPort,
+ influxProtocol,
+ influxUsername,
+ influxPassword } = this.state;
+ await Comms.connectInflux(influxHost,
+ influxPort,
+ influxProtocol,
+ influxUsername,
+ influxPassword);
+ const databases = (await Comms.getDatabases()).splice(1).sort().reverse();
+ this.setState({ influxDatabase: databases[0], influxDatabaseList: databases, influxConnecting: false });
+ }
+
+ async setInfluxDatabase() {
+ const { influxDatabase } = this.state;
+ await Comms.setDatabase(influxDatabase);
+ // Initialize Procedures. Done here since selecting databse is last step before data is recorded
+ // TODO: Maybe move this somewhere else, or rename function
+ await Comms.setProcedureState(0);
+ }
+
+ componentDidMount() {
+ }
+
+ componentWillUnmount() {
+ }
+
+ render() {
+ const { classes, open, closeSettings } = this.props;
+ const { influxHost,
+ influxPort,
+ influxProtocol,
+ influxUsername,
+ influxPassword,
+ influxConnecting,
+ influxDatabase,
+ influxDatabaseList } = this.state;
+ const influxConnected = influxDatabaseList.length > 0;
+ return (
+
+ );
+ }
+}
+
+export default withTheme(withStyles(styles)(Settings));
diff --git a/telemetry/src/components/SixValueSquare.js b/telemetry/src/components/SixValueSquare.js
new file mode 100644
index 000000000..8598a93d0
--- /dev/null
+++ b/telemetry/src/components/SixValueSquare.js
@@ -0,0 +1,57 @@
+import React, { Component } from "react";
+
+import { withStyles, withTheme } from "@material-ui/core/styles";
+import { Card, CardContent, Grid } from "@material-ui/core";
+
+import Field from "./Field";
+import SquareControls from "./SquareControls";
+
+const styles = (theme) => ({
+ root: {
+ height: "100%",
+ },
+ cardContent: {
+ height: "100%",
+ padding: "8px",
+ paddingBottom: "8px !important",
+ },
+ container: {
+ height: "100%",
+ },
+ item: {
+ height: "50%",
+ textAlign: "center",
+ },
+});
+
+class SixValueSquare extends Component {
+
+ render() {
+ const { children, classes, fields } = this.props;
+ return (
+
+
+
+
+ {fields.map((obj) => (
+
+
+
+ ))}
+ {children}
+
+
+
+ );
+ }
+}
+
+export default withTheme(withStyles(styles)(SixValueSquare));
diff --git a/telemetry/src/components/SquareControls.js b/telemetry/src/components/SquareControls.js
new file mode 100644
index 000000000..c66fb99ae
--- /dev/null
+++ b/telemetry/src/components/SquareControls.js
@@ -0,0 +1,24 @@
+import React, { Component } from "react";
+
+import { withTheme } from "@material-ui/core/styles";
+
+import OpenWithIcon from "@material-ui/icons/OpenWith";
+import CloseIcon from "@material-ui/icons/Close";
+
+class SquareControls extends Component {
+
+ render() {
+ const { reset, locked } = this.props;
+ if (!locked) {
+ return (
+
+
+
+
+ );
+ }
+ return (null);
+ }
+}
+
+export default withTheme(SquareControls);
diff --git a/telemetry/src/config/textbox-display-config.js b/telemetry/src/config/textbox-display-config.js
new file mode 100644
index 000000000..5348d89a7
--- /dev/null
+++ b/telemetry/src/config/textbox-display-config.js
@@ -0,0 +1,53 @@
+/**
+ * Applies to every node
+ * @type {[{ignoredIf: (function(*): boolean), name: string, key: string}]}
+ */
+export const GENERIC_FILTERS = [
+ {
+ name: 'Number Updates',
+ key: 't2-number-updates',
+ ignoredIf: (node) => (typeof node._val === 'number') // if node value is a number, probably is a value update
+ },
+]
+
+export const ROOT_OPTION_GROUPING = {
+ name: 'Select All',
+ key: 't1-all',
+ children: [
+ {
+ name: 'Control Updates',
+ key: 't2-controls',
+ children: [
+ { key: 'abort', highlight: 'rgba(255,0,0,0.4)' },
+ { key: 'hold' },
+ { key: 'send-custom-message', name: 'Send Custom Message'},
+ { key: 'fcEvent', name: 'Receive Custom Message', highlight: "rgba(183,255,150,0.21)"}
+ ]
+ },
+ {
+ name: 'Connection Status',
+ key: 't2-connection-status',
+ highlight: 'rgba(255,243,0,0.4)',
+ children: [
+ { key: 'flightConnected' },
+ { key: 'groundConnected' },
+ { key: 'daq1Connected' },
+ // { key: 'daq2Connected' },
+ { key: 'actCtrlr1Connected' },
+ // { key: 'actCtrlr2Connected' },
+ ]
+ },
+ {
+ name: 'Unknown (Catch All) Updates',
+ key: 't2-unknowns',
+ highlight: 'rgba(255,72,0,0.21)',
+ },
+ {
+ name: 'Spacers',
+ key: 't2-spacers',
+ children: [
+ { key: 'filler' }
+ ]
+ }
+ ]
+}
diff --git a/telemetry/src/fonts/JetBrainsMono-Regular.ttf b/telemetry/src/fonts/JetBrainsMono-Regular.ttf
new file mode 100644
index 000000000..8da8aa405
Binary files /dev/null and b/telemetry/src/fonts/JetBrainsMono-Regular.ttf differ
diff --git a/telemetry/src/fonts/JetBrainsMono-Regular.woff2 b/telemetry/src/fonts/JetBrainsMono-Regular.woff2
new file mode 100644
index 000000000..8c862e334
Binary files /dev/null and b/telemetry/src/fonts/JetBrainsMono-Regular.woff2 differ
diff --git a/telemetry/src/index.css b/telemetry/src/index.css
new file mode 100644
index 000000000..030fcc6f4
--- /dev/null
+++ b/telemetry/src/index.css
@@ -0,0 +1,19 @@
+@font-face {
+ font-family: 'JetBrains Mono';
+ src: url('./fonts/JetBrainsMono-Regular.woff2') format('woff2'),
+ url('./fonts/JetBrainsMono-Regular.ttf') format('ttf')
+}
+
+body {
+ margin: 0;
+ font-family: "JetBrains Mono", -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
+ 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
+ sans-serif;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ overflow: hidden;
+}
+
+#webpack-dev-server-client-overlay {
+ display: none;
+}
\ No newline at end of file
diff --git a/telemetry/src/index.js b/telemetry/src/index.js
new file mode 100644
index 000000000..f625fcd57
--- /dev/null
+++ b/telemetry/src/index.js
@@ -0,0 +1,127 @@
+import React, { Component } from "react";
+import { BrowserRouter } from "react-router-dom";
+import ReactDOM from "react-dom";
+import "./index.css";
+import { ThemeProvider, createTheme, CssBaseline } from "@material-ui/core";
+import Settings from "./components/Settings";
+import Navbar from "./components/Navbar";
+import comms from "./api/Comms";
+import LayoutSwitch from "./components/LayoutSwitch";
+
+class App extends Component {
+ constructor() {
+ super();
+ this.state = {
+ locked: true,
+ isDark: false,
+ showSettings: false
+ };
+
+ this.toggleLocked = this.toggleLocked.bind(this);
+ this.changeLightDark = this.changeLightDark.bind(this);
+ this.openSettings = this.openSettings.bind(this);
+ this.closeSettings = this.closeSettings.bind(this);
+ }
+
+ toggleLocked() {
+ this.setState({ locked: !this.state.locked })
+ }
+
+ changeLightDark() {
+ // comms.setDarkMode(!this.state.isDark);
+ this.setState({ isDark: !this.state.isDark });
+ }
+ openSettings() {
+ this.setState({ showSettings: true });
+ }
+
+ closeSettings() {
+ this.setState({ showSettings: false });
+ }
+
+ componentDidMount() {
+ let config = JSON.parse(atob(window.location.hash.split("&")[1]));
+ document.title = `Telemetry: ${config.windows[window.location.hash.split("&")[0].substring(2)].name}`;
+ comms.connect();
+ }
+
+ componentWillUnmount() {
+ comms.destroy();
+ }
+
+ render() {
+ const theme = createTheme({
+ palette: {
+ type: this.state.isDark ? "dark" : "light",
+ primary: {
+ main: "#43a047",
+ // darker: "#388e3c",
+ contrastText: "#fff",
+ },
+ success: {
+ main: "#43a047",
+ // darker: "#388e3c",
+ contrastText: "#fff",
+ },
+ error: {
+ main: "#d32f2f",
+ contrastText: "#fff",
+ },
+ secondary: {
+ main: "#1976d2",
+ // darker: "#115293",
+ contrastText: "#fff",
+ },
+ neutral: {
+ main: "#64748B",
+ contrastText: "#fff",
+ },
+ enabled: {
+ main: "#43a047",
+ // darker: "#388e3c",
+ contrastText: "#fff",
+ },
+ disabled: {
+ main: "#d32f2f",
+ contrastText: "#fff",
+ },
+ lox: {
+ main: "#0288d1",
+ // darker: "#388e3c",
+ contrastText: "#fff",
+ },
+ fuel: {
+ main: "#9c27b0",
+ contrastText: "#fff",
+ },
+ },
+ });
+
+ return (
+
+
+
+
+
+
+
+
+ );
+ }
+}
+
+ReactDOM.render(
+
+
+ ,
+ document.getElementById("root")
+);
diff --git a/telemetry/src/media/0.wav b/telemetry/src/media/0.wav
new file mode 100644
index 000000000..5b0860436
Binary files /dev/null and b/telemetry/src/media/0.wav differ
diff --git a/telemetry/src/media/1.wav b/telemetry/src/media/1.wav
new file mode 100644
index 000000000..ce191ea0e
Binary files /dev/null and b/telemetry/src/media/1.wav differ
diff --git a/telemetry/src/media/10.wav b/telemetry/src/media/10.wav
new file mode 100644
index 000000000..ed851573f
Binary files /dev/null and b/telemetry/src/media/10.wav differ
diff --git a/telemetry/src/media/2.wav b/telemetry/src/media/2.wav
new file mode 100644
index 000000000..161048c37
Binary files /dev/null and b/telemetry/src/media/2.wav differ
diff --git a/telemetry/src/media/3.wav b/telemetry/src/media/3.wav
new file mode 100644
index 000000000..955d42049
Binary files /dev/null and b/telemetry/src/media/3.wav differ
diff --git a/telemetry/src/media/4.wav b/telemetry/src/media/4.wav
new file mode 100644
index 000000000..f42ef0a72
Binary files /dev/null and b/telemetry/src/media/4.wav differ
diff --git a/telemetry/src/media/5.wav b/telemetry/src/media/5.wav
new file mode 100644
index 000000000..1106b6057
Binary files /dev/null and b/telemetry/src/media/5.wav differ
diff --git a/telemetry/src/media/6.wav b/telemetry/src/media/6.wav
new file mode 100644
index 000000000..2820a4672
Binary files /dev/null and b/telemetry/src/media/6.wav differ
diff --git a/telemetry/src/media/7.wav b/telemetry/src/media/7.wav
new file mode 100644
index 000000000..bf805803e
Binary files /dev/null and b/telemetry/src/media/7.wav differ
diff --git a/telemetry/src/media/8.wav b/telemetry/src/media/8.wav
new file mode 100644
index 000000000..896330a21
Binary files /dev/null and b/telemetry/src/media/8.wav differ
diff --git a/telemetry/src/media/9.wav b/telemetry/src/media/9.wav
new file mode 100644
index 000000000..e792b50ca
Binary files /dev/null and b/telemetry/src/media/9.wav differ
diff --git a/telemetry/src/media/gas1.wav b/telemetry/src/media/gas1.wav
new file mode 100644
index 000000000..9a6f129fd
Binary files /dev/null and b/telemetry/src/media/gas1.wav differ
diff --git a/telemetry/src/media/gas2.wav b/telemetry/src/media/gas2.wav
new file mode 100644
index 000000000..f149bc47b
Binary files /dev/null and b/telemetry/src/media/gas2.wav differ
diff --git a/telemetry/src/media/gas3.wav b/telemetry/src/media/gas3.wav
new file mode 100644
index 000000000..08b6c8651
Binary files /dev/null and b/telemetry/src/media/gas3.wav differ
diff --git a/telemetry/src/media/updog.wav b/telemetry/src/media/updog.wav
new file mode 100644
index 000000000..e7d45d583
Binary files /dev/null and b/telemetry/src/media/updog.wav differ
diff --git a/telemetry/src/util.js b/telemetry/src/util.js
new file mode 100644
index 000000000..65bc746f3
--- /dev/null
+++ b/telemetry/src/util.js
@@ -0,0 +1,64 @@
+import comms from "./api/Comms";
+
+export function buttonAction(action) {
+ return (...args) => {
+ switch (action.type) {
+ case "retract-full":
+ comms.sendPacket(action.board, action.packet, action.number == null ? -1 : action.number, 0, 0);
+ break;
+ case "extend-full":
+ comms.sendPacket(action.board, action.packet, action.number == null ? -1 : action.number, 1, 0);
+ break;
+ case "retract-timed":
+ comms.sendPacket(action.board, action.packet, action.number == null ? -1 : action.number, 2, args[0]);
+ break;
+ case "extend-timed":
+ comms.sendPacket(action.board, action.packet, action.number == null ? -1 : action.number, 3, args[0]);
+ break;
+ case "on":
+ comms.sendPacket(action.board, action.packet, action.number == null ? -1 : action.number, 4, 0);
+ break;
+ case "off":
+ comms.sendPacket(action.board, action.packet, action.number == null ? -1 : action.number, 5, 0);
+ break;
+ case "enable":
+ let enableButton = buttonEnabledManager[action.id];
+ if (enableButton !== undefined) {
+ enableButton(true);
+ }
+ break;
+ case "disable":
+ let disableButton = buttonEnabledManager[action.id];
+ if (disableButton !== undefined) {
+ disableButton(false);
+ }
+ break;
+ case "signal":
+ comms.sendSignalPacket(action.board, action.packet);
+ break;
+ case "signal-timed":
+ comms.sendSignalPacketTimed(action.board, action.packet, args[0]);
+ break;
+ case "start-pings":
+ setInterval(() => {
+ comms.sendSignalPacket(action.board, action.packet);
+ }, action.delay);
+ break;
+ case "zero":
+ comms.sendZeroPacket(action.board, action.packet, args[0]);
+ break;
+ default:
+ return;
+ }
+ }
+}
+
+export const buttonEnabledManager = {};
+
+export function addButtonEnabledListener(name, callback) {
+ buttonEnabledManager[name] = callback;
+}
+
+export function removeButtonEnabledListener(name) {
+ delete buttonEnabledManager[name];
+}
\ No newline at end of file
diff --git a/test/PacketTest.js b/test/PacketTest.js
new file mode 100644
index 000000000..8fe4c4733
--- /dev/null
+++ b/test/PacketTest.js
@@ -0,0 +1,52 @@
+const assert = require('assert');
+
+const Packet = require('../electron/Packet');
+
+describe('Packet', () => {
+ describe('#parsePacket()', () => {
+ it('should correctly parse basic packet', () => {
+ const packet = Packet.parsePacket('{1,2.00|e71e}');
+ assert.ok(packet, `parsePacket returned null`);
+ assert.strictEqual(packet.id, 1, `packet id doesn't match expected`);
+ assert.strictEqual(packet.length, 1, `packet length doesn't match expected`);
+ assert.strictEqual(packet.values[0], 2.00, `packet values don't match expected`);
+ });
+ it('should correctly parse packet with newlines', () => {
+ const packet = Packet.parsePacket('{1,2.00|e71e}\n');
+ assert.ok(packet, `parsePacket returned null`);
+ assert.strictEqual(packet.id, 1, `packet id doesn't match expected`);
+ assert.strictEqual(packet.length, 1, `packet length doesn't match expected`);
+ assert.strictEqual(packet.values[0], 2.00, `packet values don't match expected`);
+ });
+ it('should correctly parse packet with newlines and carriage returns', () => {
+ const packet = Packet.parsePacket('{1,2.00|e71e}\r\n');
+ assert.ok(packet, `parsePacket returned null`);
+ assert.strictEqual(packet.id, 1, `packet id doesn't match expected`);
+ assert.strictEqual(packet.length, 1, `packet length doesn't match expected`);
+ assert.strictEqual(packet.values[0], 2.00, `packet values don't match expected`);
+ });
+ it('should correctly parse packet with spaces at the front', () => {
+ const packet = Packet.parsePacket(' {1,2.00|e71e} ');
+ assert.ok(packet, `parsePacket returned null`);
+ assert.strictEqual(packet.id, 1, `packet id doesn't match expected`);
+ assert.strictEqual(packet.length, 1, `packet length doesn't match expected`);
+ assert.strictEqual(packet.values[0], 2.00, `packet values don't match expected`);
+ });
+ it('should fail to parse packet with incorrect checksum', () => {
+ const packet = Packet.parsePacket('{1,3.00|e71e}');
+ assert.ok(!packet, `parsePacket returned something`);
+ });
+ it('should fail to parse packet with incorrect checksum delimiter', () => {
+ const packet = Packet.parsePacket('{1,3.00e71e}');
+ assert.ok(!packet, `parsePacket returned something`);
+ });
+ });
+
+ describe('#stringify()', () => {
+ it('should correctly stringify packet', () => {
+ const packet = new Packet(1, [2.00]);
+ const pktString = packet.stringify();
+ assert.strictEqual(pktString, '{1,2.00|e71e}', `packet string generated is incorrectly formatted`);
+ });
+ });
+});
diff --git a/tools/DevPacket.js b/tools/DevPacket.js
new file mode 100644
index 000000000..8b9ddac3e
--- /dev/null
+++ b/tools/DevPacket.js
@@ -0,0 +1,66 @@
+const { INBOUND_PACKET_DEFS } = require("../electron/packetDefs");
+const Interpolation = require("../electron/Interpolation");
+const { asASCIIString, asFloat, asUInt8, asUInt16, asUInt32 } = Interpolation
+const Packet = require("../electron/Packet")
+
+class DevPacket extends Packet{
+ /**
+ *
+ * @param {Number} id
+ * @param {Array.
} values
+ * @param {Number|null} [timestamp]
+ */
+ constructor(id, values, timestamp) {
+ super(id, values, timestamp);
+ }
+
+ toBuffer() {
+ const packetDef = INBOUND_PACKET_DEFS[this.id]
+ if (!packetDef) {
+ console.debug(`[${this.id}] Packet ID is not defined in the INBOUND_PACKET_DEFS.`)
+ return
+ }
+
+ /**
+ * @type {Array.}
+ */
+ const dataBufArr = this.values.map((value, idx) => {
+ switch (packetDef[idx][1]) {
+ case asFloat: {
+ const _buf = Buffer.alloc(4)
+ _buf.writeFloatLE(value)
+ return _buf
+ }
+ case asUInt8: {
+ const _buf = Buffer.alloc(1)
+ _buf.writeUInt8(value)
+ return _buf
+ }
+ case asUInt16: {
+ const _buf = Buffer.alloc(2)
+ _buf.writeUInt16LE(value)
+ return _buf
+ }
+ case asUInt32: {
+ const _buf = Buffer.alloc(4)
+ _buf.writeUInt32LE(value)
+ return _buf
+ }
+ }
+ })
+
+ const idBuf = Buffer.alloc(1)
+ idBuf.writeUInt8(this.id)
+ const lenBuf = Buffer.alloc(1)
+ lenBuf.writeUInt8(dataBufArr.reduce((acc, cur) => acc + cur.length, 0))
+ const tsOffsetBuf = Buffer.alloc(4)
+ tsOffsetBuf.writeUInt32LE(Date.now() - DevPacket.initTime)
+
+ const checksumBuf = Buffer.alloc(2)
+ checksumBuf.writeUInt16LE(DevPacket.fletcher16Partitioned([idBuf, lenBuf, tsOffsetBuf, ...dataBufArr]))
+
+ return Buffer.concat([idBuf, lenBuf, tsOffsetBuf, checksumBuf, ...dataBufArr])
+ }
+}
+
+module.exports = DevPacket;
diff --git a/tools/Packet.js b/tools/Packet.js
new file mode 100644
index 000000000..676de8ab0
--- /dev/null
+++ b/tools/Packet.js
@@ -0,0 +1,156 @@
+const { OUTBOUND_PACKET_DEFS } = require("./packetDefs");
+const Interpolation = require("./Interpolation");
+const { FLOAT, UINT8, UINT32, UINT16 } = Interpolation.TYPES
+
+class Packet {
+ /**
+ *
+ * @param {Number} id
+ * @param {Array.} values
+ * @param {Number|null} [timestamp]
+ */
+ constructor(id=0, values=[], timestamp=Date.now()-Packet.startupTime) {
+ this.id = id;
+ this.values = values;
+ this.timestamp = timestamp;
+ }
+
+ static startupTime = Date.now();
+
+ /**
+ * Generates a string representation of the packet that can be transmitted
+ * @returns a string representation of the packet
+ */
+ stringify() {
+ return `{${this.id}|${this.values.map(v => {
+ if(v[1] === 'f') {
+ // value is intended as a float
+ return v[0].toFixed(2) + 'f';
+ } else if(v[1] === 'x') {
+ // value is intended as hexademical
+ return '0x' + v[0].toString(16);
+ } else if(v[1] === 'u8') {
+ return v[0].toString() + 'u8';
+ } else if(v[1] === 'u16') {
+ return v[0].toString() + 'u16';
+ } else if(v[1] === 'u32') {
+ return v[0].toString() + 'u32';
+ } else {
+ // invalid value
+ }
+ }).join(',')}}`;
+ }
+
+ toBuffer() {
+ /**
+ * @type {Array.}
+ */
+ const dataBufArr = this.values.map(v => {
+ if(v[1] === 'f') {
+ // value is intended as a float
+ const tmp = Buffer.alloc(4)
+ tmp.writeFloatLE(v[0])
+ return tmp
+ } else if(v[1] === 'x') {
+ // value is intended as hexademical
+ const tmp = Buffer.alloc(1)
+ tmp.writeUint8(v[0])
+ return tmp
+ } else if(v[1] === 'u8') {
+ const tmp = Buffer.alloc(1)
+ tmp.writeUInt8(v[0])
+ return tmp
+ } else if(v[1] === 'u16') {
+ const tmp = Buffer.alloc(2)
+ tmp.writeUInt16LE(v[0])
+ return tmp
+ } else if(v[1] === 'u32') {
+ const tmp = Buffer.alloc(4)
+ tmp.writeUInt32LE(v[0])
+ return tmp
+ } else {
+ // invalid value
+ }
+ })
+
+ const idBuf = Buffer.alloc(1)
+ idBuf.writeUInt8(this.id)
+ const lenBuf = Buffer.alloc(1)
+ lenBuf.writeUInt8(dataBufArr.reduce((acc, cur) => acc + cur.length, 0))
+ const tsOffsetBuf = Buffer.alloc(4)
+ tsOffsetBuf.writeUInt32LE(this.timestamp)
+
+ const checksumBuf = Buffer.alloc(2)
+ checksumBuf.writeUInt16LE(Packet.fletcher16Partitioned([idBuf, lenBuf, tsOffsetBuf, ...dataBufArr]))
+
+ return Buffer.concat([idBuf, lenBuf, tsOffsetBuf, checksumBuf, ...dataBufArr])
+ }
+
+ static createPacketFromText(text) {
+ const idSplit = text.split('|');
+ const id = parseInt(idSplit[0].substring(1));
+ const values = idSplit[1].split(',');
+ if(values.length > 0) {
+ values[values.length-1] = values[values.length-1].split('}')[0]
+ }
+
+ for(let i in values) {
+ const oldValue = values[i]
+ if(oldValue[oldValue.length - 1] === 'f') {
+ // value is intended as a float
+ values[i] = [parseFloat(oldValue.substring(0, oldValue.length - 1)), 'f']
+ } else if(oldValue.substring(0, 2) === '0x') {
+ // value is intended as hexademical
+ values[i] = [parseInt(oldValue.substring(2), 16), 'x']
+ } else if(oldValue.substring(oldValue.length - 2) === 'u8') {
+ values[i] = [parseInt(oldValue.substring(0, oldValue.length-2)), 'u8']
+ } else if(oldValue.substring(oldValue.length - 3) === 'u16') {
+ values[i] = [parseInt(oldValue.substring(0, oldValue.length-3)), 'u16']
+ } else if(oldValue.substring(oldValue.length - 3) === 'u32') {
+ values[i] = [parseInt(oldValue.substring(0, oldValue.length-3)), 'u32']
+ } else {
+ // invalid value
+ }
+ }
+ const pkt = new Packet(id, values);
+ return pkt
+ }
+
+ /**
+ * Calculates the fletcher16 checksum for some partitioned data.
+ *
+ * See https://en.wikipedia.org/wiki/Fletcher%27s_checksum
+ * @param {Buffer[]} bufArr the data to checksum
+ * @returns integer checksum
+ */
+ static fletcher16Partitioned(bufArr) {
+ let a = 0, b = 0;
+ for (const buf of bufArr) {
+ for (let i = 0; i < buf.length; i++) {
+ a = (a + buf[i]) % 256;
+ b = (b + a) % 256;
+ }
+ }
+ return a | (b << 8);
+ }
+
+ /**
+ * Calculates the fletcher16 checksum for some data.
+ *
+ * See https://en.wikipedia.org/wiki/Fletcher%27s_checksum
+ * @param {Buffer} data the data to checksum
+ * @returns integer checksum
+ */
+ static fletcher16(data) {
+ let a = 0, b = 0;
+ for (let i = 0; i < data.length; i++) {
+ a = (a + data[i]) % 256;
+ b = (b + a) % 256;
+ }
+ return a | (b << 8);
+ }
+
+ static initTime = Date.now()
+}
+
+module.exports = Packet;
diff --git a/tools/influx_exporter.js b/tools/influx_exporter.js
new file mode 100644
index 000000000..7b9f579fa
--- /dev/null
+++ b/tools/influx_exporter.js
@@ -0,0 +1,65 @@
+const Influx = require('influx');
+
+const influxLocal = new Influx.InfluxDB({
+ host: '127.0.0.1',
+ port: 8086,
+ protocol: 'http',
+ requestTimeout: 20000,
+ failoverTimeout: 40000,
+});
+
+const influxRemote = new Influx.InfluxDB({
+ host: '127.0.0.1',
+ port: 8086,
+ protocol: 'http',
+ requestTimeout: 20000,
+ failoverTimeout: 40000,
+});
+
+const localDatabaseName = 'justin_is_a_poop_head';
+const remoteDatabaseName = '2021_10_31_capfill';
+
+async function uploadMeasurement(m) {
+ console.log('transferring measurement ' + m);
+ let lastTime = 0;
+ while(true) {
+ console.log('selecting ' + m);
+ const query = await influxLocal.queryRaw(`select value from "${m}" where time > ${lastTime}000000 order by time limit 10000`, {
+ database: localDatabaseName,
+ precision: 'ms'
+ });
+ const values = query.results[0].series[0].values;
+ lastTime = values[values.length-1][0];
+ console.log('transforming ' + m + ' of length: ' + values.length);
+ let transformed = [];
+ for(let i of values) {
+ transformed.push({
+ fields: {
+ value: i[1]
+ },
+ timestamp: i[0],
+ });
+ }
+ console.log('writing ' + m);
+ await influxRemote.writeMeasurement(m, transformed, {
+ database: remoteDatabaseName,
+ precision: 'ms'
+ });
+ if(values.length < 10000) {
+ break;
+ }
+ }
+ console.log('done writing ' + m);
+ console.log('-----------------------');
+}
+
+(async () => {
+ const measurements = await influxLocal.getMeasurements(localDatabaseName);
+ const modMeas = measurements.slice(measurements.indexOf("syslog")+1);
+ for(let m of modMeas) {
+ await uploadMeasurement(m);
+ }
+})();
+
+
+
diff --git a/tools/packet_cli.js b/tools/packet_cli.js
new file mode 100644
index 000000000..7f4f863fb
--- /dev/null
+++ b/tools/packet_cli.js
@@ -0,0 +1,45 @@
+const dgram = require('dgram');
+
+const server = dgram.createSocket('udp4');
+
+const rl = require('readline').createInterface({
+ input: process.stdin,
+ output: process.stdout
+});
+
+// address of board to send to
+const BOARD_ADDRESS = "10.0.0.12";
+const BOARD_PORT = 42069;
+
+// checksum generator
+fletcher16 = (data) => {
+ var a = 0, b = 0;
+ for (var i = 0; i < data.length; i++) {
+ a = (a + data[i]) % 255;
+ b = (b + a) % 255;
+ }
+ return a | (b << 8);
+}
+
+server.on('error', (err) => {
+ console.log(`server error:\n${err.stack}`);
+ server.close();
+});
+server.on('message', (msg, rinfo) => {
+ console.log(msg.toString());
+});
+server.on('listening', () => {
+ const address = server.address();
+ console.log(`server listening ${address.address}:${address.port}`);
+ if(process.platform === 'win32') {
+ server.send("big yeet", 42069, "10.0.0.42");
+ }
+ rl.on('line', line => {
+ const checksum = fletcher16(Buffer.from(line, 'binary')).toString(16);
+ console.log('sending: ' + '{' + line + '|' + checksum + '}');
+ server.send('{' + line + '|' + checksum + '}', BOARD_PORT, BOARD_ADDRESS);
+ });
+});
+server.bind(42069);
+
+// Prints: server listening 0.0.0.0:42069
diff --git a/tools/packet_counter.js b/tools/packet_counter.js
new file mode 100644
index 000000000..dae37d64e
--- /dev/null
+++ b/tools/packet_counter.js
@@ -0,0 +1,36 @@
+const dgram = require('dgram');
+const server = dgram.createSocket('udp4');
+
+const Packet = require('../electron/Packet');
+
+const pktCounts = {};
+
+server.on('error', (err) => {
+ console.log(`server error:\n${err.stack}`);
+ server.close();
+});
+server.on('message', (msg, rinfo) => {
+ const pkt = Packet.parsePacket(msg.toString());
+ if(pkt) {
+ if(!pktCounts[pkt.id]) pktCounts[pkt.id] = 0;
+ pktCounts[pkt.id] += 1;
+ }
+});
+server.on('listening', () => {
+ const address = server.address();
+ console.log(`server listening ${address.address}:${address.port}`);
+ if(process.platform === 'win32') {
+ server.send("big yeet", 42069, "10.0.0.42");
+ }
+});
+server.bind(42069);
+
+setInterval(() => {
+ for(let k of Object.keys(pktCounts)) {
+ console.log(k + " : " + pktCounts[k] + ' pkt/s');
+ pktCounts[k] = 0;
+ }
+ console.log('----------------------------------');
+}, 1000);
+
+// Prints: server listening 0.0.0.0:42069
diff --git a/tools/packet_dump.js b/tools/packet_dump.js
new file mode 100644
index 000000000..5015d02c6
--- /dev/null
+++ b/tools/packet_dump.js
@@ -0,0 +1,20 @@
+const dgram = require('dgram');
+const server = dgram.createSocket('udp4');
+
+server.on('error', (err) => {
+ console.log(`server error:\n${err.stack}`);
+ server.close();
+});
+server.on('message', (msg, rinfo) => {
+ console.log(msg.toString());
+});
+server.on('listening', () => {
+ const address = server.address();
+ console.log(`server listening ${address.address}:${address.port}`);
+ if(process.platform === 'win32') {
+ server.send("big yeet", 42069, "10.0.0.42");
+ }
+});
+server.bind(42069);
+
+// Prints: server listening 0.0.0.0:42069
diff --git a/tools/packet_raw_ac.js b/tools/packet_raw_ac.js
new file mode 100644
index 000000000..b7c0a95e8
--- /dev/null
+++ b/tools/packet_raw_ac.js
@@ -0,0 +1,24 @@
+const dgram = require('dgram');
+const server = dgram.createSocket('udp4');
+
+server.on('error', (err) => {
+ console.log(`server error:\n${err.stack}`);
+ server.close();
+});
+server.on('message', (msg, rinfo) => {
+ console.log(msg.toString());
+});
+server.on('listening', () => {
+ const address = server.address();
+ console.log(`server listening ${address.address}:${address.port}`);
+ if(process.platform === 'win32') {
+ server.send("big yeet", 42069, "10.0.0.42");
+ }
+ setInterval(() => {
+ console.log("SENDING");
+ server.send("big yeet\n", 42069, "10.0.0.81");
+ }, 500)
+});
+server.bind(42069);
+
+// Prints: server listening 0.0.0.0:42069
diff --git a/tools/packet_simulator.js b/tools/packet_simulator.js
new file mode 100644
index 000000000..5a36d5cab
--- /dev/null
+++ b/tools/packet_simulator.js
@@ -0,0 +1,95 @@
+const dgram = require('dgram')
+const DevPacket = require("./DevPacket")
+
+
+const FPS = 300 // packets per second
+
+const MAX_PACKET = 5 * 1000 // max number of packets
+// const MAX_PACKET = Math.pow(2, 26)
+
+const PACKET_ID = 10
+const PACKET_GENERATOR = () => ([Math.random() + 12, Math.random() + 12, Math.random() + 12, Math.random() + 12, Math.random() + 12, Math.random() + 12])
+const BOARD_IP = "10.0.0.11"
+
+const TARGET_PORT = 42099
+const LISTENING_PORT = 42099
+const LISTENING_HOST = '0.0.0.0'
+
+const server = dgram.createSocket('udp4')
+
+server.on('error', (err) => {
+ console.log(`${LISTENING_HOST}:${LISTENING_PORT} server error:\n${err.stack}`);
+ server.close();
+});
+
+server.on('message', (msg, rinfo) => {
+ console.debug(`[${rinfo.address}] Received message but cannot decode.`)
+});
+
+server.on('listening', () => {
+ const address = server.address();
+ console.log(`server listening ${address.address}:${address.port}`);
+ server.setBroadcast(true);
+});
+
+server.bind(LISTENING_PORT, LISTENING_HOST);
+
+function sleep(ms) {
+ return new Promise((res, _) => {
+ setTimeout(() => {
+ res(true)
+ }, ms)
+ })
+}
+
+;(async () => {
+ const START = Date.now()
+ await sleep(500)
+ // await delayedRecurse(1000 / FPS, (counter) => {
+ // return new Promise((res) => {
+ // if (counter >= MAX_PACKET) {
+ // res(false)
+ // }
+
+ // const p = new DevPacket(PACKET_ID, PACKET_GENERATOR(), Date.now() - START)
+ // // console.log(`sending #${counter}`)
+ // // const pktBuffer = p.toBuffer()
+ // const pktBuffer = new Buffer([0x95, 0x05, 0xd2, 0x4a, 0x6c, 0x00, 0x9b, 0x76, 0x02, 0xc8, 0xaf, 0x00, 0x00]);
+ const pktBuffer = new Buffer([0x95, 0x05, 0x04, 0xab, 0x6e, 0x00, 0x30, 0x4e, 0x02, 0xc8, 0xaf, 0x00, 0x00]);
+ // const addressBuffer = Buffer.from(BOARD_IP, "utf8")
+ // const lenBuffer = Buffer.alloc(1)
+ // lenBuffer.writeUInt8(addressBuffer.byteLength)
+ // const msgBuffer = Buffer.concat([lenBuffer, addressBuffer, pktBuffer])
+ // server.send(pktBuffer, TARGET_PORT, ((error) => {
+ // if (error) {
+ // console.error(error)
+ // res(false)
+ // }
+ // res(true)
+ // }))
+ // })
+ // }, 0)
+ server.send(pktBuffer, TARGET_PORT, BOARD_IP, ((error) => {
+ if (error) {
+ console.error(error)
+ res(false)
+ }
+ res(true)
+ }))
+ console.log('done')
+ process.exit(0)
+})()
+
+async function delayedRecurse(ms, handler, counter) {
+ await sleep(ms)
+ let result
+ try {
+ result = await handler(counter)
+ } catch (err) {
+ console.error(err)
+ }
+ if (!result) {
+ return
+ }
+ await delayedRecurse(ms, handler, counter + 1)
+}
diff --git a/tools/packet_version.js b/tools/packet_version.js
new file mode 100644
index 000000000..48ead819c
--- /dev/null
+++ b/tools/packet_version.js
@@ -0,0 +1,49 @@
+const dgram = require('dgram');
+
+const server = dgram.createSocket('udp4');
+
+const rl = require('readline').createInterface({
+ input: process.stdin,
+ output: process.stdout
+});
+
+// address of board to send to
+const BOARD_PORT = 42069;
+
+// checksum generator
+fletcher16 = (data) => {
+ var a = 0, b = 0;
+ for (var i = 0; i < data.length; i++) {
+ a = (a + data[i]) % 255;
+ b = (b + a) % 255;
+ }
+ return a | (b << 8);
+}
+
+server.on('error', (err) => {
+ console.log(`server error:\n${err.stack}`);
+ server.close();
+});
+server.on('message', (msg, rinfo) => {
+ let ret_packet = msg.toString();
+ if (ret_packet.substring(1,3) == "99"){
+ console.log(msg.toString());
+ }
+});
+server.on('listening', () => {
+ const address = server.address();
+ console.log(`server listening ${address.address}:${address.port}`);
+ if(process.platform === 'win32') {
+ server.send("big yeet", 42069, "10.0.0.42");
+ }
+ let line = "99, 0";
+ const checksum = fletcher16(Buffer.from(line, 'binary')).toString(16);
+ console.log('sending: ' + '{' + line + '|' + checksum + '}');
+ for (var i = 0; i < 256; i++) {
+ let dest = "10.0.0." + i;
+ server.send('{' + line + '|' + checksum + '}', BOARD_PORT, dest);
+ }
+});
+server.bind(42069);
+
+// Prints: server listening 0.0.0.0:42069