From 5ceb13e24ded70bfc051375b732f1d8c74ddce2e Mon Sep 17 00:00:00 2001 From: Paul Matthew Barrameda Date: Tue, 12 Mar 2019 22:11:35 -0700 Subject: [PATCH 001/152] Reworded/restructured code to fit JSON protocol --- .../markers/vehicles/{uav.png => plane.png} | Bin .../vehicles/{uav_red.png => plane_red.png} | Bin .../markers/vehicles/{ugv.png => rover.png} | Bin .../vehicles/{ugv_red.png => rover_red.png} | Bin resources/index.js | 32 +++-- resources/locations.json | 8 +- resources/mac-address.json | 6 - resources/vehicle.json | 74 +++++++++++ src/main/index.js | 8 +- .../fixtures/updateVehiclesFixtures.js | 119 ++--------------- src/renderer/global.css | 16 ++- src/renderer/map/MapContainer.jsx | 98 ++++++-------- src/renderer/map/VehicleMarker.jsx | 53 ++++++++ src/renderer/mission/MissionContainer.jsx | 111 +--------------- src/renderer/mission/MissionTable.jsx | 120 ++++++++++++++++++ src/renderer/vehicle/VehicleContainer.jsx | 22 +++- src/renderer/vehicle/VehicleTable.jsx | 54 +++----- src/util/util.js | 22 ++++ 18 files changed, 403 insertions(+), 340 deletions(-) rename resources/images/markers/vehicles/{uav.png => plane.png} (100%) rename resources/images/markers/vehicles/{uav_red.png => plane_red.png} (100%) rename resources/images/markers/vehicles/{ugv.png => rover.png} (100%) rename resources/images/markers/vehicles/{ugv_red.png => rover_red.png} (100%) delete mode 100644 resources/mac-address.json create mode 100644 resources/vehicle.json create mode 100644 src/renderer/map/VehicleMarker.jsx create mode 100644 src/renderer/mission/MissionTable.jsx create mode 100644 src/util/util.js diff --git a/resources/images/markers/vehicles/uav.png b/resources/images/markers/vehicles/plane.png similarity index 100% rename from resources/images/markers/vehicles/uav.png rename to resources/images/markers/vehicles/plane.png diff --git a/resources/images/markers/vehicles/uav_red.png b/resources/images/markers/vehicles/plane_red.png similarity index 100% rename from resources/images/markers/vehicles/uav_red.png rename to resources/images/markers/vehicles/plane_red.png diff --git a/resources/images/markers/vehicles/ugv.png b/resources/images/markers/vehicles/rover.png similarity index 100% rename from resources/images/markers/vehicles/ugv.png rename to resources/images/markers/vehicles/rover.png diff --git a/resources/images/markers/vehicles/ugv_red.png b/resources/images/markers/vehicles/rover_red.png similarity index 100% rename from resources/images/markers/vehicles/ugv_red.png rename to resources/images/markers/vehicles/rover_red.png diff --git a/resources/index.js b/resources/index.js index 681aa9b1..3a5a31d2 100644 --- a/resources/index.js +++ b/resources/index.js @@ -13,10 +13,10 @@ import poi_fp from './images/markers/poi_fp.png'; import poi_unknwn from './images/markers/poi_unkwn.png'; import poi_vld from './images/markers/poi_vld.png'; -import uav_red from './images/markers/vehicles/uav_red.png'; -import uav from './images/markers/vehicles/uav.png'; -import ugv_red from './images/markers/vehicles/ugv_red.png'; -import ugv from './images/markers/vehicles/ugv.png'; +import plane_red from './images/markers/vehicles/plane_red.png'; +import plane from './images/markers/vehicles/plane.png'; +import rover_red from './images/markers/vehicles/rover_red.png'; +import rover from './images/markers/vehicles/rover.png'; import fullmoon_hover from './images/other/fullmoon_hover.png'; import fullmoon from './images/other/fullmoon.png'; @@ -27,10 +27,10 @@ import moon from './images/other/moon.png'; import { cache, fixtures, geolocation } from './config.json'; import { startLocation, locations } from './locations.json'; -import * as macAddress from './mac-address.json'; +import { vehicleIds, vehicleInfos, vehicleStatuses } from './vehicle.json'; export { - cache, fixtures, geolocation, startLocation, locations, macAddress, + cache, fixtures, geolocation, startLocation, locations, vehicleIds, vehicleInfos, vehicleStatuses, }; export const images = { @@ -43,10 +43,10 @@ export const images = { poi_unknwn, poi_vld, vehicles: { - uav_red, - uav, - ugv_red, - ugv, + plane_red, + plane, + rover_red, + rover, }, }, other: { @@ -58,3 +58,15 @@ export const images = { moon, }, }; + +export default { + cache, + fixtures, + images, + geolocation, + startLocation, + locations, + vehicleIds, + vehicleInfos, + vehicleStatuses, +}; diff --git a/resources/locations.json b/resources/locations.json index 977b38da..c26d5912 100644 --- a/resources/locations.json +++ b/resources/locations.json @@ -2,13 +2,13 @@ "startLocation": "Cal Poly SLO", "locations": { "Cal Poly SLO": { - "latitude": 35.306205, - "longitude": -120.662227, + "lat": 35.306205, + "lng": -120.662227, "zoom": 18 }, "Cal Poly Pomona": { - "latitude": 34.055869, - "longitude": -117.819964, + "lat": 34.055869, + "lng": -117.819964, "zoom": 18 } } diff --git a/resources/mac-address.json b/resources/mac-address.json deleted file mode 100644 index d3243be0..00000000 --- a/resources/mac-address.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "isr": "", - "ipd": "", - "ugv": "", - "vtol": "" -} diff --git a/resources/vehicle.json b/resources/vehicle.json new file mode 100644 index 00000000..58496942 --- /dev/null +++ b/resources/vehicle.json @@ -0,0 +1,74 @@ +{ + "vehicleIds": { + "UAV": 100, + "UGV": 200, + "UUV": 300, + "VTOL 1": 400, + "VTOL 2": 401, + "ROV": 500, + "Blimp": 600 + }, + "vehicleInfos": { + "100": { + "macAddress": "", + "name": "UAV", + "type": "plane" + }, + "200": { + "macAddress": "", + "name": "UGV", + "type": "rover" + }, + "300": { + "macAddress": "", + "name": "UUV", + "type": "rover" + }, + "400": { + "macAddress": "", + "name": "VTOL 1", + "type": "plane" + }, + "401": { + "macAddress": "", + "name": "VTOL 2", + "type": "plane" + }, + "500": { + "macAddress": "", + "name": "ROV", + "type": "plane" + }, + "600": { + "macAddress": "", + "name": "Blimp", + "type": "plane" + } + }, + "vehicleStatuses": { + "ready": { + "type": "success", + "message": "Ready" + }, + "error": { + "type": "failure", + "message": "Error" + }, + "waiting": { + "type": "progress", + "message": "Waiting" + }, + "running": { + "type": "progress", + "message": "Running" + }, + "paused": { + "type": "progress", + "message": "Paused" + }, + "disconnected": { + "type": "failure", + "message": "Disconnected" + } + } +} diff --git a/src/main/index.js b/src/main/index.js index 341aea3e..8209e1ac 100644 --- a/src/main/index.js +++ b/src/main/index.js @@ -167,14 +167,10 @@ function setLocationMenu() { } Object.keys(locations).forEach((label) => { - const { latitude, longitude, zoom } = locations[label]; + const { lat, lng, zoom } = locations[label]; locationMenu.push({ label, - data: { - latitude, - longitude, - zoom, - }, + data: { lat, lng, zoom }, click(menuItem) { window.webContents.send('updateMapLocation', menuItem.data); }, }); }); diff --git a/src/renderer/fixtures/updateVehiclesFixtures.js b/src/renderer/fixtures/updateVehiclesFixtures.js index 6f6c254e..e3b7f177 100644 --- a/src/renderer/fixtures/updateVehiclesFixtures.js +++ b/src/renderer/fixtures/updateVehiclesFixtures.js @@ -2,121 +2,30 @@ import { ipcRenderer } from 'electron'; // eslint-disable-line import/no-extrane let fixtures = [ { - id: 1, - latitude: 34.056482, - longitude: -117.823912, - type: 'uav', - name: 'Valiant', + sid: 100, + lat: 34.056482, + lng: -117.823912, }, { - id: 2, - latitude: 34.053095, - longitude: -117.821970, - type: 'uav', - name: 'Multirotor', + sid: 400, + lat: 34.053095, + lng: -117.821970, }, { - id: 3, - latitude: 34.053509, - longitude: -117.818452, - type: 'ugv', - name: 'UGV', - }, - { - id: 4, - latitude: 34.053509, - longitude: -117.818452, - type: 'ugv', - name: 'UGV', - }, - { - id: 5, - latitude: 34.053509, - longitude: -117.818452, - type: 'ugv', - name: 'UGV', - }, - { - id: 6, - latitude: 34.053509, - longitude: -117.818452, - type: 'ugv', - name: 'UGV', - }, - { - id: 7, - latitude: 34.053509, - longitude: -117.818452, - type: 'ugv', - name: 'UGV', - }, - { - id: 8, - latitude: 34.053509, - longitude: -117.818452, - type: 'ugv', - name: 'UGV', - }, - { - id: 9, - latitude: 34.053509, - longitude: -117.818452, - type: 'ugv', - name: 'UGV', - }, - { - id: 10, - latitude: 34.053509, - longitude: -117.818452, - type: 'ugv', - name: 'UGV', - }, - { - id: 11, - latitude: 34.053509, - longitude: -117.818452, - type: 'ugv', - name: 'UGV', - }, - { - id: 12, - latitude: 34.053509, - longitude: -117.818452, - type: 'ugv', - name: 'UGV', - }, - { - id: 13, - latitude: 34.053509, - longitude: -117.818452, - type: 'ugv', - name: 'UGV', - }, - { - id: 14, - latitude: 34.053509, - longitude: -117.818452, - type: 'ugv', - name: 'UGV', - }, -]; -const status = [ - { - type: 'success', - message: 'Connected', - }, - { - type: 'failure', - message: 'Disconnected', + sid: 200, + lat: 34.053509, + lng: -117.818452, }, ]; +const status = ['ready', 'error', 'waiting', 'running', 'paused']; + setInterval(() => { const newFixtures = fixtures.map(fixture => ({ ...fixture, - latitude: fixture.latitude + (Math.random() / 5000) - 0.0001, - longitude: fixture.longitude + (Math.random() / 5000) - 0.0001, - status: status[Math.floor(Math.random() * 2)], + lat: fixture.lat + (Math.random() / 5000) - 0.0001, + lng: fixture.lng + (Math.random() / 5000) - 0.0001, + status: status[Math.floor(Math.random() * status.length)], })); fixtures = newFixtures; diff --git a/src/renderer/global.css b/src/renderer/global.css index cd29a598..013cf0fd 100644 --- a/src/renderer/global.css +++ b/src/renderer/global.css @@ -22,6 +22,10 @@ div:focus { color: #c62828; } +.progress { + color: #a7ad00; +} + .ReactVirtualized__Table__row:hover .success, .ReactVirtualized__Table__row_dark:hover .success { color: #43a047; @@ -31,7 +35,8 @@ div:focus { color: #76d275; } -.ReactVirtualized__Table__row:hover .failure { +.ReactVirtualized__Table__row:hover .failure, +.ReactVirtualized__Table__row_dark:hover .failure { color: #f44336; } @@ -39,6 +44,15 @@ div:focus { color: #ff7961; } +.ReactVirtualized__Table__row:hover .progress, +.ReactVirtualized__Table__row_dark:hover .progress { + color: #bfc600; +} + +.ReactVirtualized__Table__row:active .progress { + color: #d3da00; +} + /* Light Theme */ .ReactVirtualized__Table { diff --git a/src/renderer/map/MapContainer.jsx b/src/renderer/map/MapContainer.jsx index f0938ee5..13fb8467 100644 --- a/src/renderer/map/MapContainer.jsx +++ b/src/renderer/map/MapContainer.jsx @@ -1,18 +1,20 @@ import { ipcRenderer } from 'electron'; // eslint-disable-line import/no-extraneous-dependencies import fs from 'fs'; -import L from 'leaflet'; import PropTypes from 'prop-types'; -import React, { Component, createRef } from 'react'; -import { Map, Marker, Popup } from 'react-leaflet'; +import React, { Component, createRef, Fragment } from 'react'; +import { Map } from 'react-leaflet'; import { - cache, images, locations, startLocation, + cache, locations, startLocation, } from '../../../resources/index'; +import { updateVehicles } from '../../util/util'; + import GeolocationControl from './control/GeolocationControl'; import ThemeControl from './control/ThemeControl'; import CachedTileLayer from './CachedTileLayer'; +import VehicleMarker from './VehicleMarker'; import './map.css'; @@ -26,7 +28,7 @@ const mapOptions = { }; // default location is (0, 0) unless there is a locations file defined already -let start = { latitude: 0, longitude: 0, zoom: 18 }; +let start = { lat: 0, lng: 0, zoom: 18 }; if (startLocation && locations[startLocation]) { start = { ...start, ...locations[startLocation] }; } @@ -41,11 +43,9 @@ export default class MapContainer extends Component { this.state = { vehicles: {}, - latitude: start.latitude, - longitude: start.longitude, - zoom: start.zoom, + ...start, viewport: { - center: [start.latitude, start.longitude], + center: [start.lat, start.lng], zoom: start.zoom, }, }; @@ -60,7 +60,6 @@ export default class MapContainer extends Component { this.saveConfig = this.saveConfig.bind(this); this.centerMapToVehicle = this.centerMapToVehicle.bind(this); this.updateMapLocation = this.updateMapLocation.bind(this); - this.updateVehicles = this.updateVehicles.bind(this); } componentDidMount() { @@ -70,7 +69,7 @@ export default class MapContainer extends Component { ipcRenderer.on('centerMapToVehicle', (event, data) => this.centerMapToVehicle(data)); ipcRenderer.on('setMapToUserLocation', this.setMapToUserLocation); ipcRenderer.on('updateMapLocation', (event, data) => this.updateMapLocation(data)); - ipcRenderer.on('updateVehicles', (event, data) => this.updateVehicles(data)); + ipcRenderer.on('updateVehicles', (event, data) => updateVehicles(this, data)); } onViewportChanged(viewport) { @@ -98,41 +97,45 @@ export default class MapContainer extends Component { centerMapToVehicle(vehicle) { this.updateMapLocation(vehicle); - this.vehicleRefs[vehicle.id].current.leafletElement.togglePopup(); } - updateMapLocation({ - position, latitude, longitude, zoom, - }) { - const { viewport, zoom: thisZoom } = this.state; + updateMapLocation(location) { + const { zoom } = location; + + // we default to (lat, lng), however geolocation api calls provide (latitude, longitude) + const lat = location.lat || location.latitude; + const lng = location.lng || location.longitude; + + const { viewport } = this.state; this.setState({ - position, latitude, longitude, zoom: zoom || viewport.zoom, + lat, lng, zoom: zoom || viewport.zoom, }); this.onViewportChanged({ - center: position || [latitude, longitude], - zoom: zoom || thisZoom, - }); - } - - updateVehicles(vehicles) { - const { vehicles: thisVehicles } = this.state; - const currentVehicles = thisVehicles; - - vehicles.forEach((vehicle) => { - if (!this.vehicleRefs[vehicle.id]) { - this.vehicleRefs[vehicle.id] = createRef(); - } - currentVehicles[vehicle.id] = vehicle; + center: [lat, lng], + zoom: zoom || viewport.zoom, }); - this.setState({ vehicles: currentVehicles }); } render() { const { theme } = this.props; const { viewport, vehicles } = this.state; + const markers = Object.keys(vehicles).map((sid) => { + if (!this.vehicleRefs[sid]) { + this.vehicleRefs[sid] = createRef(); + } + + return ( + + ); + }); + return ( - { - Object.keys(vehicles).map((id) => { - const { - position, latitude: lat, longitude: lng, type, name, status, - } = vehicles[id]; - - return ( - - -

{`#${id}: ${name} `}

-

{`Position: [${position || [lat, lng]}]`}

-

- {'Status: '} - {status.message} -

-
-
- ); - }) - } + {markers}
); } diff --git a/src/renderer/map/VehicleMarker.jsx b/src/renderer/map/VehicleMarker.jsx new file mode 100644 index 00000000..a2de8006 --- /dev/null +++ b/src/renderer/map/VehicleMarker.jsx @@ -0,0 +1,53 @@ +import L from 'leaflet'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { Marker, Tooltip } from 'react-leaflet'; + +import { images } from '../../../resources/index'; + +const propTypes = { + sid: PropTypes.number.isRequired, + name: PropTypes.string.isRequired, + lat: PropTypes.number.isRequired, + lng: PropTypes.number.isRequired, + type: PropTypes.string.isRequired, + status: PropTypes.shape({ + type: PropTypes.oneOf(['failure', 'progress', 'success']).isRequired, + message: PropTypes.string.isRequired, + }).isRequired, + vehicleRef: PropTypes.object.isRequired, // eslint-disable-line +}; + +export default class VehicleMarker extends Component { + render() { + const { + sid, name, lat, lng, type, status, vehicleRef, + } = this.props; + + return ( + + +

{`#${sid}: ${name}`}

+

+ {'Status: '} + {status.message} +

+
+
+ ); + } +} + +VehicleMarker.propTypes = propTypes; diff --git a/src/renderer/mission/MissionContainer.jsx b/src/renderer/mission/MissionContainer.jsx index 1cfebb65..4953750a 100644 --- a/src/renderer/mission/MissionContainer.jsx +++ b/src/renderer/mission/MissionContainer.jsx @@ -1,27 +1,15 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; -import { - AutoSizer, CellMeasurer, CellMeasurerCache, Column, Table, -} from 'react-virtualized'; -import './mission.css'; +import MissionTable from './MissionTable'; -const width = { - description: 0.6, - status: 0.4, -}; +import './mission.css'; const propTypes = { theme: PropTypes.oneOf(['light', 'dark']).isRequired, }; export default class MissionContainer extends Component { - static statusRenderer({ dataKey, rowData }) { - const { type, message } = rowData.status; - - return {message}; - } - constructor(props) { super(props); @@ -34,13 +22,6 @@ export default class MissionContainer extends Component { type: 'failure', }, }, - { - description: 'Verify target locations', - status: { - message: 'Not started', - type: 'failure', - }, - }, { description: 'Deliver payload to targets', status: { @@ -49,21 +30,14 @@ export default class MissionContainer extends Component { }, }, { - description: 'Determine which target to retreive', + description: 'Retreive targets', status: { message: 'Not started', type: 'failure', }, }, { - description: 'Retreive target', - status: { - message: 'Not started', - type: 'failure', - }, - }, - { - description: 'Bring target to home base', + description: 'Return to home base', status: { message: 'Not started', type: 'failure', @@ -71,52 +45,6 @@ export default class MissionContainer extends Component { }, ], }; - - this.width = width; - - this.heightCache = new CellMeasurerCache({ - fixedWidth: true, - minHeight: 40, - }); - - this.rowGetter = this.rowGetter.bind(this); - this.rowClassName = this.rowClassName.bind(this); - this.descriptionRenderer = this.descriptionRenderer.bind(this); - } - - rowGetter({ index }) { - const { missions } = this.state; - - return missions[index]; - } - - rowClassName({ index }) { - const { theme } = this.props; - - if (theme === 'dark' && index === -1) { - return 'ReactVirtualized__Table__headerRow_dark'; - } if (theme === 'dark') { - return 'ReactVirtualized__Table__row_dark'; - } - return ''; - } - - descriptionRenderer({ - dataKey, parent, rowIndex, cellData, - }) { - return ( - -
- {cellData ? String(cellData) : ''} -
-
- ); } render() { @@ -125,36 +53,7 @@ export default class MissionContainer extends Component { return (
- - {({ height, width: tableWidth }) => ( - - - -
- )} -
+
); } diff --git a/src/renderer/mission/MissionTable.jsx b/src/renderer/mission/MissionTable.jsx new file mode 100644 index 00000000..1e619880 --- /dev/null +++ b/src/renderer/mission/MissionTable.jsx @@ -0,0 +1,120 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { + AutoSizer, CellMeasurer, CellMeasurerCache, Column, Table, +} from 'react-virtualized'; + +const width = { + description: 0.6, + status: 0.4, +}; + +const propTypes = { + theme: PropTypes.oneOf(['light', 'dark']).isRequired, + missions: PropTypes.arrayOf( + PropTypes.shape({ + description: PropTypes.string.isRequired, + status: { + type: PropTypes.oneOf(['failure', 'interim', 'success']).isRequired, + message: PropTypes.string.isRequired, + }, + }), + ).isRequired, +}; + +export default class MissionTable extends Component { + static statusRenderer({ dataKey, rowData }) { + const { type, message } = rowData.status; + + return {message}; + } + + constructor(props) { + super(props); + + this.width = width; + + this.heightCache = new CellMeasurerCache({ + fixedWidth: true, + minHeight: 40, + }); + + this.rowGetter = this.rowGetter.bind(this); + this.rowClassName = this.rowClassName.bind(this); + this.descriptionRenderer = this.descriptionRenderer.bind(this); + } + + rowGetter({ index }) { + const { missions } = this.props; + + return missions[index]; + } + + rowClassName({ index }) { + const { theme } = this.props; + + if (theme === 'dark' && index === -1) { + return 'ReactVirtualized__Table__headerRow_dark'; + } if (theme === 'dark') { + return 'ReactVirtualized__Table__row_dark'; + } + return ''; + } + + descriptionRenderer({ + dataKey, parent, rowIndex, cellData, + }) { + return ( + +
+ {cellData ? String(cellData) : ''} +
+
+ ); + } + + render() { + const { theme, missions } = this.props; + + return ( + + {({ height, width: tableWidth }) => ( + + + +
+ )} +
+ ); + } +} + +MissionTable.propTypes = propTypes; diff --git a/src/renderer/vehicle/VehicleContainer.jsx b/src/renderer/vehicle/VehicleContainer.jsx index 97e5ccfd..c3078202 100644 --- a/src/renderer/vehicle/VehicleContainer.jsx +++ b/src/renderer/vehicle/VehicleContainer.jsx @@ -1,21 +1,37 @@ +import { ipcRenderer } from 'electron'; // eslint-disable-line import/no-extraneous-dependencies import PropTypes from 'prop-types'; -import React, { PureComponent } from 'react'; +import React, { Component } from 'react'; import VehicleTable from './VehicleTable'; +import { updateVehicles } from '../../util/util'; + import './vehicle.css'; const propTypes = { theme: PropTypes.oneOf(['light', 'dark']).isRequired, }; -export default class VehicleContainer extends PureComponent { +export default class VehicleContainer extends Component { + constructor(props) { + super(props); + + this.state = { + vehicles: {}, + }; + } + + componentDidMount() { + ipcRenderer.on('updateVehicles', (event, data) => updateVehicles(this, data)); + } + render() { const { theme } = this.props; + const { vehicles } = this.state; return (
- +
); } diff --git a/src/renderer/vehicle/VehicleTable.jsx b/src/renderer/vehicle/VehicleTable.jsx index 9143e7d7..e8483845 100644 --- a/src/renderer/vehicle/VehicleTable.jsx +++ b/src/renderer/vehicle/VehicleTable.jsx @@ -4,13 +4,23 @@ import React, { Component } from 'react'; import { AutoSizer, Table, Column } from 'react-virtualized'; const width = { - id: 0.15, + id: 0.2, name: 0.4, - status: 0.45, + status: 0.4, }; const propTypes = { theme: PropTypes.oneOf(['light', 'dark']).isRequired, + vehicles: PropTypes.objectOf( + PropTypes.shape({ + sid: PropTypes.number.isRequired, + name: PropTypes.string.isRequired, + status: { + type: PropTypes.oneOf(['failure', 'progress', 'success']).isRequired, + message: PropTypes.string.isRequired, + }, + }), + ).isRequired, }; export default class VehicleTable extends Component { @@ -18,49 +28,25 @@ export default class VehicleTable extends Component { return {rowData.status.message}; } - static centerMapToVehicle(vehicle) { - ipcRenderer.send('post', 'centerMapToVehicle', vehicle); - } - constructor(props) { super(props); - this.state = { - vehicles: {}, - }; - this.width = width; this.onRowClick = this.onRowClick.bind(this); - this.updateVehicles = this.updateVehicles.bind(this); this.rowGetter = this.rowGetter.bind(this); this.rowClassName = this.rowClassName.bind(this); } - componentDidMount() { - ipcRenderer.on('updateVehicles', (event, data) => this.updateVehicles(data)); - } - onRowClick({ rowData }) { - const { vehicles } = this.state; + const { vehicles } = this.props; - VehicleTable.centerMapToVehicle(vehicles[rowData.id]); - } - - updateVehicles(vehicles) { - const { vehicles: thisVehicles } = this.state; - const currentVehicles = thisVehicles; - - vehicles.forEach((vehicle) => { - currentVehicles[vehicle.id] = vehicle; - }); - - this.setState({ vehicles: currentVehicles }); + ipcRenderer.send('post', 'centerMapToVehicle', vehicles[rowData.sid]); } rowGetter({ index }) { - const { vehicles } = this.state; + const { vehicles } = this.props; const v = Object.keys(vehicles).sort((a, b) => parseInt(a, 10) - parseInt(b, 10)); return vehicles[v[index]]; @@ -78,18 +64,12 @@ export default class VehicleTable extends Component { } render() { - const { theme } = this.props; - const { vehicles } = this.state; + const { theme, vehicles } = this.props; return ( {({ height, width: tableWidth }) => ( diff --git a/src/util/util.js b/src/util/util.js new file mode 100644 index 00000000..4d501ed6 --- /dev/null +++ b/src/util/util.js @@ -0,0 +1,22 @@ +import { + vehicleInfos, vehicleStatuses, +} from '../../resources/index'; + +export function updateVehicles(component, vehicles) { + const { vehicles: thisVehicles } = component.state; + const currentVehicles = thisVehicles; + + vehicles.forEach((vehicle) => { + currentVehicles[vehicle.sid] = { + ...vehicle, + ...vehicleInfos[vehicle.sid], + status: vehicleStatuses[vehicle.status], + }; + }); + + component.setState({ vehicles: currentVehicles }); +} + +export default { + updateVehicles, +}; From 5242b42a43bc40441a8bc8658666a6f934e3c570 Mon Sep 17 00:00:00 2001 From: Paul Matthew Barrameda Date: Wed, 13 Mar 2019 11:59:17 -0700 Subject: [PATCH 002/152] Removed vehicle refs from MapContainer and fixed propTypes --- .../fixtures/updateVehiclesFixtures.js | 5 +++-- src/renderer/map/MapContainer.jsx | 21 ++++++------------- src/renderer/map/VehicleMarker.jsx | 8 +++---- src/renderer/mission/MissionTable.jsx | 6 +++--- src/renderer/vehicle/VehicleTable.jsx | 4 ++-- 5 files changed, 17 insertions(+), 27 deletions(-) diff --git a/src/renderer/fixtures/updateVehiclesFixtures.js b/src/renderer/fixtures/updateVehiclesFixtures.js index e3b7f177..0a32322b 100644 --- a/src/renderer/fixtures/updateVehiclesFixtures.js +++ b/src/renderer/fixtures/updateVehiclesFixtures.js @@ -1,4 +1,5 @@ import { ipcRenderer } from 'electron'; // eslint-disable-line import/no-extraneous-dependencies +import { vehicleStatuses } from '../../../resources/index'; let fixtures = [ { @@ -18,12 +19,12 @@ let fixtures = [ }, ]; -const status = ['ready', 'error', 'waiting', 'running', 'paused']; +const status = Object.keys(vehicleStatuses); setInterval(() => { const newFixtures = fixtures.map(fixture => ({ ...fixture, - lat: fixture.lat + (Math.random() / 5000) - 0.0001, + // lat: fixture.lat + (Math.random() / 5000) - 0.0001, lng: fixture.lng + (Math.random() / 5000) - 0.0001, status: status[Math.floor(Math.random() * status.length)], })); diff --git a/src/renderer/map/MapContainer.jsx b/src/renderer/map/MapContainer.jsx index 13fb8467..ff0f4806 100644 --- a/src/renderer/map/MapContainer.jsx +++ b/src/renderer/map/MapContainer.jsx @@ -50,8 +50,6 @@ export default class MapContainer extends Component { }, }; - this.vehicleRefs = {}; - this.ref = createRef(); this.onViewportChanged = this.onViewportChanged.bind(this); @@ -122,19 +120,12 @@ export default class MapContainer extends Component { const { theme } = this.props; const { viewport, vehicles } = this.state; - const markers = Object.keys(vehicles).map((sid) => { - if (!this.vehicleRefs[sid]) { - this.vehicleRefs[sid] = createRef(); - } - - return ( - - ); - }); + const markers = Object.keys(vehicles).map(sid => ( + + )); return ( Date: Thu, 14 Mar 2019 17:45:50 -0700 Subject: [PATCH 003/152] Added popup window It is located in index.js to maintain a global variable of it and it is better allocated there than inside MissionContainer --- src/main/index.js | 130 ++++++++++++++++------ src/renderer/mission/MissionContainer.jsx | 100 ++++++++++++++--- src/renderer/mission/MissionTable.jsx | 7 +- 3 files changed, 186 insertions(+), 51 deletions(-) diff --git a/src/main/index.js b/src/main/index.js index 8209e1ac..1dda11c5 100644 --- a/src/main/index.js +++ b/src/main/index.js @@ -4,14 +4,24 @@ import { // eslint-disable-line import/no-extraneous-dependencies import fs from 'fs'; import moment from 'moment'; import path from 'path'; -import { format as formatUrl } from 'url'; +import url from 'url'; import { images, locations } from '../../resources/index'; +process.env.GOOGLE_API_KEY = 'AIzaSyB1gepR_EONqgEcxuADmEZjizTuOU_cfnU'; + +const FILTER = { name: 'GCS Configuration', extensions: ['json'] }; +const WIDTH = 1024; +const HEIGHT = 576; + +const icon = nativeImage.createFromDataURL(images.icon); +const isDevelopment = process.env.NODE_ENV !== 'production'; + let quitting = false; let window; +let missionWindow; +let tray; -const FILTER = { name: 'GCS Configuration', extensions: ['json'] }; const quitRole = { label: 'Quit', accelerator: 'CommandOrControl+Q', @@ -21,8 +31,6 @@ const quitRole = { }, }; -process.env.GOOGLE_API_KEY = 'AIzaSyB1gepR_EONqgEcxuADmEZjizTuOU_cfnU'; - const darwinMenu = { label: 'NGCP Ground Control System', submenu: [ @@ -40,8 +48,6 @@ const darwinMenu = { quitRole, ], }; -const icon = nativeImage.createFromDataURL(images.icon); -const isDevelopment = process.env.NODE_ENV !== 'production'; function saveConfig() { const fileName = moment().format('[GCS Configuration] YYYY-MM-DD [at] h.mm.ss A'); @@ -144,16 +150,6 @@ const menu = [ ], }, ]; -const trayMenu = [ - { - label: 'NGCP Ground Control Station', - click() { window.show(); }, - }, - { type: 'separator' }, - quitRole, -]; - -let tray; function setLocationMenu() { const locationMenu = menu.find(m => m.label === 'Locations').submenu; @@ -181,16 +177,16 @@ function createMainWindow() { title: 'NGCP Ground Control Station', icon, show: false, - width: 1024, - minWidth: 1024, - height: 576, - minHeight: 576, + width: WIDTH, + minWidth: WIDTH, + height: HEIGHT, + minHeight: HEIGHT, }); if (isDevelopment) { window.loadURL(`http://localhost:${process.env.ELECTRON_WEBPACK_WDS_PORT}`); } else { - window.loadURL(formatUrl({ + window.loadURL(url.format({ pathname: path.resolve(__dirname, 'index.html'), protocol: 'file', slashes: true, @@ -199,19 +195,83 @@ function createMainWindow() { window.on('ready-to-show', () => { window.show(); - window.focus(); }); window.on('close', (event) => { if (!quitting) { event.preventDefault(); window.hide(); + if (missionWindow) { + window.webContents.send('setSelectedMission', -1); + missionWindow.hide(); + } + } else { + window = null; + } + }); +} + +function createMissionWindow() { + missionWindow = new BrowserWindow({ + title: 'NGCP Mission User Interface', + show: false, + width: WIDTH, + minWidth: WIDTH, + height: HEIGHT, + minHeight: HEIGHT, + }); + + missionWindow.loadURL(url.format({ + pathname: path.resolve(__dirname, 'ui.html'), + protocol: 'file', + slashes: true, + })); + + missionWindow.show(); + + /* + missionWindow.on('ready-to-show', () => { + missionWindow.show(); + }); + */ + + missionWindow.on('close', (event) => { + if (!quitting) { + event.preventDefault(); + // following line allows MissionContainer to update to closed mission window + window.webContents.send('setSelectedMission', -1); + missionWindow.hide(); + } else { + missionWindow = null; } }); +} + +function showWindow() { + if (!window) { + createMainWindow(); + } else { + window.show(); + } +} - return window; +function showMissionWindow() { + if (!missionWindow) { + createMissionWindow(); + } else { + missionWindow.show(); + } } +const trayMenu = [ + { + label: 'NGCP Ground Control Station', + click() { showWindow(); }, + }, + { type: 'separator' }, + quitRole, +]; + function createMenu() { setLocationMenu(); @@ -220,7 +280,7 @@ function createMenu() { } else { tray = new Tray(icon); tray.setContextMenu(Menu.buildFromTemplate(trayMenu)); - tray.on('click', () => window.show()); + tray.on('click', () => { showWindow(); }); menu.push(quitRole); } @@ -228,17 +288,10 @@ function createMenu() { Menu.setApplicationMenu(Menu.buildFromTemplate(menu)); } - -app.on('activate', () => { - if (window === null) { - createMainWindow(); - } else { - window.show(); - } -}); +app.on('activate', showWindow); app.on('ready', () => { - createMainWindow(); + showWindow(); createMenu(); }); @@ -246,4 +299,13 @@ app.on('before-quit', () => { quitting = true; }); -ipcMain.on('post', (event, notification, data) => window.webContents.send(notification, data)); +ipcMain.on('post', (event, notification, data) => { + if (notification === 'showMissionWindow') { + showMissionWindow(); + } + + window.webContents.send(notification, data); + if (missionWindow) { + missionWindow.webContents.send(notification, data); + } +}); diff --git a/src/renderer/mission/MissionContainer.jsx b/src/renderer/mission/MissionContainer.jsx index 4953750a..432f225a 100644 --- a/src/renderer/mission/MissionContainer.jsx +++ b/src/renderer/mission/MissionContainer.jsx @@ -1,3 +1,4 @@ +import { ipcRenderer } from 'electron'; // eslint-disable-line import/no-extraneous-dependencies import PropTypes from 'prop-types'; import React, { Component } from 'react'; @@ -9,42 +10,109 @@ const propTypes = { theme: PropTypes.oneOf(['light', 'dark']).isRequired, }; +let started = false; + +const statusTypes = { + notStarted: { + message: 'Not started', + type: 'failure', + }, + open: { + message: 'Currently open', + type: 'progress', + }, + started: { + message: 'Started', + type: 'progress', + }, + finished: { + message: 'Finished', + type: 'success', + }, +}; + export default class MissionContainer extends Component { constructor(props) { super(props); this.state = { + openedMissionIndex: -1, missions: [ { description: 'Find targets', - status: { - message: 'Not started', - type: 'failure', - }, + status: statusTypes.notStarted, }, { description: 'Deliver payload to targets', - status: { - message: 'Not started', - type: 'failure', - }, + status: statusTypes.notStarted, }, { description: 'Retreive targets', - status: { - message: 'Not started', - type: 'failure', - }, + status: statusTypes.notStarted, }, { description: 'Return to home base', - status: { - message: 'Not started', - type: 'failure', - }, + status: statusTypes.notStarted, }, ], }; + + this.setSelectedMission = this.setSelectedMission.bind(this); + this.completeMission = this.completeMission.bind(this); + } + + componentDidMount() { + ipcRenderer.on('setSelectedMission', (event, index) => this.setSelectedMission(index)); + + ipcRenderer.on('startMission', () => { started = true; }); + ipcRenderer.on('stopMission', () => { started = false; }); + ipcRenderer.on('completeMission', (event, index) => this.completeMission(index)); + } + + /** + * Select mission based on index and open corresponding mission window + * @param {index} number 0 <= index < this.state.missions.length, -1 means close mission window + */ + setSelectedMission(index) { + const { missions, openedMissionIndex } = this.state; + + // exit function for any finished missions + if (index !== -1 && missions[index].status === statusTypes.finished) return; + + // opens mission window for specified mission + if (index !== -1) { + ipcRenderer.send('post', 'showMissionWindow', index); + } + + // exit function if selected mission is selected again + if (openedMissionIndex === index) return; + + // update mission status to new selected mission + const newMissions = missions; + + if (openedMissionIndex !== -1) { + newMissions[openedMissionIndex].status = statusTypes.notStarted; + } + if (index !== -1) { + newMissions[index].status = statusTypes.open; + } else if (started) { + newMissions[openedMissionIndex].status = statusTypes.started; + } else { + newMissions[openedMissionIndex].status = statusTypes.notStarted; + } + + this.setState({ + openedMissionIndex: index, + missions: newMissions, + }); + } + + completeMission(index) { + const { missions } = this.state; + const newMissions = missions; + + newMissions[index].status = statusTypes.finished; + this.setState({ missions: newMissions }); } render() { diff --git a/src/renderer/mission/MissionTable.jsx b/src/renderer/mission/MissionTable.jsx index 9d8dd048..95dd94f9 100644 --- a/src/renderer/mission/MissionTable.jsx +++ b/src/renderer/mission/MissionTable.jsx @@ -1,3 +1,4 @@ +import { ipcRenderer } from 'electron'; // eslint-disable-line import/no-extraneous-dependencies import PropTypes from 'prop-types'; import React, { Component } from 'react'; import { @@ -29,6 +30,10 @@ export default class MissionTable extends Component { return {message}; } + static onRowClick({ index }) { + ipcRenderer.send('post', 'setSelectedMission', index); + } + constructor(props) { super(props); @@ -92,7 +97,7 @@ export default class MissionTable extends Component { rowHeight={this.heightCache.rowHeight} rowCount={missions.length} rowGetter={this.rowGetter} - onRowClick={this.onRowClick} + onRowClick={MissionTable.onRowClick} className={theme === 'dark' ? 'ReactVirtualized__Table_dark' : ''} rowClassName={this.rowClassName} > From 7eeb5982118b1d17289e355065bb2bb689989c0f Mon Sep 17 00:00:00 2001 From: Paul Matthew Barrameda Date: Fri, 15 Mar 2019 13:02:19 -0700 Subject: [PATCH 004/152] Versioning overhaul --- package.json | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/package.json b/package.json index 10e5ad49..d9070a5b 100644 --- a/package.json +++ b/package.json @@ -11,35 +11,35 @@ "build": "electron-webpack && electron-builder" }, "dependencies": { - "leaflet": "^1.3.4", - "moment": "^2.22.2", + "leaflet": "^1.4.0", + "moment": "^2.24.0", "pouchdb": "^7.0.0", - "prop-types": "^15.6.2", - "react": "^16.5.2", - "react-dom": "^16.5.2", - "react-leaflet": "^2.1.0", - "react-leaflet-control": "^2.0.0", + "prop-types": "^15.7.2", + "react": "^16.8.4", + "react-dom": "^16.8.4", + "react-leaflet": "^2.2.1", + "react-leaflet-control": "^2.1.1", "react-virtualized": "^9.21.0", - "serialport": "^6.2.2", - "source-map-support": "^0.5.9", + "serialport": "^7.1.4", + "source-map-support": "^0.5.11", "xbee-api": "^0.6.0" }, "devDependencies": { "@babel/preset-react": "^7.0.0", "babel-eslint": "^10.0.1", - "dotenv-webpack": "^1.5.7", - "electron": "^3.0.2", - "electron-builder": "^20.28.4", - "electron-webpack": "2.3.1", - "eslint": "^5.7.0", + "dotenv-webpack": "^1.7.0", + "electron": "^4.1.0", + "electron-builder": "^20.39.0", + "electron-webpack": "^2.6.2", + "eslint": "^5.15.2", "eslint-config-airbnb": "^17.1.0", - "eslint-plugin-import": "^2.14.0", + "eslint-plugin-import": "^2.16.0", "eslint-plugin-jsx-a11y": "^6.2.1", - "eslint-plugin-react": "^7.11.1", - "stylelint": "^9.6.0", + "eslint-plugin-react": "^7.12.4", + "stylelint": "^9.10.1", "stylelint-config-standard": "^18.2.0", - "stylelint-order": "^1.0.0", - "webpack": "^4.22.0" + "stylelint-order": "^2.1.0", + "webpack": "^4.29.6" }, "electronWebpack": { "main": { From 4ff7f5c9dec48b07e0b27f5f560a31971a6abe92 Mon Sep 17 00:00:00 2001 From: Paul Matthew Barrameda Date: Fri, 15 Mar 2019 15:17:08 -0700 Subject: [PATCH 005/152] Updated missionWindow to work as intended Does not work for building yet, will look into solutions --- package.json | 2 +- src/main/index.js | 14 ++++--- src/renderer/index.js | 24 ++++++----- src/renderer/mainWindow/MainWindow.jsx | 24 +++++++++++ .../{ => mainWindow}/log/LogContainer.jsx | 0 src/renderer/{ => mainWindow}/log/log.css | 0 .../{ => mainWindow}/map/CachedTileLayer.js | 2 +- .../{ => mainWindow}/map/MapContainer.jsx | 4 +- .../{ => mainWindow}/map/VehicleMarker.jsx | 2 +- .../{ => mainWindow}/map/control/Control.jsx | 0 .../map/control/GeolocationControl.jsx | 0 .../map/control/ThemeControl.jsx | 0 src/renderer/mainWindow/map/map.css | 42 +++++++++++++++++++ .../mission/MissionContainer.jsx | 0 .../{ => mainWindow}/mission/MissionTable.jsx | 0 .../{ => mainWindow}/mission/mission.css | 0 .../vehicle/VehicleContainer.jsx | 2 +- .../{ => mainWindow}/vehicle/VehicleTable.jsx | 0 .../vehicle/XbeeConnectContainer.jsx | 0 .../{ => mainWindow}/vehicle/vehicle.css | 0 src/renderer/map/map.css | 42 ------------------- src/renderer/missionWindow/MissionWindow.jsx | 5 +++ 22 files changed, 100 insertions(+), 63 deletions(-) create mode 100644 src/renderer/mainWindow/MainWindow.jsx rename src/renderer/{ => mainWindow}/log/LogContainer.jsx (100%) rename src/renderer/{ => mainWindow}/log/log.css (100%) rename src/renderer/{ => mainWindow}/map/CachedTileLayer.js (87%) rename src/renderer/{ => mainWindow}/map/MapContainer.jsx (97%) rename src/renderer/{ => mainWindow}/map/VehicleMarker.jsx (96%) rename src/renderer/{ => mainWindow}/map/control/Control.jsx (100%) rename src/renderer/{ => mainWindow}/map/control/GeolocationControl.jsx (100%) rename src/renderer/{ => mainWindow}/map/control/ThemeControl.jsx (100%) create mode 100644 src/renderer/mainWindow/map/map.css rename src/renderer/{ => mainWindow}/mission/MissionContainer.jsx (100%) rename src/renderer/{ => mainWindow}/mission/MissionTable.jsx (100%) rename src/renderer/{ => mainWindow}/mission/mission.css (100%) rename src/renderer/{ => mainWindow}/vehicle/VehicleContainer.jsx (94%) rename src/renderer/{ => mainWindow}/vehicle/VehicleTable.jsx (100%) rename src/renderer/{ => mainWindow}/vehicle/XbeeConnectContainer.jsx (100%) rename src/renderer/{ => mainWindow}/vehicle/vehicle.css (100%) delete mode 100644 src/renderer/map/map.css create mode 100644 src/renderer/missionWindow/MissionWindow.jsx diff --git a/package.json b/package.json index d9070a5b..5676214c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "GCS", - "version": "0.5.1", + "version": "0.5.2", "description": "Ground Control Station for autonomous vehicle platforms in NGCP", "scripts": { "test": "npm run linttest", diff --git a/src/main/index.js b/src/main/index.js index 1dda11c5..4a7bc940 100644 --- a/src/main/index.js +++ b/src/main/index.js @@ -221,11 +221,15 @@ function createMissionWindow() { minHeight: HEIGHT, }); - missionWindow.loadURL(url.format({ - pathname: path.resolve(__dirname, 'ui.html'), - protocol: 'file', - slashes: true, - })); + if (isDevelopment) { + missionWindow.loadURL(`http://localhost:${process.env.ELECTRON_WEBPACK_WDS_PORT}?route=mission`); + } else { + missionWindow.loadURL(url.format({ + pathname: path.resolve(__dirname, 'index.html'), + protocol: 'file', + slashes: true, + })); + } missionWindow.show(); diff --git a/src/renderer/index.js b/src/renderer/index.js index 8358d5e4..f0e80aab 100644 --- a/src/renderer/index.js +++ b/src/renderer/index.js @@ -6,10 +6,8 @@ import ReactDOM from 'react-dom'; import { fixtures, geolocation } from '../../resources/index'; -import LogContainer from './log/LogContainer'; -import MapContainer from './map/MapContainer'; -import MissionContainer from './mission/MissionContainer'; -import VehicleContainer from './vehicle/VehicleContainer'; +import MainWindow from './mainWindow/MainWindow'; +import MissionWindow from './missionWindow/MissionWindow'; import 'leaflet/dist/leaflet.css'; import 'react-virtualized/styles.css'; @@ -19,6 +17,12 @@ import './index.css'; const isDevelopment = process.env.NODE_ENV !== 'production'; +function Router({ routes }) { + const route = new URLSearchParams(window.location.search).get('route') || 'default'; + + return routes[route] || routes.default; +} + class Index extends Component { constructor(props) { super(props); @@ -44,12 +48,12 @@ class Index extends Component { const { theme } = this.state; return ( -
- - - - -
+ , + mission: , + }} + /> ); } } diff --git a/src/renderer/mainWindow/MainWindow.jsx b/src/renderer/mainWindow/MainWindow.jsx new file mode 100644 index 00000000..1df012e7 --- /dev/null +++ b/src/renderer/mainWindow/MainWindow.jsx @@ -0,0 +1,24 @@ +import PropTypes from 'prop-types'; +import React from 'react'; + +import LogContainer from './log/LogContainer'; +import MapContainer from './map/MapContainer'; +import MissionContainer from './mission/MissionContainer'; +import VehicleContainer from './vehicle/VehicleContainer'; + +const propTypes = { + theme: PropTypes.oneOf(['light', 'dark']).isRequired, +}; + +export default function MainWindow({ theme }) { + return ( +
+ + + + +
+ ); +} + +MainWindow.propTypes = propTypes; diff --git a/src/renderer/log/LogContainer.jsx b/src/renderer/mainWindow/log/LogContainer.jsx similarity index 100% rename from src/renderer/log/LogContainer.jsx rename to src/renderer/mainWindow/log/LogContainer.jsx diff --git a/src/renderer/log/log.css b/src/renderer/mainWindow/log/log.css similarity index 100% rename from src/renderer/log/log.css rename to src/renderer/mainWindow/log/log.css diff --git a/src/renderer/map/CachedTileLayer.js b/src/renderer/mainWindow/map/CachedTileLayer.js similarity index 87% rename from src/renderer/map/CachedTileLayer.js rename to src/renderer/mainWindow/map/CachedTileLayer.js index 9cc41a38..e6efb979 100644 --- a/src/renderer/map/CachedTileLayer.js +++ b/src/renderer/mainWindow/map/CachedTileLayer.js @@ -1,6 +1,6 @@ import { GridLayer, withLeaflet } from 'react-leaflet'; -import TileLayer from '../../util/TileLayer.CachedTileLayer'; +import TileLayer from '../../../util/TileLayer.CachedTileLayer'; class CachedTileLayer extends GridLayer { createLeafletElement(props) { diff --git a/src/renderer/map/MapContainer.jsx b/src/renderer/mainWindow/map/MapContainer.jsx similarity index 97% rename from src/renderer/map/MapContainer.jsx rename to src/renderer/mainWindow/map/MapContainer.jsx index ff0f4806..111c0fbe 100644 --- a/src/renderer/map/MapContainer.jsx +++ b/src/renderer/mainWindow/map/MapContainer.jsx @@ -6,9 +6,9 @@ import { Map } from 'react-leaflet'; import { cache, locations, startLocation, -} from '../../../resources/index'; +} from '../../../../resources/index'; -import { updateVehicles } from '../../util/util'; +import { updateVehicles } from '../../../util/util'; import GeolocationControl from './control/GeolocationControl'; import ThemeControl from './control/ThemeControl'; diff --git a/src/renderer/map/VehicleMarker.jsx b/src/renderer/mainWindow/map/VehicleMarker.jsx similarity index 96% rename from src/renderer/map/VehicleMarker.jsx rename to src/renderer/mainWindow/map/VehicleMarker.jsx index c12368f6..de8276ee 100644 --- a/src/renderer/map/VehicleMarker.jsx +++ b/src/renderer/mainWindow/map/VehicleMarker.jsx @@ -3,7 +3,7 @@ import PropTypes from 'prop-types'; import React, { PureComponent } from 'react'; import { Marker, Tooltip } from 'react-leaflet'; -import { images } from '../../../resources/index'; +import { images } from '../../../../resources/index'; const propTypes = { sid: PropTypes.number.isRequired, diff --git a/src/renderer/map/control/Control.jsx b/src/renderer/mainWindow/map/control/Control.jsx similarity index 100% rename from src/renderer/map/control/Control.jsx rename to src/renderer/mainWindow/map/control/Control.jsx diff --git a/src/renderer/map/control/GeolocationControl.jsx b/src/renderer/mainWindow/map/control/GeolocationControl.jsx similarity index 100% rename from src/renderer/map/control/GeolocationControl.jsx rename to src/renderer/mainWindow/map/control/GeolocationControl.jsx diff --git a/src/renderer/map/control/ThemeControl.jsx b/src/renderer/mainWindow/map/control/ThemeControl.jsx similarity index 100% rename from src/renderer/map/control/ThemeControl.jsx rename to src/renderer/mainWindow/map/control/ThemeControl.jsx diff --git a/src/renderer/mainWindow/map/map.css b/src/renderer/mainWindow/map/map.css new file mode 100644 index 00000000..559dbd73 --- /dev/null +++ b/src/renderer/mainWindow/map/map.css @@ -0,0 +1,42 @@ +/* Defined styles for Map.js */ + +.mapContainer p { + margin: 0; +} + +.leaflet-control-custom { + cursor: pointer; + background-size: 75%; +} + +.geolocation-control { + background-image: url('../../../../resources/images/other/geolocation.png'); +} + +.geolocation-control:hover { + background-image: url('../../../../resources/images/other/geolocation_hover.png'); +} + +/* Light theme */ + +.theme-control { + cursor: pointer; + background-size: 75%; + background-image: url('../../../../resources/images/other/moon.png'); +} + +.theme-control:hover { + background-image: url('../../../../resources/images/other/moon_hover.png'); +} + +/* Dark theme */ + +.theme-control_dark { + cursor: pointer; + background-size: 75%; + background-image: url('../../../../resources/images/other/fullmoon.png'); +} + +.theme-control_dark:hover { + background-image: url('../../../../resources/images/other/fullmoon_hover.png'); +} diff --git a/src/renderer/mission/MissionContainer.jsx b/src/renderer/mainWindow/mission/MissionContainer.jsx similarity index 100% rename from src/renderer/mission/MissionContainer.jsx rename to src/renderer/mainWindow/mission/MissionContainer.jsx diff --git a/src/renderer/mission/MissionTable.jsx b/src/renderer/mainWindow/mission/MissionTable.jsx similarity index 100% rename from src/renderer/mission/MissionTable.jsx rename to src/renderer/mainWindow/mission/MissionTable.jsx diff --git a/src/renderer/mission/mission.css b/src/renderer/mainWindow/mission/mission.css similarity index 100% rename from src/renderer/mission/mission.css rename to src/renderer/mainWindow/mission/mission.css diff --git a/src/renderer/vehicle/VehicleContainer.jsx b/src/renderer/mainWindow/vehicle/VehicleContainer.jsx similarity index 94% rename from src/renderer/vehicle/VehicleContainer.jsx rename to src/renderer/mainWindow/vehicle/VehicleContainer.jsx index c3078202..28e93627 100644 --- a/src/renderer/vehicle/VehicleContainer.jsx +++ b/src/renderer/mainWindow/vehicle/VehicleContainer.jsx @@ -4,7 +4,7 @@ import React, { Component } from 'react'; import VehicleTable from './VehicleTable'; -import { updateVehicles } from '../../util/util'; +import { updateVehicles } from '../../../util/util'; import './vehicle.css'; diff --git a/src/renderer/vehicle/VehicleTable.jsx b/src/renderer/mainWindow/vehicle/VehicleTable.jsx similarity index 100% rename from src/renderer/vehicle/VehicleTable.jsx rename to src/renderer/mainWindow/vehicle/VehicleTable.jsx diff --git a/src/renderer/vehicle/XbeeConnectContainer.jsx b/src/renderer/mainWindow/vehicle/XbeeConnectContainer.jsx similarity index 100% rename from src/renderer/vehicle/XbeeConnectContainer.jsx rename to src/renderer/mainWindow/vehicle/XbeeConnectContainer.jsx diff --git a/src/renderer/vehicle/vehicle.css b/src/renderer/mainWindow/vehicle/vehicle.css similarity index 100% rename from src/renderer/vehicle/vehicle.css rename to src/renderer/mainWindow/vehicle/vehicle.css diff --git a/src/renderer/map/map.css b/src/renderer/map/map.css deleted file mode 100644 index d7011a3c..00000000 --- a/src/renderer/map/map.css +++ /dev/null @@ -1,42 +0,0 @@ -/* Defined styles for Map.js */ - -.mapContainer p { - margin: 0; -} - -.leaflet-control-custom { - cursor: pointer; - background-size: 75%; -} - -.geolocation-control { - background-image: url('../../../resources/images/other/geolocation.png'); -} - -.geolocation-control:hover { - background-image: url('../../../resources/images/other/geolocation_hover.png'); -} - -/* Light theme */ - -.theme-control { - cursor: pointer; - background-size: 75%; - background-image: url('../../../resources/images/other/moon.png'); -} - -.theme-control:hover { - background-image: url('../../../resources/images/other/moon_hover.png'); -} - -/* Dark theme */ - -.theme-control_dark { - cursor: pointer; - background-size: 75%; - background-image: url('../../../resources/images/other/fullmoon.png'); -} - -.theme-control_dark:hover { - background-image: url('../../../resources/images/other/fullmoon_hover.png'); -} diff --git a/src/renderer/missionWindow/MissionWindow.jsx b/src/renderer/missionWindow/MissionWindow.jsx new file mode 100644 index 00000000..6de55383 --- /dev/null +++ b/src/renderer/missionWindow/MissionWindow.jsx @@ -0,0 +1,5 @@ +import React from 'react'; + +export default function MissionWindow() { + return

Waassup!

; +} From 0a4f90a2b4b5ec7a36111d80fe9dab1a1422b534 Mon Sep 17 00:00:00 2001 From: Paul Matthew Barrameda Date: Sat, 16 Mar 2019 04:47:08 -0700 Subject: [PATCH 006/152] Multi-window support for electron-webpack Allows us to create more than one window on electron and linking them all to one html file without collision. This works for building the app too, not just on development. Next step is to update the new mission window to display whatever information is necessary for mission selected. See #45 --- src/main/index.js | 84 +++++++++---------- src/renderer/fixtures/index.js | 9 +- .../fixtures/updateMessagesFixtures.js | 6 +- .../fixtures/updateVehiclesFixtures.js | 6 +- src/renderer/index.js | 20 ++--- .../mainWindow/map/control/Control.jsx | 2 +- 6 files changed, 65 insertions(+), 62 deletions(-) diff --git a/src/main/index.js b/src/main/index.js index 4a7bc940..6d28adec 100644 --- a/src/main/index.js +++ b/src/main/index.js @@ -3,8 +3,6 @@ import { // eslint-disable-line import/no-extraneous-dependencies } from 'electron'; import fs from 'fs'; import moment from 'moment'; -import path from 'path'; -import url from 'url'; import { images, locations } from '../../resources/index'; @@ -14,12 +12,18 @@ const FILTER = { name: 'GCS Configuration', extensions: ['json'] }; const WIDTH = 1024; const HEIGHT = 576; -const icon = nativeImage.createFromDataURL(images.icon); const isDevelopment = process.env.NODE_ENV !== 'production'; +// todo: put icon tray back to macOS but resize it so that its not huge on macOS's menu +const icon = nativeImage.createFromDataURL(images.icon); + let quitting = false; -let window; + +// References to window objects +let mainWindow; let missionWindow; + +// Tray object let tray; const quitRole = { @@ -51,7 +55,7 @@ const darwinMenu = { function saveConfig() { const fileName = moment().format('[GCS Configuration] YYYY-MM-DD [at] h.mm.ss A'); - const filePath = dialog.showSaveDialog(window, { + const filePath = dialog.showSaveDialog(mainWindow, { title: 'Save Configuration', filters: [FILTER], defaultPath: `./${fileName}.${FILTER.extensions[0]}`, @@ -60,14 +64,14 @@ function saveConfig() { if (!filePath) return; const data = {}; - window.webContents.send('saveConfig', { + mainWindow.webContents.send('saveConfig', { filePath, data, }); } function loadConfig() { - const filePaths = dialog.showOpenDialog(window, { + const filePaths = dialog.showOpenDialog(mainWindow, { title: 'Open Configuration', filters: [FILTER], properties: ['openFile', 'createDirectory'], @@ -79,7 +83,7 @@ function loadConfig() { if (!data) return; - window.webContents.send('loadConfig', data); + mainWindow.webContents.send('loadConfig', data); } const menu = [ @@ -129,13 +133,13 @@ const menu = [ submenu: [ { label: 'My Location', - click() { window.webContents.send('setMapToUserLocation'); }, + click() { mainWindow.webContents.send('setMapToUserLocation'); }, }, { type: 'separator' }, ], }, { - role: 'window', + role: 'mainWindow', submenu: [ { role: 'minimize' }, ], @@ -167,13 +171,13 @@ function setLocationMenu() { locationMenu.push({ label, data: { lat, lng, zoom }, - click(menuItem) { window.webContents.send('updateMapLocation', menuItem.data); }, + click(menuItem) { mainWindow.webContents.send('updateMapLocation', menuItem.data); }, }); }); } function createMainWindow() { - window = new BrowserWindow({ + mainWindow = new BrowserWindow({ title: 'NGCP Ground Control Station', icon, show: false, @@ -184,29 +188,25 @@ function createMainWindow() { }); if (isDevelopment) { - window.loadURL(`http://localhost:${process.env.ELECTRON_WEBPACK_WDS_PORT}`); + mainWindow.loadURL(`http://localhost:${process.env.ELECTRON_WEBPACK_WDS_PORT}#main`); } else { - window.loadURL(url.format({ - pathname: path.resolve(__dirname, 'index.html'), - protocol: 'file', - slashes: true, - })); + mainWindow.loadURL(`file:///${__dirname}/index.html#main`); } - window.on('ready-to-show', () => { - window.show(); + mainWindow.on('ready-to-show', () => { + mainWindow.show(); }); - window.on('close', (event) => { + mainWindow.on('close', (event) => { if (!quitting) { event.preventDefault(); - window.hide(); + mainWindow.hide(); if (missionWindow) { - window.webContents.send('setSelectedMission', -1); + mainWindow.webContents.send('setSelectedMission', -1); missionWindow.hide(); } } else { - window = null; + mainWindow = null; } }); } @@ -214,6 +214,7 @@ function createMainWindow() { function createMissionWindow() { missionWindow = new BrowserWindow({ title: 'NGCP Mission User Interface', + icon, show: false, width: WIDTH, minWidth: WIDTH, @@ -222,28 +223,22 @@ function createMissionWindow() { }); if (isDevelopment) { - missionWindow.loadURL(`http://localhost:${process.env.ELECTRON_WEBPACK_WDS_PORT}?route=mission`); + missionWindow.loadURL(`http://localhost:${process.env.ELECTRON_WEBPACK_WDS_PORT}#mission`); } else { - missionWindow.loadURL(url.format({ - pathname: path.resolve(__dirname, 'index.html'), - protocol: 'file', - slashes: true, - })); + missionWindow.loadURL(`file:///${__dirname}/index.html#mission`); } - missionWindow.show(); + missionWindow.setMenu(null); - /* missionWindow.on('ready-to-show', () => { missionWindow.show(); }); - */ missionWindow.on('close', (event) => { if (!quitting) { event.preventDefault(); - // following line allows MissionContainer to update to closed mission window - window.webContents.send('setSelectedMission', -1); + // following line allows MissionContainer to update to closed mission mainWindow + mainWindow.webContents.send('setSelectedMission', -1); missionWindow.hide(); } else { missionWindow = null; @@ -252,10 +247,10 @@ function createMissionWindow() { } function showWindow() { - if (!window) { + if (!mainWindow) { createMainWindow(); } else { - window.show(); + mainWindow.show(); } } @@ -282,21 +277,26 @@ function createMenu() { if (process.platform === 'darwin') { menu.unshift(darwinMenu); } else { - tray = new Tray(icon); - tray.setContextMenu(Menu.buildFromTemplate(trayMenu)); - tray.on('click', () => { showWindow(); }); - menu.push(quitRole); } Menu.setApplicationMenu(Menu.buildFromTemplate(menu)); } +function createTray() { + tray = new Tray(icon); + + tray.setContextMenu(Menu.buildFromTemplate(trayMenu)); + + tray.on('click', () => { showWindow(); }); +} + app.on('activate', showWindow); app.on('ready', () => { showWindow(); createMenu(); + createTray(); }); app.on('before-quit', () => { @@ -308,7 +308,7 @@ ipcMain.on('post', (event, notification, data) => { showMissionWindow(); } - window.webContents.send(notification, data); + mainWindow.webContents.send(notification, data); if (missionWindow) { missionWindow.webContents.send(notification, data); } diff --git a/src/renderer/fixtures/index.js b/src/renderer/fixtures/index.js index fe596687..e99fcd2b 100644 --- a/src/renderer/fixtures/index.js +++ b/src/renderer/fixtures/index.js @@ -6,5 +6,10 @@ * errors from happening in the first place. */ -import './updateMessagesFixtures'; -import './updateVehiclesFixtures'; +import * as updateMessages from './updateMessagesFixtures'; +import * as updateVehicles from './updateVehiclesFixtures'; + +setInterval(() => { + updateMessages.sendFixtures(); + updateVehicles.sendFixtures(); +}, 1000); diff --git a/src/renderer/fixtures/updateMessagesFixtures.js b/src/renderer/fixtures/updateMessagesFixtures.js index 9cac1058..8ac98393 100644 --- a/src/renderer/fixtures/updateMessagesFixtures.js +++ b/src/renderer/fixtures/updateMessagesFixtures.js @@ -22,7 +22,9 @@ const fixtures = [ }, ]; -setInterval(() => { +export function sendFixtures() { const fixture = fixtures[Math.floor(Math.random() * 5)]; ipcRenderer.send('post', 'updateMessages', [fixture]); -}, 1000); +} + +export default { sendFixtures }; diff --git a/src/renderer/fixtures/updateVehiclesFixtures.js b/src/renderer/fixtures/updateVehiclesFixtures.js index 0a32322b..99474915 100644 --- a/src/renderer/fixtures/updateVehiclesFixtures.js +++ b/src/renderer/fixtures/updateVehiclesFixtures.js @@ -21,7 +21,7 @@ let fixtures = [ const status = Object.keys(vehicleStatuses); -setInterval(() => { +export function sendFixtures() { const newFixtures = fixtures.map(fixture => ({ ...fixture, // lat: fixture.lat + (Math.random() / 5000) - 0.0001, @@ -31,4 +31,6 @@ setInterval(() => { fixtures = newFixtures; ipcRenderer.send('post', 'updateVehicles', fixtures); -}, 1000); +} + +export default { sendFixtures }; diff --git a/src/renderer/index.js b/src/renderer/index.js index f0e80aab..abdbefc0 100644 --- a/src/renderer/index.js +++ b/src/renderer/index.js @@ -17,11 +17,10 @@ import './index.css'; const isDevelopment = process.env.NODE_ENV !== 'production'; -function Router({ routes }) { - const route = new URLSearchParams(window.location.search).get('route') || 'default'; - - return routes[route] || routes.default; -} +const windows = { + '#main': MainWindow, + '#mission': MissionWindow, +}; class Index extends Component { constructor(props) { @@ -47,14 +46,9 @@ class Index extends Component { render() { const { theme } = this.state; - return ( - , - mission: , - }} - /> - ); + const Window = windows[window.location.hash]; + + return ; } } diff --git a/src/renderer/mainWindow/map/control/Control.jsx b/src/renderer/mainWindow/map/control/Control.jsx index 4a27f491..4a812ecb 100644 --- a/src/renderer/mainWindow/map/control/Control.jsx +++ b/src/renderer/mainWindow/map/control/Control.jsx @@ -26,7 +26,7 @@ export default class Control extends Component { Date: Sat, 16 Mar 2019 20:52:51 -0700 Subject: [PATCH 007/152] Minimal changes Finding issues with geolocation, will fix --- src/main/index.js | 48 +++++++++++++++++-- src/renderer/fixtures/index.js | 8 ++-- .../fixtures/updateMessagesFixtures.js | 18 +++---- .../fixtures/updateVehiclesFixtures.js | 6 +-- src/renderer/index.js | 28 +++++++---- 5 files changed, 74 insertions(+), 34 deletions(-) diff --git a/src/main/index.js b/src/main/index.js index 6d28adec..34f1cf02 100644 --- a/src/main/index.js +++ b/src/main/index.js @@ -6,6 +6,8 @@ import moment from 'moment'; import { images, locations } from '../../resources/index'; +// This key is required to enable geolocation in the application. +// Others cannot use this key outside of geolocation access so no need to hide it. process.env.GOOGLE_API_KEY = 'AIzaSyB1gepR_EONqgEcxuADmEZjizTuOU_cfnU'; const FILTER = { name: 'GCS Configuration', extensions: ['json'] }; @@ -17,6 +19,7 @@ const isDevelopment = process.env.NODE_ENV !== 'production'; // todo: put icon tray back to macOS but resize it so that its not huge on macOS's menu const icon = nativeImage.createFromDataURL(images.icon); +// Variable to keep track when the app will quit, which is different from hiding the app. let quitting = false; // References to window objects @@ -26,6 +29,7 @@ let missionWindow; // Tray object let tray; +// Role added to menus to allow the user to quit the app. Shortcut is Ctrl/Cmd + Q. const quitRole = { label: 'Quit', accelerator: 'CommandOrControl+Q', @@ -35,6 +39,7 @@ const quitRole = { }, }; +// Menu prepended to menu if application is running on a Darwin-based OS. const darwinMenu = { label: 'NGCP Ground Control System', submenu: [ @@ -53,39 +58,60 @@ const darwinMenu = { ], }; +/** + * Runs when the user wants to save a configuration of the GCS. + * The configuration currently includes the map location loaded. + */ function saveConfig() { const fileName = moment().format('[GCS Configuration] YYYY-MM-DD [at] h.mm.ss A'); + + // Loads a window that allows the user to choose the file path for the file to be saved. const filePath = dialog.showSaveDialog(mainWindow, { title: 'Save Configuration', filters: [FILTER], defaultPath: `./${fileName}.${FILTER.extensions[0]}`, }); + // Returns if the user chooses to close the window instead of choosing a file path. if (!filePath) return; + // Loads data up with information returned from main and mission windows. const data = {}; mainWindow.webContents.send('saveConfig', { filePath, data, }); + missionWindow.webContents.send('saveConfig', { + filePath, + data, + }); } +/** + * Runs when the user wants to load a configuration of the GCS. + * The configuration currently includes the map location loaded. + */ function loadConfig() { + // Loads a window that allows the user to choose the filePath of the file to be loaded. const filePaths = dialog.showOpenDialog(mainWindow, { title: 'Open Configuration', filters: [FILTER], properties: ['openFile', 'createDirectory'], }); + // Returns if the user chooses to close the window instead of choosing a file path. if (!filePaths || filePaths.length === 0) return; const data = JSON.parse(fs.readFileSync(filePaths[0]), 'utf8'); if (!data) return; + // Loads parsed data into main and mission windows. mainWindow.webContents.send('loadConfig', data); + missionWindow.webContents.send('loadConfig', data); } +// Menu displayed on main window. const menu = [ { label: 'File', @@ -155,6 +181,10 @@ const menu = [ }, ]; +/** + * Adds a list of locations on the menu to allow user to pan to specific location in the map. + * The list of locations comes from ../../resources/locations.json. + */ function setLocationMenu() { const locationMenu = menu.find(m => m.label === 'Locations').submenu; @@ -176,6 +206,9 @@ function setLocationMenu() { }); } +/** + * Creates the main window. This window's hash is #main. + */ function createMainWindow() { mainWindow = new BrowserWindow({ title: 'NGCP Ground Control Station', @@ -211,6 +244,11 @@ function createMainWindow() { }); } +/** + * Creates the mission window. This window's hash is #mission. + * Does not show up once app is loaded (will be hidden) and is shown only when it is opened from + * the main window. + */ function createMissionWindow() { missionWindow = new BrowserWindow({ title: 'NGCP Mission User Interface', @@ -230,10 +268,6 @@ function createMissionWindow() { missionWindow.setMenu(null); - missionWindow.on('ready-to-show', () => { - missionWindow.show(); - }); - missionWindow.on('close', (event) => { if (!quitting) { event.preventDefault(); @@ -262,6 +296,9 @@ function showMissionWindow() { } } +/** + * Small menu displayed on the bottom-right corner of windows, or upper-right corner of macOS. + */ const trayMenu = [ { label: 'NGCP Ground Control Station', @@ -294,7 +331,8 @@ function createTray() { app.on('activate', showWindow); app.on('ready', () => { - showWindow(); + createMainWindow(); + createMissionWindow(); createMenu(); createTray(); }); diff --git a/src/renderer/fixtures/index.js b/src/renderer/fixtures/index.js index e99fcd2b..655d7624 100644 --- a/src/renderer/fixtures/index.js +++ b/src/renderer/fixtures/index.js @@ -6,10 +6,10 @@ * errors from happening in the first place. */ -import * as updateMessages from './updateMessagesFixtures'; -import * as updateVehicles from './updateVehiclesFixtures'; +import updateMessages from './updateMessagesFixtures'; +import updateVehicles from './updateVehiclesFixtures'; setInterval(() => { - updateMessages.sendFixtures(); - updateVehicles.sendFixtures(); + updateMessages(); + updateVehicles(); }, 1000); diff --git a/src/renderer/fixtures/updateMessagesFixtures.js b/src/renderer/fixtures/updateMessagesFixtures.js index 8ac98393..d72d9cf3 100644 --- a/src/renderer/fixtures/updateMessagesFixtures.js +++ b/src/renderer/fixtures/updateMessagesFixtures.js @@ -1,18 +1,10 @@ import { ipcRenderer } from 'electron'; // eslint-disable-line import/no-extraneous-dependencies const fixtures = [ - { - type: 'failure', - message: 'swb fnijnfineifnioen fineig rejnlj qvjevefijn eiofqoifn jnl,a dnv kjveqjnffn jalvlkavhuevenfije eve ve j fjnfuiefn ijcads fhuf a', - }, { type: 'failure', message: 'Test failure message', }, - { - type: 'success', - message: 'zxvdfn uvoewnfnekjnan oieunqo nfrcvdfsvnad dfionfkldasklaj nio niqdnc djacnkl adsniojnjkla dvlkjvienvio anklvdnkven inkv adnklv ndanvoi', - }, { type: 'success', message: 'Test success message', @@ -20,11 +12,13 @@ const fixtures = [ { message: 'Random message', }, + { + type: 'progress', + message: 'Test progress message', + }, ]; -export function sendFixtures() { - const fixture = fixtures[Math.floor(Math.random() * 5)]; +export default function updateMessages() { + const fixture = fixtures[Math.floor(Math.random() * fixtures.length)]; ipcRenderer.send('post', 'updateMessages', [fixture]); } - -export default { sendFixtures }; diff --git a/src/renderer/fixtures/updateVehiclesFixtures.js b/src/renderer/fixtures/updateVehiclesFixtures.js index 99474915..22d049e7 100644 --- a/src/renderer/fixtures/updateVehiclesFixtures.js +++ b/src/renderer/fixtures/updateVehiclesFixtures.js @@ -21,10 +21,10 @@ let fixtures = [ const status = Object.keys(vehicleStatuses); -export function sendFixtures() { +export default function updateVehicles() { const newFixtures = fixtures.map(fixture => ({ ...fixture, - // lat: fixture.lat + (Math.random() / 5000) - 0.0001, + lat: fixture.lat + (Math.random() / 5000) - 0.0001, lng: fixture.lng + (Math.random() / 5000) - 0.0001, status: status[Math.floor(Math.random() * status.length)], })); @@ -32,5 +32,3 @@ export function sendFixtures() { fixtures = newFixtures; ipcRenderer.send('post', 'updateVehicles', fixtures); } - -export default { sendFixtures }; diff --git a/src/renderer/index.js b/src/renderer/index.js index abdbefc0..3e32fb10 100644 --- a/src/renderer/index.js +++ b/src/renderer/index.js @@ -4,8 +4,6 @@ import { ipcRenderer } from 'electron'; // eslint-disable-line import/no-extrane import React, { Component } from 'react'; import ReactDOM from 'react-dom'; -import { fixtures, geolocation } from '../../resources/index'; - import MainWindow from './mainWindow/MainWindow'; import MissionWindow from './missionWindow/MissionWindow'; @@ -15,6 +13,8 @@ import 'react-virtualized/styles.css'; import './global.css'; import './index.css'; +import { fixtures, geolocation } from '../../resources/index'; + const isDevelopment = process.env.NODE_ENV !== 'production'; const windows = { @@ -22,6 +22,22 @@ const windows = { '#mission': MissionWindow, }; +function runGeolocationAndFixtures() { + // Forces this function to only run once, when it is loaded through the main window. + // Running this function more than once (through both main and mission windows) causes + // fixtures to be sent twice a second, as well as the setMapToUserLocation command, + // and we want to prevent this. + if (window.location.hash !== '#main') return; + + if (geolocation) { + ipcRenderer.send('post', 'setMapToUserLocation'); + } + + if (isDevelopment && fixtures) { + require('./fixtures/index'); // eslint-disable-line global-require + } +} + class Index extends Component { constructor(props) { super(props); @@ -52,10 +68,4 @@ class Index extends Component { } } -ReactDOM.render(, document.getElementById('app'), () => { - if (geolocation) ipcRenderer.send('post', 'setMapToUserLocation'); - - if (isDevelopment && fixtures) { - require('./fixtures/index.js'); // eslint-disable-line global-require - } -}); +ReactDOM.render(, document.getElementById('app'), runGeolocationAndFixtures); From a5d245601a72e13100c47ff8819d2c0b5de7cf2b Mon Sep 17 00:00:00 2001 From: Paul Matthew Barrameda Date: Sat, 16 Mar 2019 21:07:16 -0700 Subject: [PATCH 008/152] Electron 4 does not support geolocation, reverted to Electron 3 See the following thread: https://github.com/electron/electron/issues/16139 This issue is fixed in the following thread, but wont be released until Electron 5: https://github.com/electron/electron/pull/15618 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 5676214c..e0d2ffab 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "@babel/preset-react": "^7.0.0", "babel-eslint": "^10.0.1", "dotenv-webpack": "^1.7.0", - "electron": "^4.1.0", + "electron": "^3.0.2", "electron-builder": "^20.39.0", "electron-webpack": "^2.6.2", "eslint": "^5.15.2", From 7f065343b86fc129e2506dbe6ccce4bbd3bb8f37 Mon Sep 17 00:00:00 2001 From: Paul Matthew Barrameda Date: Sun, 17 Mar 2019 17:23:15 -0700 Subject: [PATCH 009/152] Finished logic for MissionContainer MissionContainer has the logic and list of missions that the MissionWindow will be using. Any information (mission has started, paused, completed, etc) in the MissionWindow will be retrieved from the MissionContainer --- .eslintrc.json | 1 + src/main/index.js | 2 +- src/renderer/index.css | 5 + .../mainWindow/mission/MissionContainer.jsx | 180 +++++++++++++++--- src/renderer/missionWindow/MissionWindow.jsx | 55 +++++- .../deliverTarget/DeliverTarget.jsx | 21 ++ .../missionWindow/isrSearch/ISRSearch.jsx | 21 ++ .../missionWindow/payloadDrop/PayloadDrop.jsx | 21 ++ .../retrieveTarget/RetrieveTarget.jsx | 21 ++ src/util/util.js | 16 +- 10 files changed, 307 insertions(+), 36 deletions(-) create mode 100644 src/renderer/missionWindow/deliverTarget/DeliverTarget.jsx create mode 100644 src/renderer/missionWindow/isrSearch/ISRSearch.jsx create mode 100644 src/renderer/missionWindow/payloadDrop/PayloadDrop.jsx create mode 100644 src/renderer/missionWindow/retrieveTarget/RetrieveTarget.jsx diff --git a/.eslintrc.json b/.eslintrc.json index dab00bdf..57b1105c 100755 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -7,6 +7,7 @@ "extends": "airbnb", "rules": { "linebreak-style": "off", + "multiline-comment-style": "error", "import/no-commonjs": "error" } } diff --git a/src/main/index.js b/src/main/index.js index 34f1cf02..b35ed4d9 100644 --- a/src/main/index.js +++ b/src/main/index.js @@ -16,7 +16,7 @@ const HEIGHT = 576; const isDevelopment = process.env.NODE_ENV !== 'production'; -// todo: put icon tray back to macOS but resize it so that its not huge on macOS's menu +// TODO: put icon tray back to macOS but resize it so that its not huge on macOS's menu const icon = nativeImage.createFromDataURL(images.icon); // Variable to keep track when the app will quit, which is different from hiding the app. diff --git a/src/renderer/index.css b/src/renderer/index.css index 89eb57aa..6284ac12 100644 --- a/src/renderer/index.css +++ b/src/renderer/index.css @@ -28,6 +28,11 @@ body, grid-area: vehicle; } +.missionWrapper { + height: 100%; + width: 100%; +} + /* Light theme */ .gridWrapper { diff --git a/src/renderer/mainWindow/mission/MissionContainer.jsx b/src/renderer/mainWindow/mission/MissionContainer.jsx index 432f225a..c6daf1b9 100644 --- a/src/renderer/mainWindow/mission/MissionContainer.jsx +++ b/src/renderer/mainWindow/mission/MissionContainer.jsx @@ -1,3 +1,12 @@ +/* + * The mission container will be in charge of managing which missions are not started, started, + * and completed. + * Any other code that wants to interact with missions must go through the mission container. + * For example, if the mission window wants to know which missions are not done or not done, + * the mission + * container can provide this information. + */ + import { ipcRenderer } from 'electron'; // eslint-disable-line import/no-extraneous-dependencies import PropTypes from 'prop-types'; import React, { Component } from 'react'; @@ -10,22 +19,30 @@ const propTypes = { theme: PropTypes.oneOf(['light', 'dark']).isRequired, }; -let started = false; +/* + * Variable is -1 if no mission is running, otherwise it's the index of the mission in missions. + * If a mission is paused, it will still be the index of that mission. + */ +let startedMissionIndex = -1; const statusTypes = { notStarted: { + name: 'notStarted', message: 'Not started', type: 'failure', }, - open: { - message: 'Currently open', - type: 'progress', - }, started: { + name: 'started', message: 'Started', type: 'progress', }, + paused: { + name: 'paused', + message: 'Paused', + type: 'progress', + }, finished: { + name: 'finished', message: 'Finished', type: 'success', }, @@ -36,69 +53,126 @@ export default class MissionContainer extends Component { super(props); this.state = { + /* + * Index of the mission that is currently opened in mission window. + * Value of -1 means that no mission is currently opened. + */ openedMissionIndex: -1, + + /* + * We have four missions in the mission state, with each mission having a certain + * number of jobs required to accomplish that mission. + * + * The name field allows the mission window to determine which layout to load for which + * mission. The description and status are for the mission container to render. + * + * 1) isrSearch: UAV will search for the target. The jobs are the following: + * step 1: Assign `takeoff` to UAV. + * step 2: Assign `isrSearch` to UAV. + * step 3: Assign `quickSearch` to VTOL 1. + * step 4: Assign `detailedSearch` to VTOL 2. + * step 5: Assign `land` to UAV. (optional) + * + * 2) payloadDrop: UAV will drop a payload to the target. The jobs are the following: + * step 1: Assign `takeoff` to UAV. (not needed if UAV did not land from isrSearch) + * step 2: Assign `payloadDrop` to UAV. + * step 3: Assign `land` to UAV. + * + * 3) retrieveTarget: UGV/UUV will go and retrieve the target. The jobs are the following: + * step 1: Assign `retrieveTarget` to UGV/UUV. + * + * 4) deliverTarget: UGV/UUV will go and deliver the target. The jobs are the following: + * step 1: Assign `deliverTarget` to UGV/UUV. + */ missions: [ { - description: 'Find targets', + name: 'isrSearch', + description: 'Find target', status: statusTypes.notStarted, }, { - description: 'Deliver payload to targets', + name: 'payloadDrop', + description: 'Deliver payload to target', status: statusTypes.notStarted, }, { - description: 'Retreive targets', + name: 'retrieveTarget', + description: 'Retreive target', status: statusTypes.notStarted, }, { - description: 'Return to home base', + name: 'deliverTarget', + description: 'Deliver target', status: statusTypes.notStarted, }, ], }; this.setSelectedMission = this.setSelectedMission.bind(this); + this.updateMission = this.updateMission.bind(this); this.completeMission = this.completeMission.bind(this); } componentDidMount() { ipcRenderer.on('setSelectedMission', (event, index) => this.setSelectedMission(index)); - ipcRenderer.on('startMission', () => { started = true; }); - ipcRenderer.on('stopMission', () => { started = false; }); - ipcRenderer.on('completeMission', (event, index) => this.completeMission(index)); + ipcRenderer.on('startMission', () => this.updateMission('started')); + ipcRenderer.on('stopMission', () => this.updateMission('notStarted')); + ipcRenderer.on('pauseMission', () => this.updateMission('paused')); + ipcRenderer.on('resumeMission', () => this.updateMission('started')); + + ipcRenderer.on('completeMission', this.completeMission); } /** - * Select mission based on index and open corresponding mission window - * @param {index} number 0 <= index < this.state.missions.length, -1 means close mission window + * Selects mission to show on mission window by its index in the missions state. + * Also allows the mission container to update its display on which mission is being shown on the + * mission window. + * + * In other words, function will command the mission window to change which mission to display, + * as well as update the mission container to show which mission is currently opened. + * + * @param {index} number index that points to mission to open, -1 means mission window was closed. */ setSelectedMission(index) { const { missions, openedMissionIndex } = this.state; - // exit function for any finished missions - if (index !== -1 && missions[index].status === statusTypes.finished) return; - - // opens mission window for specified mission + // Opens mission window for specified mission. if (index !== -1) { - ipcRenderer.send('post', 'showMissionWindow', index); + ipcRenderer.send('post', 'showMissionWindow', missions[index]); } - // exit function if selected mission is selected again + /* + * Updates the mission container to reflect opened/closed mission window. + * Exits function if the same mission was chosen. + */ if (openedMissionIndex === index) return; - // update mission status to new selected mission const newMissions = missions; + /* + * Set previously opened mission to its default closed state. + * The mission container will not display opened to this mission. + * (eg. "Not started (Open)" => "Not started") + */ if (openedMissionIndex !== -1) { - newMissions[openedMissionIndex].status = statusTypes.notStarted; + const { status } = newMissions[openedMissionIndex]; + newMissions[openedMissionIndex].status = statusTypes[status.name]; } + + /* + * Set new opened mission to opened. If the mission window is being closed, + * then this if statement will not execute. + * The mission container will display opened to this mission. + * (eg. "Not started" => "Not started (Open)") + */ if (index !== -1) { - newMissions[index].status = statusTypes.open; - } else if (started) { - newMissions[openedMissionIndex].status = statusTypes.started; - } else { - newMissions[openedMissionIndex].status = statusTypes.notStarted; + const { status } = newMissions[index]; + + newMissions[index].status = { + ...status, + message: `${statusTypes[status.name].message} (Open)`, + }; } this.setState({ @@ -107,11 +181,52 @@ export default class MissionContainer extends Component { }); } - completeMission(index) { - const { missions } = this.state; + /** + * Updates mission description in mission container whenever a start/stop/pause/resume + * mission notification is sent. + * + * @param {string} name Consists of notStarted/started/paused. See the componentDidMount + * function to see which name corresponds with which notification. + */ + updateMission(name) { + const { missions, openedMissionIndex } = this.state; const newMissions = missions; - newMissions[index].status = statusTypes.finished; + /* + * All four actions can only be done through the mission window, + * so we can assume that the mission is currently open, hence we can add (Open) to the + * end of the mission description. + */ + newMissions[openedMissionIndex].status = { + ...statusTypes[name], + message: `${statusTypes[name].message} (Open)`, + }; + + if (name === 'started') { + startedMissionIndex = openedMissionIndex; + } else if (name === 'notStarted') { + startedMissionIndex = -1; + } + + this.setState({ missions: newMissions }); + } + + completeMission() { + const { missions, openedMissionIndex } = this.state; + const newMissions = missions; + + // It is possible that the mission window for started mission is currently open or closed. + if (openedMissionIndex === startedMissionIndex) { + newMissions[startedMissionIndex].status = { + ...statusTypes.finished, + message: `${statusTypes.finished.message} (Open)`, + }; + } else { + newMissions[startedMissionIndex].status = statusTypes.finished; + } + + startedMissionIndex = -1; + this.setState({ missions: newMissions }); } @@ -121,7 +236,10 @@ export default class MissionContainer extends Component { return (
- +
); } diff --git a/src/renderer/missionWindow/MissionWindow.jsx b/src/renderer/missionWindow/MissionWindow.jsx index 6de55383..33a36819 100644 --- a/src/renderer/missionWindow/MissionWindow.jsx +++ b/src/renderer/missionWindow/MissionWindow.jsx @@ -1,5 +1,54 @@ -import React from 'react'; +import { ipcRenderer } from 'electron'; // eslint-disable-line import/no-extraneous-dependencies +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; -export default function MissionWindow() { - return

Waassup!

; +import DeliverTarget from './deliverTarget/DeliverTarget'; +import ISRSearch from './isrSearch/ISRSearch'; +import PayloadDrop from './payloadDrop/PayloadDrop'; +import RetrieveTarget from './retrieveTarget/RetrieveTarget'; + +const layouts = { + deliverTarget: DeliverTarget, + isrSearch: ISRSearch, + payloadDrop: PayloadDrop, + retrieveTarget: RetrieveTarget, +}; + +const propTypes = { + theme: PropTypes.oneOf(['light', 'dark']).isRequired, +}; + +function BlankMission() { + return
; } + +export default class MissionWindow extends Component { + constructor(props) { + super(props); + + this.state = { + openedMission: '', + }; + } + + componentDidMount() { + ipcRenderer.on('showMissionWindow', (event, mission) => { + this.setState({ openedMission: mission.name }); + }); + } + + render() { + const { theme } = this.props; + const { openedMission } = this.state; + + const Layout = layouts[openedMission] || BlankMission; + + return ( +
+ +
+ ); + } +} + +MissionWindow.propTypes = propTypes; diff --git a/src/renderer/missionWindow/deliverTarget/DeliverTarget.jsx b/src/renderer/missionWindow/deliverTarget/DeliverTarget.jsx new file mode 100644 index 00000000..3c132284 --- /dev/null +++ b/src/renderer/missionWindow/deliverTarget/DeliverTarget.jsx @@ -0,0 +1,21 @@ +import PropTypes from 'prop-types'; +import React from 'react'; + +import { startMission, stopMission, completeMission } from '../../../util/util'; + +const propTypes = { + theme: PropTypes.oneOf(['light', 'dark']).isRequired, +}; + +export default function DeliverTarget({ theme }) { + return ( +
+

Deliver Target

+ + + +
+ ); +} + +DeliverTarget.propTypes = propTypes; diff --git a/src/renderer/missionWindow/isrSearch/ISRSearch.jsx b/src/renderer/missionWindow/isrSearch/ISRSearch.jsx new file mode 100644 index 00000000..619d8a9b --- /dev/null +++ b/src/renderer/missionWindow/isrSearch/ISRSearch.jsx @@ -0,0 +1,21 @@ +import PropTypes from 'prop-types'; +import React from 'react'; + +import { startMission, stopMission, completeMission } from '../../../util/util'; + +const propTypes = { + theme: PropTypes.oneOf(['light', 'dark']).isRequired, +}; + +export default function ISRSearch({ theme }) { + return ( +
+

ISR Search

+ + + +
+ ); +} + +ISRSearch.propTypes = propTypes; diff --git a/src/renderer/missionWindow/payloadDrop/PayloadDrop.jsx b/src/renderer/missionWindow/payloadDrop/PayloadDrop.jsx new file mode 100644 index 00000000..ba38688b --- /dev/null +++ b/src/renderer/missionWindow/payloadDrop/PayloadDrop.jsx @@ -0,0 +1,21 @@ +import PropTypes from 'prop-types'; +import React from 'react'; + +import { startMission, stopMission, completeMission } from '../../../util/util'; + +const propTypes = { + theme: PropTypes.oneOf(['light', 'dark']).isRequired, +}; + +export default function PayloadDrop({ theme }) { + return ( +
+

Payload Drop

+ + + +
+ ); +} + +PayloadDrop.propTypes = propTypes; diff --git a/src/renderer/missionWindow/retrieveTarget/RetrieveTarget.jsx b/src/renderer/missionWindow/retrieveTarget/RetrieveTarget.jsx new file mode 100644 index 00000000..236b836b --- /dev/null +++ b/src/renderer/missionWindow/retrieveTarget/RetrieveTarget.jsx @@ -0,0 +1,21 @@ +import PropTypes from 'prop-types'; +import React from 'react'; + +import { startMission, stopMission, completeMission } from '../../../util/util'; + +const propTypes = { + theme: PropTypes.oneOf(['light', 'dark']).isRequired, +}; + +export default function RetrieveTarget({ theme }) { + return ( +
+

Retrieve Target

+ + + +
+ ); +} + +RetrieveTarget.propTypes = propTypes; diff --git a/src/util/util.js b/src/util/util.js index 4d501ed6..9b9a5288 100644 --- a/src/util/util.js +++ b/src/util/util.js @@ -1,3 +1,5 @@ +import { ipcRenderer } from 'electron'; // eslint-disable-line import/no-extraneous-dependencies + import { vehicleInfos, vehicleStatuses, } from '../../resources/index'; @@ -17,6 +19,18 @@ export function updateVehicles(component, vehicles) { component.setState({ vehicles: currentVehicles }); } +export function startMission() { + ipcRenderer.send('post', 'startMission'); +} + +export function stopMission() { + ipcRenderer.send('post', 'stopMission'); +} + +export function completeMission() { + ipcRenderer.send('post', 'completeMission'); +} + export default { - updateVehicles, + updateVehicles, startMission, stopMission, completeMission, }; From 38633c5bbe7febffeada01ec57ef2c8579943988 Mon Sep 17 00:00:00 2001 From: Paul Matthew Barrameda Date: Sun, 17 Mar 2019 17:56:17 -0700 Subject: [PATCH 010/152] Update comments --- src/main/index.js | 6 ++++-- src/renderer/index.js | 10 ++++++---- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/main/index.js b/src/main/index.js index b35ed4d9..f33c32d6 100644 --- a/src/main/index.js +++ b/src/main/index.js @@ -6,8 +6,10 @@ import moment from 'moment'; import { images, locations } from '../../resources/index'; -// This key is required to enable geolocation in the application. -// Others cannot use this key outside of geolocation access so no need to hide it. +/* + * This key is required to enable geolocation in the application. + * Others cannot use this key outside of geolocation access so no need to hide it. + */ process.env.GOOGLE_API_KEY = 'AIzaSyB1gepR_EONqgEcxuADmEZjizTuOU_cfnU'; const FILTER = { name: 'GCS Configuration', extensions: ['json'] }; diff --git a/src/renderer/index.js b/src/renderer/index.js index 3e32fb10..75624a6f 100644 --- a/src/renderer/index.js +++ b/src/renderer/index.js @@ -23,10 +23,12 @@ const windows = { }; function runGeolocationAndFixtures() { - // Forces this function to only run once, when it is loaded through the main window. - // Running this function more than once (through both main and mission windows) causes - // fixtures to be sent twice a second, as well as the setMapToUserLocation command, - // and we want to prevent this. + /* + * Forces this function to only run once, when it is loaded through the main window. + * Running this function more than once (through both main and mission windows) causes + * fixtures to be sent twice a second, as well as the setMapToUserLocation command, + * and we want to prevent this. + */ if (window.location.hash !== '#main') return; if (geolocation) { From 0be384ce36968653a99ce71fbea2195b4d7479ee Mon Sep 17 00:00:00 2001 From: Paul Matthew Barrameda Date: Mon, 18 Mar 2019 16:07:22 -0700 Subject: [PATCH 011/152] Added hex/float converter functions to util --- src/util/util.js | 40 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/src/util/util.js b/src/util/util.js index 9b9a5288..1b983af8 100644 --- a/src/util/util.js +++ b/src/util/util.js @@ -31,6 +31,44 @@ export function completeMission() { ipcRenderer.send('post', 'completeMission'); } +/* eslint-disable no-bitwise */ + +// Bitshifting allows us to switch between hex and float + +export function floatToHexString(float) { + const getHex = i => `00${i.toString(16)}`.slice(-2); + const view = new DataView(new ArrayBuffer(4)); + view.setFloat32(0, float); + return [0, 0, 0, 0].map((_, i) => getHex(view.getUint8(i))).join(''); +} + +export function hexStringToFloat(hexString) { + const int = parseInt(hexString, 16); + if (int > 0 || int < 0) { + const sign = (int >>> 31) ? -1 : 1; + let exp = ((int >>> 23) & 0xff) - 127; + const mantissa = ((int & 0x7fffff) + 0x800000).toString(2); + let float = 0; + + for (let i = 0; i < mantissa.length; i += 1, exp -= 1) { + if (parseInt(mantissa[i], 10)) { + float += 2 ** exp; + } + } + + return float * sign; + } + + return 0; +} + +/* eslint-enable no-bitwise */ + export default { - updateVehicles, startMission, stopMission, completeMission, + updateVehicles, + startMission, + stopMission, + completeMission, + floatToHexString, + hexStringToFloat, }; From 6b55c6f8326524970ed82d3d98d8c4b868dca2bc Mon Sep 17 00:00:00 2001 From: Paul Matthew Barrameda Date: Mon, 18 Mar 2019 19:20:35 -0700 Subject: [PATCH 012/152] Reformatted files --- .env.example | 2 ++ .eslintignore | 2 +- .eslintrc.json | 13 ------------- .eslintrc.yml | 17 +++++++++++++++++ .gitattributes | 2 +- .gitignore | 8 ++++---- .stylelintrc | 10 ---------- .stylelintrc.yml | 16 ++++++++++++++++ .travis.yml | 12 +++++++----- README.md | 9 +++++---- webpack.config.js | 2 ++ 11 files changed, 55 insertions(+), 38 deletions(-) delete mode 100755 .eslintrc.json create mode 100644 .eslintrc.yml delete mode 100644 .stylelintrc create mode 100644 .stylelintrc.yml diff --git a/.env.example b/.env.example index f63fdc4b..eb89b720 100644 --- a/.env.example +++ b/.env.example @@ -1 +1,3 @@ +# Duplicate this file, rename it to .env, and replace your-access-token with your MapBox API token + MAPBOX_TOKEN=your-access-token diff --git a/.eslintignore b/.eslintignore index c16db3d1..da02cdbd 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,2 +1,2 @@ +# Directories for eslint to ignore dist -old-src diff --git a/.eslintrc.json b/.eslintrc.json deleted file mode 100755 index 57b1105c..00000000 --- a/.eslintrc.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "parser": "babel-eslint", - "env": { - "browser": true, - "node": true - }, - "extends": "airbnb", - "rules": { - "linebreak-style": "off", - "multiline-comment-style": "error", - "import/no-commonjs": "error" - } -} diff --git a/.eslintrc.yml b/.eslintrc.yml new file mode 100644 index 00000000..beca84a2 --- /dev/null +++ b/.eslintrc.yml @@ -0,0 +1,17 @@ +# Rules to lint JavaScript + +# Use Babel as we use ES6 code +parser: babel-eslint + +env: + browser: true + node: true + +# Use Airbnb rules to lint code +extends: airbnb + +# Added rules +rules: + linebreak-style: off + multiline-comment-style: error + import/no-commonjs: error diff --git a/.gitattributes b/.gitattributes index 83721108..225586d0 100755 --- a/.gitattributes +++ b/.gitattributes @@ -1,4 +1,4 @@ -# prevent newline problems between macOS and Windows +# Prevent newline problems between different systems * text=auto *.sln text eol=lf diff --git a/.gitignore b/.gitignore index be9997e9..391bca0b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,8 @@ -# files +# Ignore system generated files/folders .DS_Store -.env package-lock.json - -# folders dist node_modules + +# Ignore .env file (should be set per system) +.env diff --git a/.stylelintrc b/.stylelintrc deleted file mode 100644 index 829139d1..00000000 --- a/.stylelintrc +++ /dev/null @@ -1,10 +0,0 @@ -{ - "extends": "stylelint-config-standard", - "ignoreFiles": ["dist/**", "old-src/**", "resources/**"], - "rules": { - "indentation": 2, - "declaration-block-no-duplicate-properties": true, - "no-empty-source": null, - "string-quotes": "single" - } -} diff --git a/.stylelintrc.yml b/.stylelintrc.yml new file mode 100644 index 00000000..4ee578b7 --- /dev/null +++ b/.stylelintrc.yml @@ -0,0 +1,16 @@ +# Rules to lint CSS + +# Use stylelint preset rules to lint +extends: stylelint-config-standard + +# Directories for stylelint to ignore +ignore-files: + - dist/** + +# Added rules +rules: + indentation: 2 + declaration-block-no-duplicate-properties: true + no-empty-source: null + string-quotes: single + max-line-length: 100 diff --git a/.travis.yml b/.travis.yml index afae5906..d7ab1db8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,8 +1,10 @@ -# testing with Travis CI, will run npm test +# Travis CI testing configuration language: node_js + +# Use latest version of Node.js node_js: - - "node" -cache: - directories: - - "node_modules" + - node + +# Cache node_modules after testing +cache: npm diff --git a/README.md b/README.md index 121616be..f64e76e9 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # NGCP Ground Control Station -[![Build Status](https://travis-ci.com/NGCP/GCS.svg?branch=dev-2018)](https://travis-ci.com/NGCP/GCS) +[![Build Status](https://travis-ci.com/NGCP/GCS)](https://travis-ci.com/NGCP/GCS) The [Northrop Grumman Collaboration Project] presents the Ground Control Station. This project's objective is to view and set missions for all autonomous vehicle platforms in the project. @@ -9,11 +9,12 @@ This is based on [Node.js], [React], [Webpack], and [Electron]. ## Getting Started **:pencil2: Setting Things Up** -Open up your command line application and clone this repository +Install [Node.js]. The program will not be able to run without it. + +Open up your command line application and clone this repository: ```sh -# Command is accustomed to dev-2018 branch -git clone -b dev-2018 --single-branch https://github.com/NGCP/GCS.git +git clone https://github.com/NGCP/GCS.git ``` **:scroll: Running the Program** diff --git a/webpack.config.js b/webpack.config.js index 5247797e..61a69286 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -1,5 +1,7 @@ /* eslint-disable import/no-commonjs */ +// Webpack configuration cannot be written with ES6 import/export so we disable no-commonjs. + const Dotenv = require('dotenv-webpack'); module.exports = { From f9d8e1da154bafce47ffd00c885b7ded15088f30 Mon Sep 17 00:00:00 2001 From: Paul Matthew Barrameda Date: Mon, 18 Mar 2019 19:21:33 -0700 Subject: [PATCH 013/152] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f64e76e9..e35f11b8 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # NGCP Ground Control Station -[![Build Status](https://travis-ci.com/NGCP/GCS)](https://travis-ci.com/NGCP/GCS) +[![Build Status](https://travis-ci.com/NGCP/GCS.svg)](https://travis-ci.com/NGCP/GCS) The [Northrop Grumman Collaboration Project] presents the Ground Control Station. This project's objective is to view and set missions for all autonomous vehicle platforms in the project. From 771c8b634e39e82c29e5080dcce612127d0922f8 Mon Sep 17 00:00:00 2001 From: Paul Matthew Barrameda Date: Tue, 19 Mar 2019 01:03:24 -0700 Subject: [PATCH 014/152] Updated badges Travis CI had a small error with badge, needed to specify branch. --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index e35f11b8..dc6a7baa 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ # NGCP Ground Control Station -[![Build Status](https://travis-ci.com/NGCP/GCS.svg)](https://travis-ci.com/NGCP/GCS) +[![Build Status](https://travis-ci.com/NGCP/GCS.svg?branch=dev-2018)](https://travis-ci.com/NGCP/GCS) +[![dependencies Status](https://david-dm.org/NGCP/GCS/status.svg)](https://david-dm.org/NGCP/GCS) +[![devDependencies Status](https://david-dm.org/NGCP/GCS/dev-status.svg)](https://david-dm.org/NGCP/GCS?type=dev) The [Northrop Grumman Collaboration Project] presents the Ground Control Station. This project's objective is to view and set missions for all autonomous vehicle platforms in the project. From 1cde54b6e517c9f85f31c6dd952a2986fe348156 Mon Sep 17 00:00:00 2001 From: Paul Matthew Barrameda Date: Fri, 22 Mar 2019 12:15:09 -0700 Subject: [PATCH 015/152] Added job UI --- .stylelintignore | 2 ++ .stylelintrc.yml | 4 ---- src/renderer/mainWindow/log/log.css | 24 +++++++++++++++++-- src/renderer/missionWindow/MissionWindow.jsx | 8 +++---- .../DeliverTarget.jsx | 0 .../{isrSearch => missions}/ISRSearch.jsx | 0 .../{payloadDrop => missions}/PayloadDrop.jsx | 0 .../RetrieveTarget.jsx | 0 .../missions/jobs/DeliverTarget.jsx | 0 .../missions/jobs/DetailedSearch.jsx | 0 .../missionWindow/missions/jobs/ISRSearch.jsx | 0 .../missionWindow/missions/jobs/Land.jsx | 0 .../missionWindow/missions/jobs/Loiter.jsx | 0 .../missions/jobs/PayloadDrop.jsx | 0 .../missionWindow/missions/jobs/QuickScan.jsx | 0 .../missions/jobs/RetrieveTarget.jsx | 0 .../missionWindow/missions/jobs/Takeoff.jsx | 0 17 files changed, 28 insertions(+), 10 deletions(-) create mode 100644 .stylelintignore rename src/renderer/missionWindow/{deliverTarget => missions}/DeliverTarget.jsx (100%) rename src/renderer/missionWindow/{isrSearch => missions}/ISRSearch.jsx (100%) rename src/renderer/missionWindow/{payloadDrop => missions}/PayloadDrop.jsx (100%) rename src/renderer/missionWindow/{retrieveTarget => missions}/RetrieveTarget.jsx (100%) create mode 100644 src/renderer/missionWindow/missions/jobs/DeliverTarget.jsx create mode 100644 src/renderer/missionWindow/missions/jobs/DetailedSearch.jsx create mode 100644 src/renderer/missionWindow/missions/jobs/ISRSearch.jsx create mode 100644 src/renderer/missionWindow/missions/jobs/Land.jsx create mode 100644 src/renderer/missionWindow/missions/jobs/Loiter.jsx create mode 100644 src/renderer/missionWindow/missions/jobs/PayloadDrop.jsx create mode 100644 src/renderer/missionWindow/missions/jobs/QuickScan.jsx create mode 100644 src/renderer/missionWindow/missions/jobs/RetrieveTarget.jsx create mode 100644 src/renderer/missionWindow/missions/jobs/Takeoff.jsx diff --git a/.stylelintignore b/.stylelintignore new file mode 100644 index 00000000..6336a5fd --- /dev/null +++ b/.stylelintignore @@ -0,0 +1,2 @@ +# Directories for stylelint to ignore +dist diff --git a/.stylelintrc.yml b/.stylelintrc.yml index 4ee578b7..f0263013 100644 --- a/.stylelintrc.yml +++ b/.stylelintrc.yml @@ -3,10 +3,6 @@ # Use stylelint preset rules to lint extends: stylelint-config-standard -# Directories for stylelint to ignore -ignore-files: - - dist/** - # Added rules rules: indentation: 2 diff --git a/src/renderer/mainWindow/log/log.css b/src/renderer/mainWindow/log/log.css index 0f8fc709..58f5ab82 100644 --- a/src/renderer/mainWindow/log/log.css +++ b/src/renderer/mainWindow/log/log.css @@ -8,7 +8,17 @@ .logContainer .messages { background-color: #fff; color: #000; - font-family: Consolas, Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono, Bitstream Vera Sans Mono, Courier New, monospace, serif; + font-family: + Consolas, + Menlo, + Monaco, + Lucida Console, + Liberation Mono, + DejaVu Sans Mono, + Bitstream Vera Sans Mono, + Courier New, + monospace, + serif; font-size: 14px; width: 100%; height: calc(100% - 25px); @@ -23,7 +33,17 @@ .logContainer .messages_dark { background-color: #141414; color: #fff; - font-family: Consolas, Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono, Bitstream Vera Sans Mono, Courier New, monospace, serif; + font-family: + Consolas, + Menlo, + Monaco, + Lucida Console, + Liberation Mono, + DejaVu Sans Mono, + Bitstream Vera Sans Mono, + Courier New, + monospace, + serif; font-size: 14px; width: 100%; height: calc(100% - 25px); diff --git a/src/renderer/missionWindow/MissionWindow.jsx b/src/renderer/missionWindow/MissionWindow.jsx index 33a36819..a6bf54a0 100644 --- a/src/renderer/missionWindow/MissionWindow.jsx +++ b/src/renderer/missionWindow/MissionWindow.jsx @@ -2,10 +2,10 @@ import { ipcRenderer } from 'electron'; // eslint-disable-line import/no-extrane import PropTypes from 'prop-types'; import React, { Component } from 'react'; -import DeliverTarget from './deliverTarget/DeliverTarget'; -import ISRSearch from './isrSearch/ISRSearch'; -import PayloadDrop from './payloadDrop/PayloadDrop'; -import RetrieveTarget from './retrieveTarget/RetrieveTarget'; +import DeliverTarget from './missions/DeliverTarget'; +import ISRSearch from './missions/ISRSearch'; +import PayloadDrop from './missions/PayloadDrop'; +import RetrieveTarget from './missions/RetrieveTarget'; const layouts = { deliverTarget: DeliverTarget, diff --git a/src/renderer/missionWindow/deliverTarget/DeliverTarget.jsx b/src/renderer/missionWindow/missions/DeliverTarget.jsx similarity index 100% rename from src/renderer/missionWindow/deliverTarget/DeliverTarget.jsx rename to src/renderer/missionWindow/missions/DeliverTarget.jsx diff --git a/src/renderer/missionWindow/isrSearch/ISRSearch.jsx b/src/renderer/missionWindow/missions/ISRSearch.jsx similarity index 100% rename from src/renderer/missionWindow/isrSearch/ISRSearch.jsx rename to src/renderer/missionWindow/missions/ISRSearch.jsx diff --git a/src/renderer/missionWindow/payloadDrop/PayloadDrop.jsx b/src/renderer/missionWindow/missions/PayloadDrop.jsx similarity index 100% rename from src/renderer/missionWindow/payloadDrop/PayloadDrop.jsx rename to src/renderer/missionWindow/missions/PayloadDrop.jsx diff --git a/src/renderer/missionWindow/retrieveTarget/RetrieveTarget.jsx b/src/renderer/missionWindow/missions/RetrieveTarget.jsx similarity index 100% rename from src/renderer/missionWindow/retrieveTarget/RetrieveTarget.jsx rename to src/renderer/missionWindow/missions/RetrieveTarget.jsx diff --git a/src/renderer/missionWindow/missions/jobs/DeliverTarget.jsx b/src/renderer/missionWindow/missions/jobs/DeliverTarget.jsx new file mode 100644 index 00000000..e69de29b diff --git a/src/renderer/missionWindow/missions/jobs/DetailedSearch.jsx b/src/renderer/missionWindow/missions/jobs/DetailedSearch.jsx new file mode 100644 index 00000000..e69de29b diff --git a/src/renderer/missionWindow/missions/jobs/ISRSearch.jsx b/src/renderer/missionWindow/missions/jobs/ISRSearch.jsx new file mode 100644 index 00000000..e69de29b diff --git a/src/renderer/missionWindow/missions/jobs/Land.jsx b/src/renderer/missionWindow/missions/jobs/Land.jsx new file mode 100644 index 00000000..e69de29b diff --git a/src/renderer/missionWindow/missions/jobs/Loiter.jsx b/src/renderer/missionWindow/missions/jobs/Loiter.jsx new file mode 100644 index 00000000..e69de29b diff --git a/src/renderer/missionWindow/missions/jobs/PayloadDrop.jsx b/src/renderer/missionWindow/missions/jobs/PayloadDrop.jsx new file mode 100644 index 00000000..e69de29b diff --git a/src/renderer/missionWindow/missions/jobs/QuickScan.jsx b/src/renderer/missionWindow/missions/jobs/QuickScan.jsx new file mode 100644 index 00000000..e69de29b diff --git a/src/renderer/missionWindow/missions/jobs/RetrieveTarget.jsx b/src/renderer/missionWindow/missions/jobs/RetrieveTarget.jsx new file mode 100644 index 00000000..e69de29b diff --git a/src/renderer/missionWindow/missions/jobs/Takeoff.jsx b/src/renderer/missionWindow/missions/jobs/Takeoff.jsx new file mode 100644 index 00000000..e69de29b From 1d12df43cabea63ee3b9ee429e1e96d4f8cb9085 Mon Sep 17 00:00:00 2001 From: Paul Matthew Barrameda Date: Fri, 22 Mar 2019 21:23:53 -0700 Subject: [PATCH 016/152] Version bump Currently has vulnerabilities, will be fixed in https://github.com/svg/svgo/pull/1089 --- package.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index e0d2ffab..4c0155f8 100644 --- a/package.json +++ b/package.json @@ -3,9 +3,9 @@ "version": "0.5.2", "description": "Ground Control Station for autonomous vehicle platforms in NGCP", "scripts": { + "postinstall": "electron-builder install-app-deps", "test": "npm run linttest", "linttest": "eslint --ext .js,.jsx \".\" && stylelint \"**/*.css\"", - "postinstall": "electron-builder install-app-deps", "lint": "eslint --fix --ext .js,.jsx \".\" && stylelint \"**/*.css\" --fix", "start": "electron-webpack dev", "build": "electron-webpack && electron-builder" @@ -15,8 +15,8 @@ "moment": "^2.24.0", "pouchdb": "^7.0.0", "prop-types": "^15.7.2", - "react": "^16.8.4", - "react-dom": "^16.8.4", + "react": "^16.8.5", + "react-dom": "^16.8.5", "react-leaflet": "^2.2.1", "react-leaflet-control": "^2.1.1", "react-virtualized": "^9.21.0", @@ -31,7 +31,7 @@ "electron": "^3.0.2", "electron-builder": "^20.39.0", "electron-webpack": "^2.6.2", - "eslint": "^5.15.2", + "eslint": "^5.15.3", "eslint-config-airbnb": "^17.1.0", "eslint-plugin-import": "^2.16.0", "eslint-plugin-jsx-a11y": "^6.2.1", From 6b1f2632bc4232c2a8e76acf71ff5ad8e7094bad Mon Sep 17 00:00:00 2001 From: Paul Matthew Barrameda Date: Fri, 22 Mar 2019 23:57:43 -0700 Subject: [PATCH 017/152] Updated CSS structure Note: when importing files (import X from './X'), and there are two files X.js and x.css, the program will import the CSS file (when intended to import the JS file). This will cause an error in the program. The solution is to not name the CSS file as JS files. Took a while to figure out since all code looked normal. Ill get working on getting structure up (will do Takeoff as an example) --- src/renderer/global.css | 118 --------------- src/renderer/index.css | 142 ++++++++++++------ src/renderer/index.js | 1 - src/renderer/mainWindow/MainWindow.jsx | 4 +- src/renderer/mainWindow/main.css | 61 ++++++++ src/renderer/missionWindow/MissionWindow.jsx | 4 +- src/renderer/missionWindow/mission.css | 23 +++ .../missionWindow/missions/DeliverTarget.jsx | 11 +- .../missionWindow/missions/ISRSearch.jsx | 11 +- .../missionWindow/missions/PayloadDrop.jsx | 11 +- .../missionWindow/missions/RetrieveTarget.jsx | 9 +- webpack.config.js | 2 +- 12 files changed, 197 insertions(+), 200 deletions(-) delete mode 100644 src/renderer/global.css create mode 100644 src/renderer/mainWindow/main.css create mode 100644 src/renderer/missionWindow/mission.css diff --git a/src/renderer/global.css b/src/renderer/global.css deleted file mode 100644 index 013cf0fd..00000000 --- a/src/renderer/global.css +++ /dev/null @@ -1,118 +0,0 @@ -/* Defined styles for the whole application */ - -html { - font-family: 'Helvetica Neue', Arial, Helvetica, sans-serif; - user-select: none; -} - -::-webkit-scrollbar { - width: 0; - background: transparent; -} - -div:focus { - outline: none; -} - -.success { - color: #2e7d32; -} - -.failure { - color: #c62828; -} - -.progress { - color: #a7ad00; -} - -.ReactVirtualized__Table__row:hover .success, -.ReactVirtualized__Table__row_dark:hover .success { - color: #43a047; -} - -.ReactVirtualized__Table__row:active .success { - color: #76d275; -} - -.ReactVirtualized__Table__row:hover .failure, -.ReactVirtualized__Table__row_dark:hover .failure { - color: #f44336; -} - -.ReactVirtualized__Table__row:active .failure { - color: #ff7961; -} - -.ReactVirtualized__Table__row:hover .progress, -.ReactVirtualized__Table__row_dark:hover .progress { - color: #bfc600; -} - -.ReactVirtualized__Table__row:active .progress { - color: #d3da00; -} - -/* Light Theme */ - -.ReactVirtualized__Table { - color: #4a4241; -} - -.ReactVirtualized__Table__headerRow { - color: #000; - background-color: rgb(0, 0, 0, 0.15); - text-transform: none; -} - -.ReactVirtualized__Table__row { - width: 100%; - border-top: 2px solid #626262; - cursor: pointer; -} - -.ReactVirtualized__Table__row:first-child { - border-top: 0; -} - -.ReactVirtualized__Table__row:hover { - color: #211e1d; - background-color: rgb(0, 0, 0, 0.05); -} - -.ReactVirtualized__Table__row:active { - color: #000; - background-color: rgb(0, 0, 0, 0.1); -} - -/* Dark Theme */ - -.ReactVirtualized__Table_dark { - color: #bdbdbd; -} - -.ReactVirtualized__Table__headerRow_dark { - color: #fff; - background-color: rgb(255, 255, 255, 0.15); - text-transform: none; -} - -.ReactVirtualized__Table__row_dark { - width: 100%; - border-top: 2px solid #626262; - cursor: pointer; -} - -.ReactVirtualized__Table__row_dark:first-child { - border-top: 0; -} - -.ReactVirtualized__Table__row_dark:hover { - color: #e9e9e9; - background-color: rgb(255, 255, 255, 0.025); -} - -.ReactVirtualized__Table__row_dark:active { - color: #fff; - background-color: rgb(255, 255, 255, 0.05); -} diff --git a/src/renderer/index.css b/src/renderer/index.css index 6284ac12..1ad36cf1 100644 --- a/src/renderer/index.css +++ b/src/renderer/index.css @@ -1,4 +1,4 @@ -/* Defined styles for index.js */ +/* Defined styles for the whole application */ html, body, @@ -9,70 +9,126 @@ body, overflow-y: hidden; padding: 0; width: 100%; - font-size: 15px; } -.logContainer { - grid-area: log; +html { + font-family: 'Helvetica Neue', Arial, Helvetica, sans-serif; + user-select: none; } -.mapContainer { - grid-area: map; +::-webkit-scrollbar { + width: 0; + background: transparent; } -.missionContainer { - grid-area: mission; +div:focus { + outline: none; } -.vehicleContainer { - grid-area: vehicle; +.success { + color: #2e7d32; } -.missionWrapper { - height: 100%; - width: 100%; +.failure { + color: #c62828; +} + +.progress { + color: #a7ad00; +} + +.ReactVirtualized__Table, +.ReactVirtualized__Tabl_dark { + font-size: 1em; +} + +.ReactVirtualized__Table__row:hover .success, +.ReactVirtualized__Table__row_dark:hover .success { + color: #43a047; +} + +.ReactVirtualized__Table__row:active .success { + color: #76d275; } -/* Light theme */ +.ReactVirtualized__Table__row:hover .failure, +.ReactVirtualized__Table__row_dark:hover .failure { + color: #f44336; +} + +.ReactVirtualized__Table__row:active .failure { + color: #ff7961; +} + +.ReactVirtualized__Table__row:hover .progress, +.ReactVirtualized__Table__row_dark:hover .progress { + color: #bfc600; +} + +.ReactVirtualized__Table__row:active .progress { + color: #d3da00; +} + +/* Light Theme */ + +.ReactVirtualized__Table { + color: #4a4241; +} -.gridWrapper { - background: #928b8a; +.ReactVirtualized__Table__headerRow { color: #000; - display: grid; - grid-gap: 3px; - grid-template-areas: - 'map map map vehicle' - 'map map map vehicle' - 'map map map mission' - 'log log log mission'; - grid-template-columns: repeat(4, 1fr); - grid-template-rows: repeat(4, 1fr); - height: 100%; + background-color: rgb(0, 0, 0, 0.15); + text-transform: none; +} + +.ReactVirtualized__Table__row { width: 100%; + border-top: 2px solid #626262; + cursor: pointer; +} + +.ReactVirtualized__Table__row:first-child { + border-top: 0; } -.container { - background-color: #eee; +.ReactVirtualized__Table__row:hover { + color: #211e1d; + background-color: rgb(0, 0, 0, 0.05); } -/* Dark theme */ +.ReactVirtualized__Table__row:active { + color: #000; + background-color: rgb(0, 0, 0, 0.1); +} -.gridWrapper_dark { - background: #000; +/* Dark Theme */ + +.ReactVirtualized__Table_dark { + color: #bdbdbd; +} + +.ReactVirtualized__Table__headerRow_dark { color: #fff; - display: grid; - grid-gap: 3px; - grid-template-areas: - 'map map map vehicle' - 'map map map vehicle' - 'map map map mission' - 'log log log mission'; - grid-template-columns: repeat(4, 1fr); - grid-template-rows: repeat(4, 1fr); - height: 100%; + background-color: rgb(255, 255, 255, 0.15); + text-transform: none; +} + +.ReactVirtualized__Table__row_dark { width: 100%; + border-top: 2px solid #626262; + cursor: pointer; } -.container_dark { - background-color: #212121; +.ReactVirtualized__Table__row_dark:first-child { + border-top: 0; +} + +.ReactVirtualized__Table__row_dark:hover { + color: #e9e9e9; + background-color: rgb(255, 255, 255, 0.025); +} + +.ReactVirtualized__Table__row_dark:active { + color: #fff; + background-color: rgb(255, 255, 255, 0.05); } diff --git a/src/renderer/index.js b/src/renderer/index.js index 75624a6f..d897b542 100644 --- a/src/renderer/index.js +++ b/src/renderer/index.js @@ -10,7 +10,6 @@ import MissionWindow from './missionWindow/MissionWindow'; import 'leaflet/dist/leaflet.css'; import 'react-virtualized/styles.css'; -import './global.css'; import './index.css'; import { fixtures, geolocation } from '../../resources/index'; diff --git a/src/renderer/mainWindow/MainWindow.jsx b/src/renderer/mainWindow/MainWindow.jsx index 1df012e7..2383e7eb 100644 --- a/src/renderer/mainWindow/MainWindow.jsx +++ b/src/renderer/mainWindow/MainWindow.jsx @@ -6,13 +6,15 @@ import MapContainer from './map/MapContainer'; import MissionContainer from './mission/MissionContainer'; import VehicleContainer from './vehicle/VehicleContainer'; +import './main.css'; + const propTypes = { theme: PropTypes.oneOf(['light', 'dark']).isRequired, }; export default function MainWindow({ theme }) { return ( -
+
diff --git a/src/renderer/mainWindow/main.css b/src/renderer/mainWindow/main.css new file mode 100644 index 00000000..b59742f6 --- /dev/null +++ b/src/renderer/mainWindow/main.css @@ -0,0 +1,61 @@ +/* Defined styles for index.js */ + +.logContainer { + grid-area: log; +} + +.mapContainer { + grid-area: map; +} + +.missionContainer { + grid-area: mission; +} + +.vehicleContainer { + grid-area: vehicle; +} + +/* Light theme */ + +.mainWrapper { + background: #928b8a; + color: #000; + display: grid; + grid-gap: 3px; + grid-template-areas: + 'map map map vehicle' + 'map map map vehicle' + 'map map map mission' + 'log log log mission'; + grid-template-columns: repeat(4, 1fr); + grid-template-rows: repeat(4, 1fr); + height: 100%; + width: 100%; +} + +.container { + background-color: #eee; +} + +/* Dark theme */ + +.mainWrapper_dark { + background: #000; + color: #fff; + display: grid; + grid-gap: 3px; + grid-template-areas: + 'map map map vehicle' + 'map map map vehicle' + 'map map map mission' + 'log log log mission'; + grid-template-columns: repeat(4, 1fr); + grid-template-rows: repeat(4, 1fr); + height: 100%; + width: 100%; +} + +.container_dark { + background-color: #212121; +} diff --git a/src/renderer/missionWindow/MissionWindow.jsx b/src/renderer/missionWindow/MissionWindow.jsx index a6bf54a0..d65b857b 100644 --- a/src/renderer/missionWindow/MissionWindow.jsx +++ b/src/renderer/missionWindow/MissionWindow.jsx @@ -7,6 +7,8 @@ import ISRSearch from './missions/ISRSearch'; import PayloadDrop from './missions/PayloadDrop'; import RetrieveTarget from './missions/RetrieveTarget'; +import './mission.css'; + const layouts = { deliverTarget: DeliverTarget, isrSearch: ISRSearch, @@ -44,7 +46,7 @@ export default class MissionWindow extends Component { const Layout = layouts[openedMission] || BlankMission; return ( -
+
); diff --git a/src/renderer/missionWindow/mission.css b/src/renderer/missionWindow/mission.css new file mode 100644 index 00000000..c5c11686 --- /dev/null +++ b/src/renderer/missionWindow/mission.css @@ -0,0 +1,23 @@ +/* Defined styles for MissionWindow.jsx */ + +.missionWrapper, +.missionWrapper_dark, +.mission, +.job { + height: 100%; + width: 100; +} + +/* Light theme */ + +.missionWrapper { + background-color: #eee; + color: #000; +} + +/* Dark theme */ + +.missionWrapper_dark { + background-color: #212121; + color: #fff; +} diff --git a/src/renderer/missionWindow/missions/DeliverTarget.jsx b/src/renderer/missionWindow/missions/DeliverTarget.jsx index 3c132284..bca0c44e 100644 --- a/src/renderer/missionWindow/missions/DeliverTarget.jsx +++ b/src/renderer/missionWindow/missions/DeliverTarget.jsx @@ -1,15 +1,10 @@ -import PropTypes from 'prop-types'; import React from 'react'; import { startMission, stopMission, completeMission } from '../../../util/util'; -const propTypes = { - theme: PropTypes.oneOf(['light', 'dark']).isRequired, -}; - -export default function DeliverTarget({ theme }) { +export default function DeliverTarget() { return ( -
+

Deliver Target

@@ -17,5 +12,3 @@ export default function DeliverTarget({ theme }) {
); } - -DeliverTarget.propTypes = propTypes; diff --git a/src/renderer/missionWindow/missions/ISRSearch.jsx b/src/renderer/missionWindow/missions/ISRSearch.jsx index 619d8a9b..8f6dbb1b 100644 --- a/src/renderer/missionWindow/missions/ISRSearch.jsx +++ b/src/renderer/missionWindow/missions/ISRSearch.jsx @@ -1,15 +1,10 @@ -import PropTypes from 'prop-types'; import React from 'react'; import { startMission, stopMission, completeMission } from '../../../util/util'; -const propTypes = { - theme: PropTypes.oneOf(['light', 'dark']).isRequired, -}; - -export default function ISRSearch({ theme }) { +export default function ISRSearch() { return ( -
+

ISR Search

@@ -17,5 +12,3 @@ export default function ISRSearch({ theme }) {
); } - -ISRSearch.propTypes = propTypes; diff --git a/src/renderer/missionWindow/missions/PayloadDrop.jsx b/src/renderer/missionWindow/missions/PayloadDrop.jsx index ba38688b..49e7a5a2 100644 --- a/src/renderer/missionWindow/missions/PayloadDrop.jsx +++ b/src/renderer/missionWindow/missions/PayloadDrop.jsx @@ -1,15 +1,10 @@ -import PropTypes from 'prop-types'; import React from 'react'; import { startMission, stopMission, completeMission } from '../../../util/util'; -const propTypes = { - theme: PropTypes.oneOf(['light', 'dark']).isRequired, -}; - -export default function PayloadDrop({ theme }) { +export default function PayloadDrop() { return ( -
+

Payload Drop

@@ -17,5 +12,3 @@ export default function PayloadDrop({ theme }) {
); } - -PayloadDrop.propTypes = propTypes; diff --git a/src/renderer/missionWindow/missions/RetrieveTarget.jsx b/src/renderer/missionWindow/missions/RetrieveTarget.jsx index 236b836b..a5997fed 100644 --- a/src/renderer/missionWindow/missions/RetrieveTarget.jsx +++ b/src/renderer/missionWindow/missions/RetrieveTarget.jsx @@ -1,15 +1,10 @@ -import PropTypes from 'prop-types'; import React from 'react'; import { startMission, stopMission, completeMission } from '../../../util/util'; -const propTypes = { - theme: PropTypes.oneOf(['light', 'dark']).isRequired, -}; - export default function RetrieveTarget({ theme }) { return ( -
+

Retrieve Target

@@ -17,5 +12,3 @@ export default function RetrieveTarget({ theme }) {
); } - -RetrieveTarget.propTypes = propTypes; diff --git a/webpack.config.js b/webpack.config.js index 61a69286..e7df76fe 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -23,6 +23,6 @@ module.exports = { new Dotenv(), ], resolve: { - extensions: ['.js', '.jsx'], + extensions: ['.jsx'], }, }; From 322b64684d31243cac7bab8c389b654551cc1cec Mon Sep 17 00:00:00 2001 From: Paul Matthew Barrameda Date: Sun, 24 Mar 2019 15:31:40 -0700 Subject: [PATCH 018/152] Added logic and basic structure to mission window Needs more work: 1. Create a general mission jsx class, since these classes seem to have similar logic. 2. Create some jobs. Its likely that jobs will need a general class too. These general classes, by the way, should be superclasses to be extended from. --- src/main/index.js | 40 +++++--- src/renderer/mainWindow/main.css | 25 ++--- .../mainWindow/mission/MissionContainer.jsx | 41 ++++---- src/renderer/missionWindow/MissionWindow.jsx | 20 ++-- src/renderer/missionWindow/mission.css | 16 +++ .../missionWindow/missions/DeliverPayload.jsx | 14 +++ .../missionWindow/missions/DeliverTarget.jsx | 14 --- .../missionWindow/missions/FindTarget.jsx | 14 +++ .../missionWindow/missions/GetTarget.jsx | 99 +++++++++++++++++++ .../missionWindow/missions/ISRSearch.jsx | 14 --- .../missionWindow/missions/PayloadDrop.jsx | 14 --- .../missionWindow/missions/RetrieveTarget.jsx | 14 --- src/util/util.js | 63 +++++++++--- 13 files changed, 259 insertions(+), 129 deletions(-) create mode 100644 src/renderer/missionWindow/missions/DeliverPayload.jsx delete mode 100644 src/renderer/missionWindow/missions/DeliverTarget.jsx create mode 100644 src/renderer/missionWindow/missions/FindTarget.jsx create mode 100644 src/renderer/missionWindow/missions/GetTarget.jsx delete mode 100644 src/renderer/missionWindow/missions/ISRSearch.jsx delete mode 100644 src/renderer/missionWindow/missions/PayloadDrop.jsx delete mode 100644 src/renderer/missionWindow/missions/RetrieveTarget.jsx diff --git a/src/main/index.js b/src/main/index.js index f33c32d6..edfdaf00 100644 --- a/src/main/index.js +++ b/src/main/index.js @@ -18,13 +18,13 @@ const HEIGHT = 576; const isDevelopment = process.env.NODE_ENV !== 'production'; -// TODO: put icon tray back to macOS but resize it so that its not huge on macOS's menu +// TODO: put icon tray back to macOS but resize it so that its not huge on macOS's menu. const icon = nativeImage.createFromDataURL(images.icon); // Variable to keep track when the app will quit, which is different from hiding the app. let quitting = false; -// References to window objects +// References to window objects. let mainWindow; let missionWindow; @@ -208,6 +208,11 @@ function setLocationMenu() { }); } +function hideMissionWindow() { + mainWindow.webContents.send('setSelectedMission', -1); + missionWindow.hide(); +} + /** * Creates the main window. This window's hash is #main. */ @@ -237,8 +242,7 @@ function createMainWindow() { event.preventDefault(); mainWindow.hide(); if (missionWindow) { - mainWindow.webContents.send('setSelectedMission', -1); - missionWindow.hide(); + hideMissionWindow(); } } else { mainWindow = null; @@ -256,9 +260,10 @@ function createMissionWindow() { title: 'NGCP Mission User Interface', icon, show: false, - width: WIDTH, - minWidth: WIDTH, + width: Math.floor(WIDTH / 3), + minWidth: Math.floor(WIDTH / 3), height: HEIGHT, + autoHideMenuBar: true, minHeight: HEIGHT, }); @@ -266,16 +271,21 @@ function createMissionWindow() { missionWindow.loadURL(`http://localhost:${process.env.ELECTRON_WEBPACK_WDS_PORT}#mission`); } else { missionWindow.loadURL(`file:///${__dirname}/index.html#mission`); + + /* + * Generally we should not have a menu on the mission window, but the menu helps us when + * developing the mission window (mainly gives us access to developer console, which then + * allows us to see which elements are loaded, as well as the browser's console log). + */ + missionWindow.setMenu(null); } - missionWindow.setMenu(null); missionWindow.on('close', (event) => { if (!quitting) { event.preventDefault(); - // following line allows MissionContainer to update to closed mission mainWindow - mainWindow.webContents.send('setSelectedMission', -1); - missionWindow.hide(); + // This allows the mission container to update to closed mission window. + hideMissionWindow(); } else { missionWindow = null; } @@ -334,8 +344,9 @@ app.on('activate', showWindow); app.on('ready', () => { createMainWindow(); - createMissionWindow(); createMenu(); + + createMissionWindow(); createTray(); }); @@ -346,8 +357,15 @@ app.on('before-quit', () => { ipcMain.on('post', (event, notification, data) => { if (notification === 'showMissionWindow') { showMissionWindow(); + } else if (notification === 'hideMissionWindow') { + hideMissionWindow(); } + /* + * We must forward ipc notifications to both windows or else + * some notifications will not be picked up. Of course we can filter out which + * notifications go for which window, but its simpler to have it forwarded to both. + */ mainWindow.webContents.send(notification, data); if (missionWindow) { missionWindow.webContents.send(notification, data); diff --git a/src/renderer/mainWindow/main.css b/src/renderer/mainWindow/main.css index b59742f6..a4f5e817 100644 --- a/src/renderer/mainWindow/main.css +++ b/src/renderer/mainWindow/main.css @@ -16,11 +16,8 @@ grid-area: vehicle; } -/* Light theme */ - -.mainWrapper { - background: #928b8a; - color: #000; +.mainWrapper, +.mainWrapper_dark { display: grid; grid-gap: 3px; grid-template-areas: @@ -34,6 +31,13 @@ width: 100%; } +/* Light theme */ + +.mainWrapper { + background: #928b8a; + color: #000; +} + .container { background-color: #eee; } @@ -43,17 +47,6 @@ .mainWrapper_dark { background: #000; color: #fff; - display: grid; - grid-gap: 3px; - grid-template-areas: - 'map map map vehicle' - 'map map map vehicle' - 'map map map mission' - 'log log log mission'; - grid-template-columns: repeat(4, 1fr); - grid-template-rows: repeat(4, 1fr); - height: 100%; - width: 100%; } .container_dark { diff --git a/src/renderer/mainWindow/mission/MissionContainer.jsx b/src/renderer/mainWindow/mission/MissionContainer.jsx index c6daf1b9..17138777 100644 --- a/src/renderer/mainWindow/mission/MissionContainer.jsx +++ b/src/renderer/mainWindow/mission/MissionContainer.jsx @@ -42,8 +42,8 @@ const statusTypes = { type: 'progress', }, finished: { - name: 'finished', - message: 'Finished', + name: 'completed', + message: 'Completed', type: 'success', }, }; @@ -86,23 +86,18 @@ export default class MissionContainer extends Component { */ missions: [ { - name: 'isrSearch', - description: 'Find target', + name: 'findTarget', + description: "Find target's location", status: statusTypes.notStarted, }, { - name: 'payloadDrop', + name: 'deliverPayload', description: 'Deliver payload to target', status: statusTypes.notStarted, }, { - name: 'retrieveTarget', - description: 'Retreive target', - status: statusTypes.notStarted, - }, - { - name: 'deliverTarget', - description: 'Deliver target', + name: 'getTarget', + description: 'Get the target', status: statusTypes.notStarted, }, ], @@ -139,6 +134,9 @@ export default class MissionContainer extends Component { // Opens mission window for specified mission. if (index !== -1) { + // Does not open any window for completed missions. + if (missions[index].status.name === 'completed') return; + ipcRenderer.send('post', 'showMissionWindow', missions[index]); } @@ -206,27 +204,24 @@ export default class MissionContainer extends Component { startedMissionIndex = openedMissionIndex; } else if (name === 'notStarted') { startedMissionIndex = -1; + + // Closes the mission window. + ipcRenderer.send('post', 'hideMissionWindow'); } this.setState({ missions: newMissions }); } completeMission() { - const { missions, openedMissionIndex } = this.state; + const { missions } = this.state; const newMissions = missions; - // It is possible that the mission window for started mission is currently open or closed. - if (openedMissionIndex === startedMissionIndex) { - newMissions[startedMissionIndex].status = { - ...statusTypes.finished, - message: `${statusTypes.finished.message} (Open)`, - }; - } else { - newMissions[startedMissionIndex].status = statusTypes.finished; - } - + newMissions[startedMissionIndex].status = statusTypes.completed; startedMissionIndex = -1; + // Closes the mission window. + ipcRenderer.send('post', 'hideMissionWindow'); + this.setState({ missions: newMissions }); } diff --git a/src/renderer/missionWindow/MissionWindow.jsx b/src/renderer/missionWindow/MissionWindow.jsx index d65b857b..e209dbcf 100644 --- a/src/renderer/missionWindow/MissionWindow.jsx +++ b/src/renderer/missionWindow/MissionWindow.jsx @@ -2,27 +2,23 @@ import { ipcRenderer } from 'electron'; // eslint-disable-line import/no-extrane import PropTypes from 'prop-types'; import React, { Component } from 'react'; -import DeliverTarget from './missions/DeliverTarget'; -import ISRSearch from './missions/ISRSearch'; -import PayloadDrop from './missions/PayloadDrop'; -import RetrieveTarget from './missions/RetrieveTarget'; +import DeliverPayload from './missions/DeliverPayload'; +import FindTarget from './missions/FindTarget'; +import GetTarget from './missions/GetTarget'; import './mission.css'; const layouts = { - deliverTarget: DeliverTarget, - isrSearch: ISRSearch, - payloadDrop: PayloadDrop, - retrieveTarget: RetrieveTarget, + deliverPayload: DeliverPayload, + findTarget: FindTarget, + getTarget: GetTarget, }; const propTypes = { theme: PropTypes.oneOf(['light', 'dark']).isRequired, }; -function BlankMission() { - return
; -} +const div = () =>
; export default class MissionWindow extends Component { constructor(props) { @@ -43,7 +39,7 @@ export default class MissionWindow extends Component { const { theme } = this.props; const { openedMission } = this.state; - const Layout = layouts[openedMission] || BlankMission; + const Layout = layouts[openedMission] || div; return (
diff --git a/src/renderer/missionWindow/mission.css b/src/renderer/missionWindow/mission.css index c5c11686..dd9662c3 100644 --- a/src/renderer/missionWindow/mission.css +++ b/src/renderer/missionWindow/mission.css @@ -4,10 +4,26 @@ .missionWrapper_dark, .mission, .job { + display: grid; height: 100%; width: 100; } +.job { + grid-template-areas: + 'info' + 'button'; + grid-template-rows: 7fr 1fr; +} + +.infoContainer { + grid-area: info; +} + +.buttonContainer { + grid-area: button; +} + /* Light theme */ .missionWrapper { diff --git a/src/renderer/missionWindow/missions/DeliverPayload.jsx b/src/renderer/missionWindow/missions/DeliverPayload.jsx new file mode 100644 index 00000000..206eed9f --- /dev/null +++ b/src/renderer/missionWindow/missions/DeliverPayload.jsx @@ -0,0 +1,14 @@ +import React from 'react'; + +import { mission } from '../../../util/util'; + +export default function DeliverPayload() { + return ( +
+

Deliver payload to target

+ + + +
+ ); +} diff --git a/src/renderer/missionWindow/missions/DeliverTarget.jsx b/src/renderer/missionWindow/missions/DeliverTarget.jsx deleted file mode 100644 index bca0c44e..00000000 --- a/src/renderer/missionWindow/missions/DeliverTarget.jsx +++ /dev/null @@ -1,14 +0,0 @@ -import React from 'react'; - -import { startMission, stopMission, completeMission } from '../../../util/util'; - -export default function DeliverTarget() { - return ( -
-

Deliver Target

- - - -
- ); -} diff --git a/src/renderer/missionWindow/missions/FindTarget.jsx b/src/renderer/missionWindow/missions/FindTarget.jsx new file mode 100644 index 00000000..1111be64 --- /dev/null +++ b/src/renderer/missionWindow/missions/FindTarget.jsx @@ -0,0 +1,14 @@ +import React from 'react'; + +import { mission } from '../../../util/util'; + +export default function FindTarget() { + return ( +
+

Find the target

+ + + +
+ ); +} diff --git a/src/renderer/missionWindow/missions/GetTarget.jsx b/src/renderer/missionWindow/missions/GetTarget.jsx new file mode 100644 index 00000000..c354641b --- /dev/null +++ b/src/renderer/missionWindow/missions/GetTarget.jsx @@ -0,0 +1,99 @@ +import { ipcRenderer } from 'electron'; // eslint-disable-line import/no-extraneous-dependencies +import React, { Component } from 'react'; + +import DeliverTarget from './jobs/DeliverTarget'; +import RetrieveTarget from './jobs/RetrieveTarget'; + +import { mission } from '../../../util/util'; + +const layouts = { + deliverTarget: DeliverTarget, + retrieveTarget: RetrieveTarget, +}; + +/* + * We can create a Mission class that this class extends that will have these functions. + * The Mission class can take in a parameter that will define all the jobs for that mission. + * All Mission classes have very similar logic so this is why I add this comment here. + * Stuff that we probably can pass to the Mission class: + * 1. Jobs and their respective layout + * 2. Description of Mission to load to the h2 component in the info container (see below). + * TODO: Add a Mission class that this class extends. The class can be in the same directory. + */ + +const Default = () => ( +
+
+

Get the target

+
+
+ +
+
+); + +export default class GetTarget extends Component { + constructor(props) { + super(props); + + this.state = { + currentJobIndex: -1, + jobs: [ + { + name: 'retrieveTarget', + }, + { + name: 'deliverTarget', + }, + ], + }; + + this.completeJob = this.completeJob.bind(this); + } + + componentDidMount() { + ipcRenderer.on('startMission', () => { + this.setState({ currentJobIndex: 0 }); + }); + + ipcRenderer.on('stopMission', () => { + this.setState({ currentJobIndex: -1 }); + }); + + ipcRenderer.on('completeJob', this.completeJob); + ipcRenderer.on('nextJob', this.nextJob); + } + + nextJob() { + const { currentJobIndex } = this.state; + this.setState({ currentJobIndex: currentJobIndex + 1 }); + } + + completeJob() { + const { currentJobIndex, jobs } = this.state; + + if (currentJobIndex === jobs.length - 1) { + /* + * This will forward a notification to the job window that will create a button + * that, when clicked, will send the "completeMission" notification. The + * mission container will handle closing the mission window when the notification + * is sent. + */ + ipcRenderer.send('post', 'showCompleteMissionButton'); + } else { + ipcRenderer.send('post', 'showNextJobButton'); + } + } + + render() { + const { currentJobIndex } = this.state; + + const Layout = currentJobIndex >= 0 ? layouts[currentJobIndex] : Default; + + return ( +
+ +
+ ); + } +} diff --git a/src/renderer/missionWindow/missions/ISRSearch.jsx b/src/renderer/missionWindow/missions/ISRSearch.jsx deleted file mode 100644 index 8f6dbb1b..00000000 --- a/src/renderer/missionWindow/missions/ISRSearch.jsx +++ /dev/null @@ -1,14 +0,0 @@ -import React from 'react'; - -import { startMission, stopMission, completeMission } from '../../../util/util'; - -export default function ISRSearch() { - return ( -
-

ISR Search

- - - -
- ); -} diff --git a/src/renderer/missionWindow/missions/PayloadDrop.jsx b/src/renderer/missionWindow/missions/PayloadDrop.jsx deleted file mode 100644 index 49e7a5a2..00000000 --- a/src/renderer/missionWindow/missions/PayloadDrop.jsx +++ /dev/null @@ -1,14 +0,0 @@ -import React from 'react'; - -import { startMission, stopMission, completeMission } from '../../../util/util'; - -export default function PayloadDrop() { - return ( -
-

Payload Drop

- - - -
- ); -} diff --git a/src/renderer/missionWindow/missions/RetrieveTarget.jsx b/src/renderer/missionWindow/missions/RetrieveTarget.jsx deleted file mode 100644 index a5997fed..00000000 --- a/src/renderer/missionWindow/missions/RetrieveTarget.jsx +++ /dev/null @@ -1,14 +0,0 @@ -import React from 'react'; - -import { startMission, stopMission, completeMission } from '../../../util/util'; - -export default function RetrieveTarget({ theme }) { - return ( -
-

Retrieve Target

- - - -
- ); -} diff --git a/src/util/util.js b/src/util/util.js index 1b983af8..d7b59b4b 100644 --- a/src/util/util.js +++ b/src/util/util.js @@ -4,6 +4,7 @@ import { vehicleInfos, vehicleStatuses, } from '../../resources/index'; +// Called by map container and vehicle container to update vehicles being shown. export function updateVehicles(component, vehicles) { const { vehicles: thisVehicles } = component.state; const currentVehicles = thisVehicles; @@ -19,30 +20,70 @@ export function updateVehicles(component, vehicles) { component.setState({ vehicles: currentVehicles }); } -export function startMission() { +/* + * These functions will allow us to maintain mission states (starting missions, job, etc.) + * I'll add explanation here soon. All explanations are in Slack right now. + * The way we should name functions that interact with ipcRenderer are the following: + * Assuming we have the notification "startMission", the function startMission() is the + * callback to when the notification is received. The function sendStartMission() is the + * function that sends out this notification. + */ + +function sendStartMission() { ipcRenderer.send('post', 'startMission'); } -export function stopMission() { +function sendStopMission() { ipcRenderer.send('post', 'stopMission'); } -export function completeMission() { +function sendCompleteMission() { ipcRenderer.send('post', 'completeMission'); } +// We can have this or directly call function from Orchestrator. +function sendStartJob(data) { + ipcRenderer.send('post', 'startJob', data); +} + +// We can have this or directly call function from Orchestrator. +function sendPauseJob() { + ipcRenderer.send('post', 'pauseJob'); +} + +// We can have this or directly call function from Orchestrator. +function sendResumeJob() { + ipcRenderer.send('post', 'resumeJob'); +} + +// Orchestrator should call this function, or send the notification directly. +function sendCompleteJob() { + ipcRenderer.send('post', 'completeJob'); +} + +export const mission = { sendStartMission, sendStopMission, sendCompleteMission }; +export const job = { + sendStartJob, + sendPauseJob, + sendResumeJob, + sendCompleteJob, +}; + /* eslint-disable no-bitwise */ -// Bitshifting allows us to switch between hex and float +/* + * Bitshifting allows us to switch between hex and float. + * We will use hex for all floats so these functions will be in help. + */ -export function floatToHexString(float) { +function toHexString(float) { const getHex = i => `00${i.toString(16)}`.slice(-2); const view = new DataView(new ArrayBuffer(4)); view.setFloat32(0, float); return [0, 0, 0, 0].map((_, i) => getHex(view.getUint8(i))).join(''); } -export function hexStringToFloat(hexString) { +function toFloat(hexString) { const int = parseInt(hexString, 16); if (int > 0 || int < 0) { const sign = (int >>> 31) ? -1 : 1; @@ -62,13 +103,13 @@ export function hexStringToFloat(hexString) { return 0; } +export const floatHex = { toHexString, toFloat }; + /* eslint-enable no-bitwise */ export default { updateVehicles, - startMission, - stopMission, - completeMission, - floatToHexString, - hexStringToFloat, + job, + mission, + floatHex, }; From 649b0f6f1b3f871dea0315c5cda6172eed4d30c2 Mon Sep 17 00:00:00 2001 From: Paul Matthew Barrameda Date: Sun, 24 Mar 2019 15:38:27 -0700 Subject: [PATCH 019/152] Forgot to bind next job to mission --- src/renderer/missionWindow/missions/GetTarget.jsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/renderer/missionWindow/missions/GetTarget.jsx b/src/renderer/missionWindow/missions/GetTarget.jsx index c354641b..6ad3614b 100644 --- a/src/renderer/missionWindow/missions/GetTarget.jsx +++ b/src/renderer/missionWindow/missions/GetTarget.jsx @@ -48,6 +48,7 @@ export default class GetTarget extends Component { ], }; + this.nextJob = this.nextJob.bind(this); this.completeJob = this.completeJob.bind(this); } From de216f988870a4f79fbd02a52fb01ee7154eec80 Mon Sep 17 00:00:00 2001 From: Paul Matthew Barrameda Date: Sun, 24 Mar 2019 16:16:23 -0700 Subject: [PATCH 020/152] Fixed logic --- .../mainWindow/mission/MissionContainer.jsx | 29 ++++++++++++------- .../missionWindow/missions/DeliverPayload.jsx | 6 ++-- .../missionWindow/missions/FindTarget.jsx | 6 ++-- 3 files changed, 24 insertions(+), 17 deletions(-) diff --git a/src/renderer/mainWindow/mission/MissionContainer.jsx b/src/renderer/mainWindow/mission/MissionContainer.jsx index 17138777..2b9372f4 100644 --- a/src/renderer/mainWindow/mission/MissionContainer.jsx +++ b/src/renderer/mainWindow/mission/MissionContainer.jsx @@ -19,11 +19,6 @@ const propTypes = { theme: PropTypes.oneOf(['light', 'dark']).isRequired, }; -/* - * Variable is -1 if no mission is running, otherwise it's the index of the mission in missions. - * If a mission is paused, it will still be the index of that mission. - */ -let startedMissionIndex = -1; const statusTypes = { notStarted: { @@ -41,7 +36,7 @@ const statusTypes = { message: 'Paused', type: 'progress', }, - finished: { + completed: { name: 'completed', message: 'Completed', type: 'success', @@ -59,6 +54,12 @@ export default class MissionContainer extends Component { */ openedMissionIndex: -1, + /* + * Variable is -1 if no mission is running, otherwise it's the index of the + * mission in missions. If a mission is paused, it will still be the index of that mission. + */ + startedMissionIndex: -1, + /* * We have four missions in the mission state, with each mission having a certain * number of jobs required to accomplish that mission. @@ -201,9 +202,9 @@ export default class MissionContainer extends Component { }; if (name === 'started') { - startedMissionIndex = openedMissionIndex; + this.setState({ startedMissionIndex: openedMissionIndex }); } else if (name === 'notStarted') { - startedMissionIndex = -1; + this.setState({ startedMissionIndex: -1 }); // Closes the mission window. ipcRenderer.send('post', 'hideMissionWindow'); @@ -213,16 +214,22 @@ export default class MissionContainer extends Component { } completeMission() { - const { missions } = this.state; + const { missions, startedMissionIndex } = this.state; const newMissions = missions; + if (startedMissionIndex === -1) { + throw new Error('Mission must be started before completing it'); + } + newMissions[startedMissionIndex].status = statusTypes.completed; - startedMissionIndex = -1; // Closes the mission window. ipcRenderer.send('post', 'hideMissionWindow'); - this.setState({ missions: newMissions }); + this.setState({ + missions: newMissions, + startedMissionIndex: -1, + }); } render() { diff --git a/src/renderer/missionWindow/missions/DeliverPayload.jsx b/src/renderer/missionWindow/missions/DeliverPayload.jsx index 206eed9f..c05fda56 100644 --- a/src/renderer/missionWindow/missions/DeliverPayload.jsx +++ b/src/renderer/missionWindow/missions/DeliverPayload.jsx @@ -6,9 +6,9 @@ export default function DeliverPayload() { return (

Deliver payload to target

- - - + + +
); } diff --git a/src/renderer/missionWindow/missions/FindTarget.jsx b/src/renderer/missionWindow/missions/FindTarget.jsx index 1111be64..3a96fffe 100644 --- a/src/renderer/missionWindow/missions/FindTarget.jsx +++ b/src/renderer/missionWindow/missions/FindTarget.jsx @@ -6,9 +6,9 @@ export default function FindTarget() { return (

Find the target

- - - + + +
); } From 56226c8f7674435eb8dc44c639a7e4faaee72c60 Mon Sep 17 00:00:00 2001 From: Paul Matthew Barrameda Date: Sun, 24 Mar 2019 16:59:55 -0700 Subject: [PATCH 021/152] Forwarded mission index to mission windows to allow multiple missions to run More logic. Should not affect building other missions. Just understand to forward mission index to mission jsx classes to be able to return that index to mission container to complete missions (and be able to run more than one mission at a time). --- .../mainWindow/mission/MissionContainer.jsx | 51 +++++++++---------- src/renderer/missionWindow/MissionWindow.jsx | 8 +-- .../missionWindow/missions/DeliverPayload.jsx | 13 +++-- .../missionWindow/missions/FindTarget.jsx | 9 +++- src/util/util.js | 11 ++-- 5 files changed, 52 insertions(+), 40 deletions(-) diff --git a/src/renderer/mainWindow/mission/MissionContainer.jsx b/src/renderer/mainWindow/mission/MissionContainer.jsx index 2b9372f4..83055914 100644 --- a/src/renderer/mainWindow/mission/MissionContainer.jsx +++ b/src/renderer/mainWindow/mission/MissionContainer.jsx @@ -54,12 +54,6 @@ export default class MissionContainer extends Component { */ openedMissionIndex: -1, - /* - * Variable is -1 if no mission is running, otherwise it's the index of the - * mission in missions. If a mission is paused, it will still be the index of that mission. - */ - startedMissionIndex: -1, - /* * We have four missions in the mission state, with each mission having a certain * number of jobs required to accomplish that mission. @@ -114,10 +108,10 @@ export default class MissionContainer extends Component { ipcRenderer.on('startMission', () => this.updateMission('started')); ipcRenderer.on('stopMission', () => this.updateMission('notStarted')); - ipcRenderer.on('pauseMission', () => this.updateMission('paused')); - ipcRenderer.on('resumeMission', () => this.updateMission('started')); + ipcRenderer.on('pauseJob', () => this.updateMission('paused')); + ipcRenderer.on('resumeJob', () => this.updateMission('started')); - ipcRenderer.on('completeMission', this.completeMission); + ipcRenderer.on('completeMission', (event, index) => this.completeMission(index)); } /** @@ -138,7 +132,15 @@ export default class MissionContainer extends Component { // Does not open any window for completed missions. if (missions[index].status.name === 'completed') return; - ipcRenderer.send('post', 'showMissionWindow', missions[index]); + /* + * Passes index to mission window so that when mission is completed, the mission + * window can forward that index back to the mission container to update. + * This helps us have multiple missions running at once. + */ + ipcRenderer.send('post', 'showMissionWindow', { + ...missions[index], + index, + }); } /* @@ -184,10 +186,10 @@ export default class MissionContainer extends Component { * Updates mission description in mission container whenever a start/stop/pause/resume * mission notification is sent. * - * @param {string} name Consists of notStarted/started/paused. See the componentDidMount + * @param {string} type Consists of notStarted/started/paused. See the componentDidMount * function to see which name corresponds with which notification. */ - updateMission(name) { + updateMission(statusName) { const { missions, openedMissionIndex } = this.state; const newMissions = missions; @@ -197,39 +199,32 @@ export default class MissionContainer extends Component { * end of the mission description. */ newMissions[openedMissionIndex].status = { - ...statusTypes[name], - message: `${statusTypes[name].message} (Open)`, + ...statusTypes[statusName], + message: `${statusTypes[statusName].message} (Open)`, }; - if (name === 'started') { - this.setState({ startedMissionIndex: openedMissionIndex }); - } else if (name === 'notStarted') { - this.setState({ startedMissionIndex: -1 }); - - // Closes the mission window. + // Closes the mission window on stop mission. + if (statusName === 'notStarted') { ipcRenderer.send('post', 'hideMissionWindow'); } this.setState({ missions: newMissions }); } - completeMission() { - const { missions, startedMissionIndex } = this.state; + completeMission(index) { + const { missions } = this.state; const newMissions = missions; - if (startedMissionIndex === -1) { + if (missions[index].status.name !== 'started') { throw new Error('Mission must be started before completing it'); } - newMissions[startedMissionIndex].status = statusTypes.completed; + newMissions[index].status = statusTypes.completed; // Closes the mission window. ipcRenderer.send('post', 'hideMissionWindow'); - this.setState({ - missions: newMissions, - startedMissionIndex: -1, - }); + this.setState({ missions: newMissions }); } render() { diff --git a/src/renderer/missionWindow/MissionWindow.jsx b/src/renderer/missionWindow/MissionWindow.jsx index e209dbcf..b09a41f8 100644 --- a/src/renderer/missionWindow/MissionWindow.jsx +++ b/src/renderer/missionWindow/MissionWindow.jsx @@ -25,13 +25,13 @@ export default class MissionWindow extends Component { super(props); this.state = { - openedMission: '', + openedMission: {}, }; } componentDidMount() { ipcRenderer.on('showMissionWindow', (event, mission) => { - this.setState({ openedMission: mission.name }); + this.setState({ openedMission: mission }); }); } @@ -39,11 +39,11 @@ export default class MissionWindow extends Component { const { theme } = this.props; const { openedMission } = this.state; - const Layout = layouts[openedMission] || div; + const Layout = layouts[openedMission.name] || div; return (
- +
); } diff --git a/src/renderer/missionWindow/missions/DeliverPayload.jsx b/src/renderer/missionWindow/missions/DeliverPayload.jsx index c05fda56..a421a300 100644 --- a/src/renderer/missionWindow/missions/DeliverPayload.jsx +++ b/src/renderer/missionWindow/missions/DeliverPayload.jsx @@ -1,14 +1,21 @@ +import PropTypes from 'prop-types'; import React from 'react'; -import { mission } from '../../../util/util'; +import { job, mission } from '../../../util/util'; -export default function DeliverPayload() { +export default function DeliverPayload({ index }) { return (

Deliver payload to target

+ + - +
); } + +DeliverPayload.propTypes = { + index: PropTypes.number.isRequired, +}; diff --git a/src/renderer/missionWindow/missions/FindTarget.jsx b/src/renderer/missionWindow/missions/FindTarget.jsx index 3a96fffe..1912ea41 100644 --- a/src/renderer/missionWindow/missions/FindTarget.jsx +++ b/src/renderer/missionWindow/missions/FindTarget.jsx @@ -1,14 +1,19 @@ +import PropTypes from 'prop-types'; import React from 'react'; import { mission } from '../../../util/util'; -export default function FindTarget() { +export default function FindTarget({ index }) { return (

Find the target

- +
); } + +FindTarget.propTypes = { + index: PropTypes.number.isRequired, +}; diff --git a/src/util/util.js b/src/util/util.js index d7b59b4b..e6a5a8cf 100644 --- a/src/util/util.js +++ b/src/util/util.js @@ -37,10 +37,16 @@ function sendStopMission() { ipcRenderer.send('post', 'stopMission'); } -function sendCompleteMission() { - ipcRenderer.send('post', 'completeMission'); +function sendCompleteMission(index) { + ipcRenderer.send('post', 'completeMission', index); } +export const mission = { + sendStartMission, + sendStopMission, + sendCompleteMission, +}; + // We can have this or directly call function from Orchestrator. function sendStartJob(data) { ipcRenderer.send('post', 'startJob', data); @@ -61,7 +67,6 @@ function sendCompleteJob() { ipcRenderer.send('post', 'completeJob'); } -export const mission = { sendStartMission, sendStopMission, sendCompleteMission }; export const job = { sendStartJob, sendPauseJob, From b0ec5e712ade3a0bd8af0bc3e8ed912ef3ed33c0 Mon Sep 17 00:00:00 2001 From: Paul Matthew Barrameda Date: Mon, 25 Mar 2019 00:00:23 -0700 Subject: [PATCH 022/152] Added general Mission and Job classes. See #56. Needs a lot of styling. Needs more logic. - Logic to add parameters from jobs to Orchestrator through startJob. - Logic to filter out unrelated veihicles while mission window is open. I might work on this next. There are minor logic issues still. Will push anyway to share. - Buttons might not be properly turned on/off - PropTypes need to be fixed --- src/main/index.js | 2 +- src/renderer/fixtures/completeJobFixture.js | 5 + src/renderer/fixtures/index.js | 4 +- .../mainWindow/mission/MissionContainer.jsx | 40 ++--- src/renderer/missionWindow/MissionWindow.jsx | 2 +- .../missionWindow/missions/DeliverPayload.jsx | 49 ++++-- .../missionWindow/missions/FindTarget.jsx | 59 +++++-- .../missionWindow/missions/GetTarget.jsx | 121 ++++---------- .../missionWindow/missions/Mission.jsx | 101 ++++++++++++ .../missions/jobs/DeliverTarget.jsx | 15 ++ .../missions/jobs/DetailedSearch.jsx | 15 ++ .../missionWindow/missions/jobs/ISRSearch.jsx | 15 ++ .../missionWindow/missions/jobs/Job.jsx | 147 ++++++++++++++++++ .../missionWindow/missions/jobs/Land.jsx | 15 ++ .../missionWindow/missions/jobs/Loiter.jsx | 15 ++ .../missions/jobs/PayloadDrop.jsx | 15 ++ .../missionWindow/missions/jobs/QuickScan.jsx | 15 ++ .../missions/jobs/RetrieveTarget.jsx | 15 ++ .../missionWindow/missions/jobs/Takeoff.jsx | 15 ++ 19 files changed, 515 insertions(+), 150 deletions(-) create mode 100644 src/renderer/fixtures/completeJobFixture.js create mode 100644 src/renderer/missionWindow/missions/Mission.jsx create mode 100644 src/renderer/missionWindow/missions/jobs/Job.jsx diff --git a/src/main/index.js b/src/main/index.js index edfdaf00..12951703 100644 --- a/src/main/index.js +++ b/src/main/index.js @@ -18,7 +18,7 @@ const HEIGHT = 576; const isDevelopment = process.env.NODE_ENV !== 'production'; -// TODO: put icon tray back to macOS but resize it so that its not huge on macOS's menu. +// TODO: Put icon tray back to macOS but resize it so that its not huge on macOS's menu. const icon = nativeImage.createFromDataURL(images.icon); // Variable to keep track when the app will quit, which is different from hiding the app. diff --git a/src/renderer/fixtures/completeJobFixture.js b/src/renderer/fixtures/completeJobFixture.js new file mode 100644 index 00000000..833cbdd9 --- /dev/null +++ b/src/renderer/fixtures/completeJobFixture.js @@ -0,0 +1,5 @@ +import { ipcRenderer } from 'electron'; // eslint-disable-line import/no-extraneous-dependencies + +export default function completeJob() { + ipcRenderer.send('post', 'completeJob'); +} diff --git a/src/renderer/fixtures/index.js b/src/renderer/fixtures/index.js index 655d7624..daaaf70b 100644 --- a/src/renderer/fixtures/index.js +++ b/src/renderer/fixtures/index.js @@ -6,10 +6,12 @@ * errors from happening in the first place. */ +import completeJob from './completeJobFixture'; import updateMessages from './updateMessagesFixtures'; import updateVehicles from './updateVehiclesFixtures'; setInterval(() => { + completeJob(); updateMessages(); updateVehicles(); -}, 1000); +}, 5000); diff --git a/src/renderer/mainWindow/mission/MissionContainer.jsx b/src/renderer/mainWindow/mission/MissionContainer.jsx index 83055914..46d49945 100644 --- a/src/renderer/mainWindow/mission/MissionContainer.jsx +++ b/src/renderer/mainWindow/mission/MissionContainer.jsx @@ -19,7 +19,6 @@ const propTypes = { theme: PropTypes.oneOf(['light', 'dark']).isRequired, }; - const statusTypes = { notStarted: { name: 'notStarted', @@ -100,7 +99,6 @@ export default class MissionContainer extends Component { this.setSelectedMission = this.setSelectedMission.bind(this); this.updateMission = this.updateMission.bind(this); - this.completeMission = this.completeMission.bind(this); } componentDidMount() { @@ -108,10 +106,10 @@ export default class MissionContainer extends Component { ipcRenderer.on('startMission', () => this.updateMission('started')); ipcRenderer.on('stopMission', () => this.updateMission('notStarted')); + ipcRenderer.on('completeMission', () => this.updateMission('completed')); + ipcRenderer.on('pauseJob', () => this.updateMission('paused')); ipcRenderer.on('resumeJob', () => this.updateMission('started')); - - ipcRenderer.on('completeMission', (event, index) => this.completeMission(index)); } /** @@ -137,10 +135,7 @@ export default class MissionContainer extends Component { * window can forward that index back to the mission container to update. * This helps us have multiple missions running at once. */ - ipcRenderer.send('post', 'showMissionWindow', { - ...missions[index], - index, - }); + ipcRenderer.send('post', 'showMissionWindow', missions[index]); } /* @@ -193,8 +188,12 @@ export default class MissionContainer extends Component { const { missions, openedMissionIndex } = this.state; const newMissions = missions; + if (statusName === 'completed' && missions[openedMissionIndex].status.name !== 'started') { + throw new Error('Mission must be started before completing it'); + } + /* - * All four actions can only be done through the mission window, + * All of the actions that change status are done while mission window is opened, * so we can assume that the mission is currently open, hence we can add (Open) to the * end of the mission description. */ @@ -204,39 +203,20 @@ export default class MissionContainer extends Component { }; // Closes the mission window on stop mission. - if (statusName === 'notStarted') { + if (statusName === 'notStarted' || statusName === 'completed') { ipcRenderer.send('post', 'hideMissionWindow'); } this.setState({ missions: newMissions }); } - completeMission(index) { - const { missions } = this.state; - const newMissions = missions; - - if (missions[index].status.name !== 'started') { - throw new Error('Mission must be started before completing it'); - } - - newMissions[index].status = statusTypes.completed; - - // Closes the mission window. - ipcRenderer.send('post', 'hideMissionWindow'); - - this.setState({ missions: newMissions }); - } - render() { const { theme } = this.props; const { missions } = this.state; return (
- +
); } diff --git a/src/renderer/missionWindow/MissionWindow.jsx b/src/renderer/missionWindow/MissionWindow.jsx index b09a41f8..487a6b2a 100644 --- a/src/renderer/missionWindow/MissionWindow.jsx +++ b/src/renderer/missionWindow/MissionWindow.jsx @@ -43,7 +43,7 @@ export default class MissionWindow extends Component { return (
- +
); } diff --git a/src/renderer/missionWindow/missions/DeliverPayload.jsx b/src/renderer/missionWindow/missions/DeliverPayload.jsx index a421a300..01d61fe9 100644 --- a/src/renderer/missionWindow/missions/DeliverPayload.jsx +++ b/src/renderer/missionWindow/missions/DeliverPayload.jsx @@ -1,21 +1,44 @@ import PropTypes from 'prop-types'; import React from 'react'; -import { job, mission } from '../../../util/util'; +import Land from './jobs/Land'; +import PayloadDrop from './jobs/PayloadDrop'; +import Takeoff from './jobs/Takeoff'; -export default function DeliverPayload({ index }) { +import Mission from './Mission'; + +const jobs = [ + { + name: 'takeoff', + description: 'Takeoff', + layout: Takeoff, + optional: true, + pausable: false, + }, + { + name: 'payloadDrop', + description: 'Drop payload to target', + layout: PayloadDrop, + }, + { + name: 'land', + description: 'Land', + layout: Land, + pausable: false, + }, +]; + +const propTypes = { + mission: PropTypes.shape({ + name: PropTypes.string.isRequired, + description: PropTypes.string.isRequired, + }).isRequired, +}; + +export default function GetTarget({ mission }) { return ( -
-

Deliver payload to target

- - - - - -
+ ); } -DeliverPayload.propTypes = { - index: PropTypes.number.isRequired, -}; +GetTarget.propTypes = propTypes; diff --git a/src/renderer/missionWindow/missions/FindTarget.jsx b/src/renderer/missionWindow/missions/FindTarget.jsx index 1912ea41..3251e366 100644 --- a/src/renderer/missionWindow/missions/FindTarget.jsx +++ b/src/renderer/missionWindow/missions/FindTarget.jsx @@ -1,19 +1,56 @@ import PropTypes from 'prop-types'; import React from 'react'; -import { mission } from '../../../util/util'; +import DetailedSearch from './jobs/DetailedSearch'; +import Land from './jobs/Land'; +import ISRSearch from './jobs/ISRSearch'; +import QuickScan from './jobs/QuickScan'; +import Takeoff from './jobs/Takeoff'; -export default function FindTarget({ index }) { +import Mission from './Mission'; + +const jobs = [ + { + name: 'takeoff', + description: 'Takeoff', + layout: Takeoff, + pausable: false, + }, + { + name: 'isrSearch', + description: 'ISR Search', + layout: ISRSearch, + }, + { + name: 'quickScan', + description: 'Quick Scan', + layout: QuickScan, + }, + { + name: 'detailedSearch', + description: 'Detailed Search', + layout: DetailedSearch, + }, + { + name: 'land', + description: 'Land', + layout: Land, + optional: true, + pausable: false, + }, +]; + +const propTypes = { + mission: PropTypes.shape({ + name: PropTypes.string.isRequired, + description: PropTypes.string.isRequired, + }).isRequired, +}; + +export default function GetTarget({ mission }) { return ( -
-

Find the target

- - - -
+ ); } -FindTarget.propTypes = { - index: PropTypes.number.isRequired, -}; +GetTarget.propTypes = propTypes; diff --git a/src/renderer/missionWindow/missions/GetTarget.jsx b/src/renderer/missionWindow/missions/GetTarget.jsx index 6ad3614b..0c5fee1c 100644 --- a/src/renderer/missionWindow/missions/GetTarget.jsx +++ b/src/renderer/missionWindow/missions/GetTarget.jsx @@ -1,100 +1,35 @@ -import { ipcRenderer } from 'electron'; // eslint-disable-line import/no-extraneous-dependencies -import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import React from 'react'; import DeliverTarget from './jobs/DeliverTarget'; import RetrieveTarget from './jobs/RetrieveTarget'; -import { mission } from '../../../util/util'; - -const layouts = { - deliverTarget: DeliverTarget, - retrieveTarget: RetrieveTarget, +import Mission from './Mission'; + +const jobs = [ + { + name: 'retrieveTarget', + description: 'Retrieve the target', + layout: RetrieveTarget, + }, + { + name: 'deliverTarget', + description: 'Deliver the target back to base', + layout: DeliverTarget, + }, +]; + +const propTypes = { + mission: PropTypes.shape({ + name: PropTypes.string.isRequired, + description: PropTypes.string.isRequired, + }).isRequired, }; -/* - * We can create a Mission class that this class extends that will have these functions. - * The Mission class can take in a parameter that will define all the jobs for that mission. - * All Mission classes have very similar logic so this is why I add this comment here. - * Stuff that we probably can pass to the Mission class: - * 1. Jobs and their respective layout - * 2. Description of Mission to load to the h2 component in the info container (see below). - * TODO: Add a Mission class that this class extends. The class can be in the same directory. - */ - -const Default = () => ( -
-
-

Get the target

-
-
- -
-
-); - -export default class GetTarget extends Component { - constructor(props) { - super(props); - - this.state = { - currentJobIndex: -1, - jobs: [ - { - name: 'retrieveTarget', - }, - { - name: 'deliverTarget', - }, - ], - }; - - this.nextJob = this.nextJob.bind(this); - this.completeJob = this.completeJob.bind(this); - } - - componentDidMount() { - ipcRenderer.on('startMission', () => { - this.setState({ currentJobIndex: 0 }); - }); - - ipcRenderer.on('stopMission', () => { - this.setState({ currentJobIndex: -1 }); - }); - - ipcRenderer.on('completeJob', this.completeJob); - ipcRenderer.on('nextJob', this.nextJob); - } - - nextJob() { - const { currentJobIndex } = this.state; - this.setState({ currentJobIndex: currentJobIndex + 1 }); - } - - completeJob() { - const { currentJobIndex, jobs } = this.state; - - if (currentJobIndex === jobs.length - 1) { - /* - * This will forward a notification to the job window that will create a button - * that, when clicked, will send the "completeMission" notification. The - * mission container will handle closing the mission window when the notification - * is sent. - */ - ipcRenderer.send('post', 'showCompleteMissionButton'); - } else { - ipcRenderer.send('post', 'showNextJobButton'); - } - } - - render() { - const { currentJobIndex } = this.state; - - const Layout = currentJobIndex >= 0 ? layouts[currentJobIndex] : Default; - - return ( -
- -
- ); - } +export default function GetTarget({ mission }) { + return ( + + ); } + +GetTarget.propTypes = propTypes; diff --git a/src/renderer/missionWindow/missions/Mission.jsx b/src/renderer/missionWindow/missions/Mission.jsx new file mode 100644 index 00000000..9f6ba5a2 --- /dev/null +++ b/src/renderer/missionWindow/missions/Mission.jsx @@ -0,0 +1,101 @@ +import { ipcRenderer } from 'electron'; // eslint-disable-line import/no-extraneous-dependencies +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; + +import { mission as missionUtil } from '../../../util/util'; + +// TODO: Make this component more pretty. +const Default = ({ description }) => ( +
+
+

{description}

+
+
+ +
+
+); + +Default.propTypes = { + description: PropTypes.string.isRequired, +}; + +const propTypes = { + mission: PropTypes.shape({ + name: PropTypes.string.isRequired, + description: PropTypes.string.isRequired, + }).isRequired, + jobs: PropTypes.arrayOf( + PropTypes.shape({ + name: PropTypes.string.isRequired, + layout: PropTypes.func.isRequired, + }).isRequired, + ).isRequired, +}; + +export default class Mission extends Component { + constructor(props) { + super(props); + + this.state = { + currentJobIndex: -1, + }; + + this.nextJob = this.nextJob.bind(this); + this.completeJob = this.completeJob.bind(this); + } + + componentDidMount() { + ipcRenderer.on('startMission', () => { + this.setState({ currentJobIndex: 0 }); + }); + + ipcRenderer.on('stopMission', () => { + this.setState({ currentJobIndex: -1 }); + }); + + ipcRenderer.on('completeJob', this.completeJob); + ipcRenderer.on('nextJob', this.nextJob); + } + + nextJob() { + const { currentJobIndex } = this.state; + this.setState({ currentJobIndex: currentJobIndex + 1 }); + } + + completeJob() { + const { currentJobIndex } = this.state; + const { jobs } = this.props; + + if (currentJobIndex === jobs.length - 1) { + /* + * This will forward a notification to the job window that will create a button + * that, when clicked, will send the "completeMission" notification. The + * mission container will handle closing the mission window when the notification + * is sent. + */ + ipcRenderer.send('post', 'showCompleteMissionButton'); + } else { + ipcRenderer.send('post', 'showNextJobButton'); + } + } + + render() { + const { currentJobIndex: index } = this.state; + const { mission, jobs } = this.props; + + const Layout = index >= 0 ? jobs[index].layout : Default; + + return ( +
+ = 0 ? jobs[index].description : mission.description} + optional={index >= 0 && jobs[index].optional} + /> +
+ ); + } +} + +Mission.propTypes = propTypes; diff --git a/src/renderer/missionWindow/missions/jobs/DeliverTarget.jsx b/src/renderer/missionWindow/missions/jobs/DeliverTarget.jsx index e69de29b..83e0b163 100644 --- a/src/renderer/missionWindow/missions/jobs/DeliverTarget.jsx +++ b/src/renderer/missionWindow/missions/jobs/DeliverTarget.jsx @@ -0,0 +1,15 @@ +import PropTypes from 'prop-types'; +import React from 'react'; + +import Job from './Job'; + +const propTypes = { + description: PropTypes.string.isRequired, + lastJob: PropTypes.bool.isRequired, +}; + +export default function DeliverTarget(props) { + return ; +} + +DeliverTarget.propTypes = propTypes; diff --git a/src/renderer/missionWindow/missions/jobs/DetailedSearch.jsx b/src/renderer/missionWindow/missions/jobs/DetailedSearch.jsx index e69de29b..ed4bb680 100644 --- a/src/renderer/missionWindow/missions/jobs/DetailedSearch.jsx +++ b/src/renderer/missionWindow/missions/jobs/DetailedSearch.jsx @@ -0,0 +1,15 @@ +import PropTypes from 'prop-types'; +import React from 'react'; + +import Job from './Job'; + +const propTypes = { + description: PropTypes.string.isRequired, + lastJob: PropTypes.bool.isRequired, +}; + +export default function DetailedSearch(props) { + return ; +} + +DetailedSearch.propTypes = propTypes; diff --git a/src/renderer/missionWindow/missions/jobs/ISRSearch.jsx b/src/renderer/missionWindow/missions/jobs/ISRSearch.jsx index e69de29b..1b79a88d 100644 --- a/src/renderer/missionWindow/missions/jobs/ISRSearch.jsx +++ b/src/renderer/missionWindow/missions/jobs/ISRSearch.jsx @@ -0,0 +1,15 @@ +import PropTypes from 'prop-types'; +import React from 'react'; + +import Job from './Job'; + +const propTypes = { + description: PropTypes.string.isRequired, + lastJob: PropTypes.bool.isRequired, +}; + +export default function ISRSearch(props) { + return ; +} + +ISRSearch.propTypes = propTypes; diff --git a/src/renderer/missionWindow/missions/jobs/Job.jsx b/src/renderer/missionWindow/missions/jobs/Job.jsx new file mode 100644 index 00000000..d1be7074 --- /dev/null +++ b/src/renderer/missionWindow/missions/jobs/Job.jsx @@ -0,0 +1,147 @@ +import { ipcRenderer } from 'electron'; // eslint-disable-line import/no-extraneous-dependencies +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; + +import { job, mission } from '../../../../util/util'; + +const propTypes = { + description: PropTypes.string.isRequired, + lastJob: PropTypes.bool.isRequired, + optional: PropTypes.bool, + pausable: PropTypes.bool, +}; + +const defaultProps = { + optional: false, + pausable: true, +}; + +const defaultState = { + // True if the button should be shown, false if it should be hidden. + startJobButton: true, + stopMissionButton: true, + pauseJobButton: false, + resumeJobButton: false, + nextJobButton: false, + completeMissionButton: false, +}; + +export default class Job extends Component { + constructor(props) { + super(props); + + const { lastJob, pausable } = this.props; + + this.state = { ...defaultState }; + + this.sendStartJob = this.sendStartJob.bind(this); + this.sendStopMission = this.sendStopMission.bind(this); + this.sendPauseJob = this.sendPauseJob.bind(this); + this.sendResumeJob = this.sendResumeJob.bind(this); + this.sendNextJob = this.sendNextJob.bind(this); + this.sendCompleteMission = this.sendCompleteMission.bind(this); + + this.buttons = { + startJobButton: , + stopMissionButton: , + pauseJobButton: , + resumeJobButton: , + nextJobButton: , + completeMissionButton: , + skipJobButton: , + }; + } + + componentDidMount() { + ipcRenderer.on('showNextJobButton', () => { + this.setState({ nextJobButton: true }); + }); + ipcRenderer.on('showCompleteMissionButton', () => { + this.setState({ completeMissionButton: true }); + }); + } + + sendStartJob() { + console.log(this.state); + + this.setState({ + startJobButton: false, + pauseJobButton: true, + }); + + job.sendStartJob(); + } + + sendStopMission() { + // Reset state for future use of this job. + this.setState({ ...defaultState }); + + mission.sendStopMission(); + } + + sendPauseJob() { + this.setState({ + pauseJobButton: false, + resumeJobButton: true, + }); + + job.sendPauseJob(); + } + + sendResumeJob() { + this.setState({ + resumeJobButton: false, + pauseJobButton: true, + }); + + job.sendResumeJob(); + } + + sendNextJob() { + // Reset state for future use of this job. + this.setState({ ...defaultState }); + + ipcRenderer.send('post', 'nextJob'); + } + + sendCompleteMission() { + // Reset state for future use of this job. + this.setState({ ...defaultState }); + + mission.sendCompleteMission(); + } + + render() { + const { description, optional } = this.props; + const { + startJobButton, + stopMissionButton, + pauseJobButton, + resumeJobButton, + nextJobButton, + completeMissionButton, + } = this.state; + + return ( +
+
+

{description}

+
+
+ {startJobButton && this.buttons.startJobButton} + {!nextJobButton && !completeMissionButton + && pauseJobButton && this.buttons.pauseJobButton} + {!nextJobButton && !completeMissionButton + && resumeJobButton && this.buttons.resumeJobButton} + {nextJobButton && this.buttons.nextJobButton} + {completeMissionButton && this.buttons.completeMissionButton} + {stopMissionButton && this.buttons.stopMissionButton} + {!nextJobButton && !completeMissionButton && optional && this.buttons.skipJobButton} +
+
+ ); + } +} + +Job.propTypes = propTypes; +Job.defaultProps = defaultProps; diff --git a/src/renderer/missionWindow/missions/jobs/Land.jsx b/src/renderer/missionWindow/missions/jobs/Land.jsx index e69de29b..61f218e3 100644 --- a/src/renderer/missionWindow/missions/jobs/Land.jsx +++ b/src/renderer/missionWindow/missions/jobs/Land.jsx @@ -0,0 +1,15 @@ +import PropTypes from 'prop-types'; +import React from 'react'; + +import Job from './Job'; + +const propTypes = { + description: PropTypes.string.isRequired, + lastJob: PropTypes.bool.isRequired, +}; + +export default function Land(props) { + return ; +} + +Land.propTypes = propTypes; diff --git a/src/renderer/missionWindow/missions/jobs/Loiter.jsx b/src/renderer/missionWindow/missions/jobs/Loiter.jsx index e69de29b..2a9d11a0 100644 --- a/src/renderer/missionWindow/missions/jobs/Loiter.jsx +++ b/src/renderer/missionWindow/missions/jobs/Loiter.jsx @@ -0,0 +1,15 @@ +import PropTypes from 'prop-types'; +import React from 'react'; + +import Job from './Job'; + +const propTypes = { + description: PropTypes.string.isRequired, + lastJob: PropTypes.bool.isRequired, +}; + +export default function Loiter(props) { + return ; +} + +Loiter.propTypes = propTypes; diff --git a/src/renderer/missionWindow/missions/jobs/PayloadDrop.jsx b/src/renderer/missionWindow/missions/jobs/PayloadDrop.jsx index e69de29b..b1ccc0ca 100644 --- a/src/renderer/missionWindow/missions/jobs/PayloadDrop.jsx +++ b/src/renderer/missionWindow/missions/jobs/PayloadDrop.jsx @@ -0,0 +1,15 @@ +import PropTypes from 'prop-types'; +import React from 'react'; + +import Job from './Job'; + +const propTypes = { + description: PropTypes.string.isRequired, + lastJob: PropTypes.bool.isRequired, +}; + +export default function PayloadDrop(props) { + return ; +} + +PayloadDrop.propTypes = propTypes; diff --git a/src/renderer/missionWindow/missions/jobs/QuickScan.jsx b/src/renderer/missionWindow/missions/jobs/QuickScan.jsx index e69de29b..3df6b87d 100644 --- a/src/renderer/missionWindow/missions/jobs/QuickScan.jsx +++ b/src/renderer/missionWindow/missions/jobs/QuickScan.jsx @@ -0,0 +1,15 @@ +import PropTypes from 'prop-types'; +import React from 'react'; + +import Job from './Job'; + +const propTypes = { + description: PropTypes.string.isRequired, + lastJob: PropTypes.bool.isRequired, +}; + +export default function QuickScan(props) { + return ; +} + +QuickScan.propTypes = propTypes; diff --git a/src/renderer/missionWindow/missions/jobs/RetrieveTarget.jsx b/src/renderer/missionWindow/missions/jobs/RetrieveTarget.jsx index e69de29b..1d54fb1d 100644 --- a/src/renderer/missionWindow/missions/jobs/RetrieveTarget.jsx +++ b/src/renderer/missionWindow/missions/jobs/RetrieveTarget.jsx @@ -0,0 +1,15 @@ +import PropTypes from 'prop-types'; +import React from 'react'; + +import Job from './Job'; + +const propTypes = { + description: PropTypes.string.isRequired, + lastJob: PropTypes.bool.isRequired, +}; + +export default function RetrieveTarget(props) { + return ; +} + +RetrieveTarget.propTypes = propTypes; diff --git a/src/renderer/missionWindow/missions/jobs/Takeoff.jsx b/src/renderer/missionWindow/missions/jobs/Takeoff.jsx index e69de29b..b5a5371e 100644 --- a/src/renderer/missionWindow/missions/jobs/Takeoff.jsx +++ b/src/renderer/missionWindow/missions/jobs/Takeoff.jsx @@ -0,0 +1,15 @@ +import PropTypes from 'prop-types'; +import React from 'react'; + +import Job from './Job'; + +const propTypes = { + description: PropTypes.string.isRequired, + lastJob: PropTypes.bool.isRequired, +}; + +export default function Takeoff(props) { + return ; +} + +Takeoff.propTypes = propTypes; From 0b744832fcacd9df7c6249496372fa8f42335283 Mon Sep 17 00:00:00 2001 From: Paul Matthew Barrameda Date: Mon, 25 Mar 2019 12:42:40 -0700 Subject: [PATCH 023/152] Small change Current version struggles to keep mission window state as well as job state. The mission window can only keep track of one misison at a time, but it should be able to keep track of all missions. If I start one mission and start another, the mission window will forget all information about the first mission. There should be a fix to this. --- src/renderer/fixtures/completeJobFixture.js | 7 +++- src/renderer/fixtures/index.js | 5 ++- .../missionWindow/missions/DeliverPayload.jsx | 3 ++ .../missionWindow/missions/FindTarget.jsx | 7 ++++ .../missionWindow/missions/GetTarget.jsx | 4 ++ .../missionWindow/missions/Mission.jsx | 24 ++++++++---- .../missions/jobs/DeliverTarget.jsx | 7 +++- .../missions/jobs/DetailedSearch.jsx | 7 +++- .../missionWindow/missions/jobs/ISRSearch.jsx | 7 +++- .../missionWindow/missions/jobs/Job.jsx | 38 +++++++++---------- .../missionWindow/missions/jobs/Land.jsx | 7 +++- .../missionWindow/missions/jobs/Loiter.jsx | 7 +++- .../missions/jobs/PayloadDrop.jsx | 7 +++- .../missionWindow/missions/jobs/QuickScan.jsx | 7 +++- .../missions/jobs/RetrieveTarget.jsx | 7 +++- .../missionWindow/missions/jobs/Takeoff.jsx | 7 +++- 16 files changed, 112 insertions(+), 39 deletions(-) diff --git a/src/renderer/fixtures/completeJobFixture.js b/src/renderer/fixtures/completeJobFixture.js index 833cbdd9..d2dbe1f2 100644 --- a/src/renderer/fixtures/completeJobFixture.js +++ b/src/renderer/fixtures/completeJobFixture.js @@ -1,5 +1,10 @@ import { ipcRenderer } from 'electron'; // eslint-disable-line import/no-extraneous-dependencies export default function completeJob() { - ipcRenderer.send('post', 'completeJob'); + // Sends completeJob notification after receiving startJob notification. + ipcRenderer.on('startJob', () => { + setTimeout(() => { + ipcRenderer.send('post', 'completeJob'); + }, 5000); + }); } diff --git a/src/renderer/fixtures/index.js b/src/renderer/fixtures/index.js index daaaf70b..0fb4bfc4 100644 --- a/src/renderer/fixtures/index.js +++ b/src/renderer/fixtures/index.js @@ -10,8 +10,9 @@ import completeJob from './completeJobFixture'; import updateMessages from './updateMessagesFixtures'; import updateVehicles from './updateVehiclesFixtures'; +completeJob(); + setInterval(() => { - completeJob(); updateMessages(); updateVehicles(); -}, 5000); +}, 1000); diff --git a/src/renderer/missionWindow/missions/DeliverPayload.jsx b/src/renderer/missionWindow/missions/DeliverPayload.jsx index 01d61fe9..3da2d244 100644 --- a/src/renderer/missionWindow/missions/DeliverPayload.jsx +++ b/src/renderer/missionWindow/missions/DeliverPayload.jsx @@ -19,11 +19,14 @@ const jobs = [ name: 'payloadDrop', description: 'Drop payload to target', layout: PayloadDrop, + optional: false, + pausable: false, }, { name: 'land', description: 'Land', layout: Land, + optional: false, pausable: false, }, ]; diff --git a/src/renderer/missionWindow/missions/FindTarget.jsx b/src/renderer/missionWindow/missions/FindTarget.jsx index 3251e366..33f2f99c 100644 --- a/src/renderer/missionWindow/missions/FindTarget.jsx +++ b/src/renderer/missionWindow/missions/FindTarget.jsx @@ -14,22 +14,29 @@ const jobs = [ name: 'takeoff', description: 'Takeoff', layout: Takeoff, + optional: false, pausable: false, }, { name: 'isrSearch', description: 'ISR Search', layout: ISRSearch, + optional: false, + pausable: false, }, { name: 'quickScan', description: 'Quick Scan', layout: QuickScan, + optional: false, + pausable: true, }, { name: 'detailedSearch', description: 'Detailed Search', layout: DetailedSearch, + optional: false, + pausable: true, }, { name: 'land', diff --git a/src/renderer/missionWindow/missions/GetTarget.jsx b/src/renderer/missionWindow/missions/GetTarget.jsx index 0c5fee1c..ac1db4ab 100644 --- a/src/renderer/missionWindow/missions/GetTarget.jsx +++ b/src/renderer/missionWindow/missions/GetTarget.jsx @@ -11,11 +11,15 @@ const jobs = [ name: 'retrieveTarget', description: 'Retrieve the target', layout: RetrieveTarget, + optional: false, + pausable: true, }, { name: 'deliverTarget', description: 'Deliver the target back to base', layout: DeliverTarget, + optional: false, + pausable: true, }, ]; diff --git a/src/renderer/missionWindow/missions/Mission.jsx b/src/renderer/missionWindow/missions/Mission.jsx index 9f6ba5a2..bf4c9bd3 100644 --- a/src/renderer/missionWindow/missions/Mission.jsx +++ b/src/renderer/missionWindow/missions/Mission.jsx @@ -5,10 +5,10 @@ import React, { Component } from 'react'; import { mission as missionUtil } from '../../../util/util'; // TODO: Make this component more pretty. -const Default = ({ description }) => ( +const Default = ({ job }) => (
-

{description}

+

{job.description}

@@ -17,7 +17,9 @@ const Default = ({ description }) => ( ); Default.propTypes = { - description: PropTypes.string.isRequired, + job: PropTypes.shape({ + description: PropTypes.string.isRequired, + }).isRequired, }; const propTypes = { @@ -28,7 +30,10 @@ const propTypes = { jobs: PropTypes.arrayOf( PropTypes.shape({ name: PropTypes.string.isRequired, + description: PropTypes.string.isRequired, layout: PropTypes.func.isRequired, + pausable: PropTypes.bool.isRequired, + optional: PropTypes.bool.isRequired, }).isRequired, ).isRequired, }; @@ -67,6 +72,8 @@ export default class Mission extends Component { const { currentJobIndex } = this.state; const { jobs } = this.props; + console.log('completed', jobs[currentJobIndex].name); + if (currentJobIndex === jobs.length - 1) { /* * This will forward a notification to the job window that will create a button @@ -81,17 +88,18 @@ export default class Mission extends Component { } render() { - const { currentJobIndex: index } = this.state; + const { currentJobIndex } = this.state; const { mission, jobs } = this.props; - const Layout = index >= 0 ? jobs[index].layout : Default; + console.log('now going to', currentJobIndex >= 0 ? jobs[currentJobIndex].name : 'main page'); + + const Layout = currentJobIndex >= 0 ? jobs[currentJobIndex].layout : Default; return (
= 0 ? jobs[index].description : mission.description} - optional={index >= 0 && jobs[index].optional} + lastJob={currentJobIndex === jobs.length - 1} + job={currentJobIndex >= 0 ? jobs[currentJobIndex] : mission} />
); diff --git a/src/renderer/missionWindow/missions/jobs/DeliverTarget.jsx b/src/renderer/missionWindow/missions/jobs/DeliverTarget.jsx index 83e0b163..fcea0133 100644 --- a/src/renderer/missionWindow/missions/jobs/DeliverTarget.jsx +++ b/src/renderer/missionWindow/missions/jobs/DeliverTarget.jsx @@ -4,7 +4,12 @@ import React from 'react'; import Job from './Job'; const propTypes = { - description: PropTypes.string.isRequired, + job: PropTypes.shape({ + name: PropTypes.string.isRequired, + description: PropTypes.string.isRequired, + optional: PropTypes.bool.isRequired, + pausable: PropTypes.bool.isRequired, + }).isRequired, lastJob: PropTypes.bool.isRequired, }; diff --git a/src/renderer/missionWindow/missions/jobs/DetailedSearch.jsx b/src/renderer/missionWindow/missions/jobs/DetailedSearch.jsx index ed4bb680..fc2b6f1e 100644 --- a/src/renderer/missionWindow/missions/jobs/DetailedSearch.jsx +++ b/src/renderer/missionWindow/missions/jobs/DetailedSearch.jsx @@ -4,7 +4,12 @@ import React from 'react'; import Job from './Job'; const propTypes = { - description: PropTypes.string.isRequired, + job: PropTypes.shape({ + name: PropTypes.string.isRequired, + description: PropTypes.string.isRequired, + optional: PropTypes.bool.isRequired, + pausable: PropTypes.bool.isRequired, + }).isRequired, lastJob: PropTypes.bool.isRequired, }; diff --git a/src/renderer/missionWindow/missions/jobs/ISRSearch.jsx b/src/renderer/missionWindow/missions/jobs/ISRSearch.jsx index 1b79a88d..ff7e71d1 100644 --- a/src/renderer/missionWindow/missions/jobs/ISRSearch.jsx +++ b/src/renderer/missionWindow/missions/jobs/ISRSearch.jsx @@ -4,7 +4,12 @@ import React from 'react'; import Job from './Job'; const propTypes = { - description: PropTypes.string.isRequired, + job: PropTypes.shape({ + name: PropTypes.string.isRequired, + description: PropTypes.string.isRequired, + optional: PropTypes.bool.isRequired, + pausable: PropTypes.bool.isRequired, + }).isRequired, lastJob: PropTypes.bool.isRequired, }; diff --git a/src/renderer/missionWindow/missions/jobs/Job.jsx b/src/renderer/missionWindow/missions/jobs/Job.jsx index d1be7074..bf9054fc 100644 --- a/src/renderer/missionWindow/missions/jobs/Job.jsx +++ b/src/renderer/missionWindow/missions/jobs/Job.jsx @@ -2,18 +2,16 @@ import { ipcRenderer } from 'electron'; // eslint-disable-line import/no-extrane import PropTypes from 'prop-types'; import React, { Component } from 'react'; -import { job, mission } from '../../../../util/util'; +import { job as jobUtil, mission } from '../../../../util/util'; const propTypes = { - description: PropTypes.string.isRequired, + job: PropTypes.shape({ + name: PropTypes.string.isRequired, + description: PropTypes.string.isRequired, + optional: PropTypes.bool.isRequired, + pausable: PropTypes.bool.isRequired, + }).isRequired, lastJob: PropTypes.bool.isRequired, - optional: PropTypes.bool, - pausable: PropTypes.bool, -}; - -const defaultProps = { - optional: false, - pausable: true, }; const defaultState = { @@ -30,7 +28,7 @@ export default class Job extends Component { constructor(props) { super(props); - const { lastJob, pausable } = this.props; + const { lastJob, job } = this.props; this.state = { ...defaultState }; @@ -44,7 +42,7 @@ export default class Job extends Component { this.buttons = { startJobButton: , stopMissionButton: , - pauseJobButton: , + pauseJobButton: , resumeJobButton: , nextJobButton: , completeMissionButton: , @@ -62,14 +60,17 @@ export default class Job extends Component { } sendStartJob() { - console.log(this.state); + const { job } = this.props; this.setState({ startJobButton: false, pauseJobButton: true, }); - job.sendStartJob(); + jobUtil.sendStartJob({ + jobType: job.name, + // TODO: Add missionInfo. + }); } sendStopMission() { @@ -85,7 +86,7 @@ export default class Job extends Component { resumeJobButton: true, }); - job.sendPauseJob(); + jobUtil.sendPauseJob(); } sendResumeJob() { @@ -94,7 +95,7 @@ export default class Job extends Component { pauseJobButton: true, }); - job.sendResumeJob(); + jobUtil.sendResumeJob(); } sendNextJob() { @@ -112,7 +113,7 @@ export default class Job extends Component { } render() { - const { description, optional } = this.props; + const { job } = this.props; const { startJobButton, stopMissionButton, @@ -125,7 +126,7 @@ export default class Job extends Component { return (
-

{description}

+

{job.description}

{startJobButton && this.buttons.startJobButton} @@ -136,7 +137,7 @@ export default class Job extends Component { {nextJobButton && this.buttons.nextJobButton} {completeMissionButton && this.buttons.completeMissionButton} {stopMissionButton && this.buttons.stopMissionButton} - {!nextJobButton && !completeMissionButton && optional && this.buttons.skipJobButton} + {!nextJobButton && !completeMissionButton && job.optional && this.buttons.skipJobButton}
); @@ -144,4 +145,3 @@ export default class Job extends Component { } Job.propTypes = propTypes; -Job.defaultProps = defaultProps; diff --git a/src/renderer/missionWindow/missions/jobs/Land.jsx b/src/renderer/missionWindow/missions/jobs/Land.jsx index 61f218e3..1aa7fb27 100644 --- a/src/renderer/missionWindow/missions/jobs/Land.jsx +++ b/src/renderer/missionWindow/missions/jobs/Land.jsx @@ -4,7 +4,12 @@ import React from 'react'; import Job from './Job'; const propTypes = { - description: PropTypes.string.isRequired, + job: PropTypes.shape({ + name: PropTypes.string.isRequired, + description: PropTypes.string.isRequired, + optional: PropTypes.bool.isRequired, + pausable: PropTypes.bool.isRequired, + }).isRequired, lastJob: PropTypes.bool.isRequired, }; diff --git a/src/renderer/missionWindow/missions/jobs/Loiter.jsx b/src/renderer/missionWindow/missions/jobs/Loiter.jsx index 2a9d11a0..8e1b5fe8 100644 --- a/src/renderer/missionWindow/missions/jobs/Loiter.jsx +++ b/src/renderer/missionWindow/missions/jobs/Loiter.jsx @@ -4,7 +4,12 @@ import React from 'react'; import Job from './Job'; const propTypes = { - description: PropTypes.string.isRequired, + job: PropTypes.shape({ + name: PropTypes.string.isRequired, + description: PropTypes.string.isRequired, + optional: PropTypes.bool.isRequired, + pausable: PropTypes.bool.isRequired, + }).isRequired, lastJob: PropTypes.bool.isRequired, }; diff --git a/src/renderer/missionWindow/missions/jobs/PayloadDrop.jsx b/src/renderer/missionWindow/missions/jobs/PayloadDrop.jsx index b1ccc0ca..b91c914b 100644 --- a/src/renderer/missionWindow/missions/jobs/PayloadDrop.jsx +++ b/src/renderer/missionWindow/missions/jobs/PayloadDrop.jsx @@ -4,7 +4,12 @@ import React from 'react'; import Job from './Job'; const propTypes = { - description: PropTypes.string.isRequired, + job: PropTypes.shape({ + name: PropTypes.string.isRequired, + description: PropTypes.string.isRequired, + optional: PropTypes.bool.isRequired, + pausable: PropTypes.bool.isRequired, + }).isRequired, lastJob: PropTypes.bool.isRequired, }; diff --git a/src/renderer/missionWindow/missions/jobs/QuickScan.jsx b/src/renderer/missionWindow/missions/jobs/QuickScan.jsx index 3df6b87d..3e9b14d4 100644 --- a/src/renderer/missionWindow/missions/jobs/QuickScan.jsx +++ b/src/renderer/missionWindow/missions/jobs/QuickScan.jsx @@ -4,7 +4,12 @@ import React from 'react'; import Job from './Job'; const propTypes = { - description: PropTypes.string.isRequired, + job: PropTypes.shape({ + name: PropTypes.string.isRequired, + description: PropTypes.string.isRequired, + optional: PropTypes.bool.isRequired, + pausable: PropTypes.bool.isRequired, + }).isRequired, lastJob: PropTypes.bool.isRequired, }; diff --git a/src/renderer/missionWindow/missions/jobs/RetrieveTarget.jsx b/src/renderer/missionWindow/missions/jobs/RetrieveTarget.jsx index 1d54fb1d..17f7d0af 100644 --- a/src/renderer/missionWindow/missions/jobs/RetrieveTarget.jsx +++ b/src/renderer/missionWindow/missions/jobs/RetrieveTarget.jsx @@ -4,7 +4,12 @@ import React from 'react'; import Job from './Job'; const propTypes = { - description: PropTypes.string.isRequired, + job: PropTypes.shape({ + name: PropTypes.string.isRequired, + description: PropTypes.string.isRequired, + optional: PropTypes.bool.isRequired, + pausable: PropTypes.bool.isRequired, + }).isRequired, lastJob: PropTypes.bool.isRequired, }; diff --git a/src/renderer/missionWindow/missions/jobs/Takeoff.jsx b/src/renderer/missionWindow/missions/jobs/Takeoff.jsx index b5a5371e..c51b19b2 100644 --- a/src/renderer/missionWindow/missions/jobs/Takeoff.jsx +++ b/src/renderer/missionWindow/missions/jobs/Takeoff.jsx @@ -4,7 +4,12 @@ import React from 'react'; import Job from './Job'; const propTypes = { - description: PropTypes.string.isRequired, + job: PropTypes.shape({ + name: PropTypes.string.isRequired, + description: PropTypes.string.isRequired, + optional: PropTypes.bool.isRequired, + pausable: PropTypes.bool.isRequired, + }).isRequired, lastJob: PropTypes.bool.isRequired, }; From 2b8929d9b9b20c22578cd9bba6ddbdead22118eb Mon Sep 17 00:00:00 2001 From: Paul Matthew Barrameda Date: Tue, 26 Mar 2019 10:16:40 -0700 Subject: [PATCH 024/152] Modified some files for TypeScript support Work in progress. Note: I realized Leaflet loves to maky a lot of things any type, and our linter doesnt like that --- .eslintrc.yml | 70 +++++++++- package.json | 13 +- {resources => src/config}/config.json | 0 {resources => src/config}/images/arrow.png | Bin {resources => src/config}/images/icon.png | Bin .../config}/images/logo/ngcp_calpoly.png | Bin .../config}/images/logo/ngcp_pomona.png | Bin .../config}/images/markers/poi_fp.png | Bin .../config}/images/markers/poi_unkwn.png | Bin .../config}/images/markers/poi_vld.png | Bin .../config}/images/markers/vehicles/plane.png | Bin .../images/markers/vehicles/plane_red.png | Bin .../config}/images/markers/vehicles/rover.png | Bin .../images/markers/vehicles/rover_red.png | Bin .../config}/images/other/fullmoon.png | Bin .../config}/images/other/fullmoon_hover.png | Bin .../config}/images/other/geolocation.png | Bin .../images/other/geolocation_hover.png | Bin .../config}/images/other/moon.png | Bin .../config}/images/other/moon_hover.png | Bin {resources => src/config}/images/pin.png | Bin resources/index.js => src/config/index.ts | 2 +- {resources => src/config}/locations.json | 0 {resources => src/config}/vehicle.json | 0 src/main/{index.js => index.ts} | 128 ++++++++++++------ ...eteJobFixture.js => completeJobFixture.ts} | 2 +- src/renderer/fixtures/{index.js => index.ts} | 2 +- ...sFixtures.js => updateMessagesFixtures.ts} | 2 +- ...sFixtures.js => updateVehiclesFixtures.ts} | 4 +- src/renderer/index.js | 2 +- .../{LogContainer.jsx => LogContainer.tsx} | 62 ++++++--- src/renderer/mainWindow/log/log.css | 2 +- .../mainWindow/map/CachedTileLayer.js | 19 --- .../mainWindow/map/CachedTileLayer.ts | 22 +++ src/renderer/mainWindow/map/MapContainer.jsx | 2 +- src/renderer/mainWindow/map/VehicleMarker.jsx | 2 +- ....CachedTileLayer.js => CachedTileLayer.js} | 18 +-- src/util/types.ts | 15 ++ src/util/util.js | 2 +- tsconfig.json | 11 ++ typings/external.d.ts | 2 + webpack.config.js | 2 +- 42 files changed, 271 insertions(+), 113 deletions(-) rename {resources => src/config}/config.json (100%) rename {resources => src/config}/images/arrow.png (100%) mode change 100755 => 100644 rename {resources => src/config}/images/icon.png (100%) mode change 100755 => 100644 rename {resources => src/config}/images/logo/ngcp_calpoly.png (100%) rename {resources => src/config}/images/logo/ngcp_pomona.png (100%) rename {resources => src/config}/images/markers/poi_fp.png (100%) rename {resources => src/config}/images/markers/poi_unkwn.png (100%) rename {resources => src/config}/images/markers/poi_vld.png (100%) rename {resources => src/config}/images/markers/vehicles/plane.png (100%) rename {resources => src/config}/images/markers/vehicles/plane_red.png (100%) rename {resources => src/config}/images/markers/vehicles/rover.png (100%) rename {resources => src/config}/images/markers/vehicles/rover_red.png (100%) rename {resources => src/config}/images/other/fullmoon.png (100%) rename {resources => src/config}/images/other/fullmoon_hover.png (100%) rename {resources => src/config}/images/other/geolocation.png (100%) rename {resources => src/config}/images/other/geolocation_hover.png (100%) rename {resources => src/config}/images/other/moon.png (100%) rename {resources => src/config}/images/other/moon_hover.png (100%) rename {resources => src/config}/images/pin.png (100%) mode change 100755 => 100644 rename resources/index.js => src/config/index.ts (97%) rename {resources => src/config}/locations.json (100%) rename {resources => src/config}/vehicle.json (100%) rename src/main/{index.js => index.ts} (75%) rename src/renderer/fixtures/{completeJobFixture.js => completeJobFixture.ts} (86%) rename src/renderer/fixtures/{index.js => index.ts} (99%) rename src/renderer/fixtures/{updateMessagesFixtures.js => updateMessagesFixtures.ts} (91%) rename src/renderer/fixtures/{updateVehiclesFixtures.js => updateVehiclesFixtures.ts} (87%) rename src/renderer/mainWindow/log/{LogContainer.jsx => LogContainer.tsx} (67%) delete mode 100644 src/renderer/mainWindow/map/CachedTileLayer.js create mode 100644 src/renderer/mainWindow/map/CachedTileLayer.ts rename src/util/{TileLayer.CachedTileLayer.js => CachedTileLayer.js} (92%) create mode 100644 src/util/types.ts create mode 100644 tsconfig.json create mode 100644 typings/external.d.ts diff --git a/.eslintrc.yml b/.eslintrc.yml index beca84a2..36f720ea 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -1,17 +1,73 @@ -# Rules to lint JavaScript +# Use typescript-eslint to lint our TypeScript code +parser: "@typescript-eslint/parser" -# Use Babel as we use ES6 code -parser: babel-eslint +# Use Airbnb rules to check code +extends: + - airbnb + - plugin:@typescript-eslint/recommended + +plugins: + - import env: browser: true node: true -# Use Airbnb rules to lint code -extends: airbnb +# TypeScript compatibility +settings: + import/parsers: + "@typescript-eslint/parser": + - .ts + - .tsx + import/resolver: + typescript: {} -# Added rules +# Additional rules rules: linebreak-style: off + + "@typescript-eslint/explicit-function-return-type": error + "@typescript-eslint/no-explicit-any": error multiline-comment-style: error - import/no-commonjs: error + + # Modifying react/jsx-filename-extension to support TypeScript React files + 'react/jsx-filename-extension': + - error + - extensions: + - .jsx + - .tsx + + # @typescript-eslint/no-use-before-define is already defined + no-use-before-define: off + + # Re-adding changed Airbnb rules (removed most likely from @typescript-eslint/recommended) + "@typescript-eslint/camelcase": + - error + - properties: never + + "@typescript-eslint/indent": + - error + - 2 + - SwitchCase: 1 + VariableDeclarator: 1 + outerIIFEBody: 1 + FunctionDeclaration: + parameters: 1 + body: 1 + FunctionExpression: + parameters: 1 + body: 1 + CallExpression: + arguments: 1 + ArrayExpression: 1 + ObjectExpression: 1 + ImportDeclaration: 1 + flatTernaryExpressions: false + ignoreComments: false + + + "@typescript-eslint/no-unused-vars": + - error + - vars: all + args: after-used + ignoreRestSiblings: true diff --git a/package.json b/package.json index 4c0155f8..f5dd9f26 100644 --- a/package.json +++ b/package.json @@ -5,8 +5,8 @@ "scripts": { "postinstall": "electron-builder install-app-deps", "test": "npm run linttest", - "linttest": "eslint --ext .js,.jsx \".\" && stylelint \"**/*.css\"", - "lint": "eslint --fix --ext .js,.jsx \".\" && stylelint \"**/*.css\" --fix", + "linttest": "eslint --ext .js,.jsx,.ts,.tsx \".\" && stylelint \"**/*.css\"", + "lint": "eslint --fix --ext .js,.jsx,.ts,.tsx \".\" && stylelint \"**/*.css\" --fix", "start": "electron-webpack dev", "build": "electron-webpack && electron-builder" }, @@ -26,19 +26,28 @@ }, "devDependencies": { "@babel/preset-react": "^7.0.0", + "@types/pouchdb": "^6.3.3", + "@types/react": "^16.8.8", + "@types/react-leaflet": "^2.2.1", + "@types/react-virtualized": "^9.21.0", + "@typescript-eslint/eslint-plugin": "^1.5.0", + "@typescript-eslint/parser": "^1.5.0", "babel-eslint": "^10.0.1", "dotenv-webpack": "^1.7.0", "electron": "^3.0.2", "electron-builder": "^20.39.0", "electron-webpack": "^2.6.2", + "electron-webpack-ts": "^3.1.1", "eslint": "^5.15.3", "eslint-config-airbnb": "^17.1.0", + "eslint-import-resolver-typescript": "^1.1.1", "eslint-plugin-import": "^2.16.0", "eslint-plugin-jsx-a11y": "^6.2.1", "eslint-plugin-react": "^7.12.4", "stylelint": "^9.10.1", "stylelint-config-standard": "^18.2.0", "stylelint-order": "^2.1.0", + "typescript": "^3.3.4000", "webpack": "^4.29.6" }, "electronWebpack": { diff --git a/resources/config.json b/src/config/config.json similarity index 100% rename from resources/config.json rename to src/config/config.json diff --git a/resources/images/arrow.png b/src/config/images/arrow.png old mode 100755 new mode 100644 similarity index 100% rename from resources/images/arrow.png rename to src/config/images/arrow.png diff --git a/resources/images/icon.png b/src/config/images/icon.png old mode 100755 new mode 100644 similarity index 100% rename from resources/images/icon.png rename to src/config/images/icon.png diff --git a/resources/images/logo/ngcp_calpoly.png b/src/config/images/logo/ngcp_calpoly.png similarity index 100% rename from resources/images/logo/ngcp_calpoly.png rename to src/config/images/logo/ngcp_calpoly.png diff --git a/resources/images/logo/ngcp_pomona.png b/src/config/images/logo/ngcp_pomona.png similarity index 100% rename from resources/images/logo/ngcp_pomona.png rename to src/config/images/logo/ngcp_pomona.png diff --git a/resources/images/markers/poi_fp.png b/src/config/images/markers/poi_fp.png similarity index 100% rename from resources/images/markers/poi_fp.png rename to src/config/images/markers/poi_fp.png diff --git a/resources/images/markers/poi_unkwn.png b/src/config/images/markers/poi_unkwn.png similarity index 100% rename from resources/images/markers/poi_unkwn.png rename to src/config/images/markers/poi_unkwn.png diff --git a/resources/images/markers/poi_vld.png b/src/config/images/markers/poi_vld.png similarity index 100% rename from resources/images/markers/poi_vld.png rename to src/config/images/markers/poi_vld.png diff --git a/resources/images/markers/vehicles/plane.png b/src/config/images/markers/vehicles/plane.png similarity index 100% rename from resources/images/markers/vehicles/plane.png rename to src/config/images/markers/vehicles/plane.png diff --git a/resources/images/markers/vehicles/plane_red.png b/src/config/images/markers/vehicles/plane_red.png similarity index 100% rename from resources/images/markers/vehicles/plane_red.png rename to src/config/images/markers/vehicles/plane_red.png diff --git a/resources/images/markers/vehicles/rover.png b/src/config/images/markers/vehicles/rover.png similarity index 100% rename from resources/images/markers/vehicles/rover.png rename to src/config/images/markers/vehicles/rover.png diff --git a/resources/images/markers/vehicles/rover_red.png b/src/config/images/markers/vehicles/rover_red.png similarity index 100% rename from resources/images/markers/vehicles/rover_red.png rename to src/config/images/markers/vehicles/rover_red.png diff --git a/resources/images/other/fullmoon.png b/src/config/images/other/fullmoon.png similarity index 100% rename from resources/images/other/fullmoon.png rename to src/config/images/other/fullmoon.png diff --git a/resources/images/other/fullmoon_hover.png b/src/config/images/other/fullmoon_hover.png similarity index 100% rename from resources/images/other/fullmoon_hover.png rename to src/config/images/other/fullmoon_hover.png diff --git a/resources/images/other/geolocation.png b/src/config/images/other/geolocation.png similarity index 100% rename from resources/images/other/geolocation.png rename to src/config/images/other/geolocation.png diff --git a/resources/images/other/geolocation_hover.png b/src/config/images/other/geolocation_hover.png similarity index 100% rename from resources/images/other/geolocation_hover.png rename to src/config/images/other/geolocation_hover.png diff --git a/resources/images/other/moon.png b/src/config/images/other/moon.png similarity index 100% rename from resources/images/other/moon.png rename to src/config/images/other/moon.png diff --git a/resources/images/other/moon_hover.png b/src/config/images/other/moon_hover.png similarity index 100% rename from resources/images/other/moon_hover.png rename to src/config/images/other/moon_hover.png diff --git a/resources/images/pin.png b/src/config/images/pin.png old mode 100755 new mode 100644 similarity index 100% rename from resources/images/pin.png rename to src/config/images/pin.png diff --git a/resources/index.js b/src/config/index.ts similarity index 97% rename from resources/index.js rename to src/config/index.ts index 3a5a31d2..dee4ebae 100644 --- a/resources/index.js +++ b/src/config/index.ts @@ -1,6 +1,6 @@ /* When importing anything inside resources, please import through this file */ -/* eslint-disable camelcase */ +/* eslint-disable @typescript-eslint/camelcase */ import arrow from './images/arrow.png'; import icon from './images/icon.png'; diff --git a/resources/locations.json b/src/config/locations.json similarity index 100% rename from resources/locations.json rename to src/config/locations.json diff --git a/resources/vehicle.json b/src/config/vehicle.json similarity index 100% rename from resources/vehicle.json rename to src/config/vehicle.json diff --git a/src/main/index.js b/src/main/index.ts similarity index 75% rename from src/main/index.js rename to src/main/index.ts index 12951703..51cdff06 100644 --- a/src/main/index.js +++ b/src/main/index.ts @@ -1,10 +1,26 @@ import { // eslint-disable-line import/no-extraneous-dependencies - app, BrowserWindow, dialog, ipcMain, Menu, nativeImage, shell, Tray, + app, + BrowserWindow, + dialog, + Event, + ipcMain, + Menu, + MenuItemConstructorOptions, + nativeImage, + shell, + Tray, } from 'electron'; import fs from 'fs'; import moment from 'moment'; -import { images, locations } from '../../resources/index'; +import { images, locations as locationsConfig } from '../config/index'; + +interface LocationLabel { + [key: string]: { lat: number; lng: number; zoom: number }; +} + +// Recast locations to LocationLabel to allow us to use keys properly. +const locations: LocationLabel = locationsConfig; /* * This key is required to enable geolocation in the application. @@ -25,24 +41,24 @@ const icon = nativeImage.createFromDataURL(images.icon); let quitting = false; // References to window objects. -let mainWindow; -let missionWindow; +let mainWindow: BrowserWindow | null; +let missionWindow: BrowserWindow | null; -// Tray object -let tray; +// Tray object. +let tray: Tray; // Role added to menus to allow the user to quit the app. Shortcut is Ctrl/Cmd + Q. -const quitRole = { +const quitRole: MenuItemConstructorOptions = { label: 'Quit', accelerator: 'CommandOrControl+Q', - click() { + click(): void { quitting = true; app.quit(); }, }; // Menu prepended to menu if application is running on a Darwin-based OS. -const darwinMenu = { +const darwinMenu: MenuItemConstructorOptions = { label: 'NGCP Ground Control System', submenu: [ { role: 'about' }, @@ -64,7 +80,9 @@ const darwinMenu = { * Runs when the user wants to save a configuration of the GCS. * The configuration currently includes the map location loaded. */ -function saveConfig() { +function saveConfig(): void { + if (!mainWindow) return; + const fileName = moment().format('[GCS Configuration] YYYY-MM-DD [at] h.mm.ss A'); // Loads a window that allows the user to choose the file path for the file to be saved. @@ -83,17 +101,22 @@ function saveConfig() { filePath, data, }); - missionWindow.webContents.send('saveConfig', { - filePath, - data, - }); + + if (missionWindow) { + missionWindow.webContents.send('saveConfig', { + filePath, + data, + }); + } } /** * Runs when the user wants to load a configuration of the GCS. * The configuration currently includes the map location loaded. */ -function loadConfig() { +function loadConfig(): void { + if (!mainWindow) return; + // Loads a window that allows the user to choose the filePath of the file to be loaded. const filePaths = dialog.showOpenDialog(mainWindow, { title: 'Open Configuration', @@ -102,19 +125,22 @@ function loadConfig() { }); // Returns if the user chooses to close the window instead of choosing a file path. - if (!filePaths || filePaths.length === 0) return; + if (filePaths.length === 0) return; - const data = JSON.parse(fs.readFileSync(filePaths[0]), 'utf8'); + // TODO: Add type for data. + const data = JSON.parse(fs.readFileSync(filePaths[0]).toString()); if (!data) return; // Loads parsed data into main and mission windows. mainWindow.webContents.send('loadConfig', data); - missionWindow.webContents.send('loadConfig', data); + if (missionWindow) { + missionWindow.webContents.send('loadConfig', data); + } } // Menu displayed on main window. -const menu = [ +const menu: MenuItemConstructorOptions[] = [ { label: 'File', submenu: [ @@ -161,7 +187,11 @@ const menu = [ submenu: [ { label: 'My Location', - click() { mainWindow.webContents.send('setMapToUserLocation'); }, + click() { + if (mainWindow) { + mainWindow.webContents.send('setMapToUserLocation'); + } + }, }, { type: 'separator' }, ], @@ -187,11 +217,15 @@ const menu = [ * Adds a list of locations on the menu to allow user to pan to specific location in the map. * The list of locations comes from ../../resources/locations.json. */ -function setLocationMenu() { - const locationMenu = menu.find(m => m.label === 'Locations').submenu; +function setLocationMenu(): void { + const location = menu.find(m => m.label === 'Locations'); + if (!location) return; - if (!locations || locations.length === 0) { - locationMenu.push({ + const locationMenu = location.submenu; + if (!locationMenu) return; + + if (!locations || Object.keys(locations).length === 0) { + (locationMenu as MenuItemConstructorOptions[]).push({ label: 'No locations defined', enabled: false, }); @@ -199,24 +233,30 @@ function setLocationMenu() { } Object.keys(locations).forEach((label) => { - const { lat, lng, zoom } = locations[label]; - locationMenu.push({ + (locationMenu as MenuItemConstructorOptions[]).push({ label, - data: { lat, lng, zoom }, - click(menuItem) { mainWindow.webContents.send('updateMapLocation', menuItem.data); }, + click(menuItem) { + if (mainWindow) { + mainWindow.webContents.send('updateMapLocation', locations[menuItem.label]); + } + }, }); }); } -function hideMissionWindow() { - mainWindow.webContents.send('setSelectedMission', -1); - missionWindow.hide(); +function hideMissionWindow(): void { + if (mainWindow) { + mainWindow.webContents.send('setSelectedMission', -1); + } + if (missionWindow) { + missionWindow.hide(); + } } /** * Creates the main window. This window's hash is #main. */ -function createMainWindow() { +function createMainWindow(): void { mainWindow = new BrowserWindow({ title: 'NGCP Ground Control Station', icon, @@ -234,13 +274,15 @@ function createMainWindow() { } mainWindow.on('ready-to-show', () => { - mainWindow.show(); + if (mainWindow) mainWindow.show(); }); mainWindow.on('close', (event) => { if (!quitting) { event.preventDefault(); - mainWindow.hide(); + if (mainWindow) { + mainWindow.hide(); + } if (missionWindow) { hideMissionWindow(); } @@ -255,7 +297,7 @@ function createMainWindow() { * Does not show up once app is loaded (will be hidden) and is shown only when it is opened from * the main window. */ -function createMissionWindow() { +function createMissionWindow(): void { missionWindow = new BrowserWindow({ title: 'NGCP Mission User Interface', icon, @@ -292,7 +334,7 @@ function createMissionWindow() { }); } -function showWindow() { +function showWindow(): void { if (!mainWindow) { createMainWindow(); } else { @@ -300,7 +342,7 @@ function showWindow() { } } -function showMissionWindow() { +function showMissionWindow(): void { if (!missionWindow) { createMissionWindow(); } else { @@ -311,7 +353,7 @@ function showMissionWindow() { /** * Small menu displayed on the bottom-right corner of windows, or upper-right corner of macOS. */ -const trayMenu = [ +const trayMenu: MenuItemConstructorOptions[] = [ { label: 'NGCP Ground Control Station', click() { showWindow(); }, @@ -320,7 +362,7 @@ const trayMenu = [ quitRole, ]; -function createMenu() { +function createMenu(): void { setLocationMenu(); if (process.platform === 'darwin') { @@ -332,7 +374,7 @@ function createMenu() { Menu.setApplicationMenu(Menu.buildFromTemplate(menu)); } -function createTray() { +function createTray(): void { tray = new Tray(icon); tray.setContextMenu(Menu.buildFromTemplate(trayMenu)); @@ -354,7 +396,7 @@ app.on('before-quit', () => { quitting = true; }); -ipcMain.on('post', (event, notification, data) => { +ipcMain.on('post', (_: Event, notification: string, data: object) => { if (notification === 'showMissionWindow') { showMissionWindow(); } else if (notification === 'hideMissionWindow') { @@ -366,7 +408,9 @@ ipcMain.on('post', (event, notification, data) => { * some notifications will not be picked up. Of course we can filter out which * notifications go for which window, but its simpler to have it forwarded to both. */ - mainWindow.webContents.send(notification, data); + if (mainWindow) { + mainWindow.webContents.send(notification, data); + } if (missionWindow) { missionWindow.webContents.send(notification, data); } diff --git a/src/renderer/fixtures/completeJobFixture.js b/src/renderer/fixtures/completeJobFixture.ts similarity index 86% rename from src/renderer/fixtures/completeJobFixture.js rename to src/renderer/fixtures/completeJobFixture.ts index d2dbe1f2..c1189f9c 100644 --- a/src/renderer/fixtures/completeJobFixture.js +++ b/src/renderer/fixtures/completeJobFixture.ts @@ -1,6 +1,6 @@ import { ipcRenderer } from 'electron'; // eslint-disable-line import/no-extraneous-dependencies -export default function completeJob() { +export default function completeJob(): void { // Sends completeJob notification after receiving startJob notification. ipcRenderer.on('startJob', () => { setTimeout(() => { diff --git a/src/renderer/fixtures/index.js b/src/renderer/fixtures/index.ts similarity index 99% rename from src/renderer/fixtures/index.js rename to src/renderer/fixtures/index.ts index 0fb4bfc4..9faa186b 100644 --- a/src/renderer/fixtures/index.js +++ b/src/renderer/fixtures/index.ts @@ -1,4 +1,4 @@ -/** +/* * Import all fixtures in here. Feel free to enable/disable fixtures by commenting them out. * * Note we do not use fs in here since building the app will not properly link the items. diff --git a/src/renderer/fixtures/updateMessagesFixtures.js b/src/renderer/fixtures/updateMessagesFixtures.ts similarity index 91% rename from src/renderer/fixtures/updateMessagesFixtures.js rename to src/renderer/fixtures/updateMessagesFixtures.ts index d72d9cf3..f4a4b70b 100644 --- a/src/renderer/fixtures/updateMessagesFixtures.js +++ b/src/renderer/fixtures/updateMessagesFixtures.ts @@ -18,7 +18,7 @@ const fixtures = [ }, ]; -export default function updateMessages() { +export default function updateMessages(): void { const fixture = fixtures[Math.floor(Math.random() * fixtures.length)]; ipcRenderer.send('post', 'updateMessages', [fixture]); } diff --git a/src/renderer/fixtures/updateVehiclesFixtures.js b/src/renderer/fixtures/updateVehiclesFixtures.ts similarity index 87% rename from src/renderer/fixtures/updateVehiclesFixtures.js rename to src/renderer/fixtures/updateVehiclesFixtures.ts index 22d049e7..ecba6b83 100644 --- a/src/renderer/fixtures/updateVehiclesFixtures.js +++ b/src/renderer/fixtures/updateVehiclesFixtures.ts @@ -1,5 +1,5 @@ import { ipcRenderer } from 'electron'; // eslint-disable-line import/no-extraneous-dependencies -import { vehicleStatuses } from '../../../resources/index'; +import { vehicleStatuses } from '../../config/index'; let fixtures = [ { @@ -21,7 +21,7 @@ let fixtures = [ const status = Object.keys(vehicleStatuses); -export default function updateVehicles() { +export default function updateVehicles(): void { const newFixtures = fixtures.map(fixture => ({ ...fixture, lat: fixture.lat + (Math.random() / 5000) - 0.0001, diff --git a/src/renderer/index.js b/src/renderer/index.js index d897b542..61ecc147 100644 --- a/src/renderer/index.js +++ b/src/renderer/index.js @@ -12,7 +12,7 @@ import 'react-virtualized/styles.css'; import './index.css'; -import { fixtures, geolocation } from '../../resources/index'; +import { fixtures, geolocation } from '../config/index'; const isDevelopment = process.env.NODE_ENV !== 'production'; diff --git a/src/renderer/mainWindow/log/LogContainer.jsx b/src/renderer/mainWindow/log/LogContainer.tsx similarity index 67% rename from src/renderer/mainWindow/log/LogContainer.jsx rename to src/renderer/mainWindow/log/LogContainer.tsx index edd2ca5c..bc99900e 100644 --- a/src/renderer/mainWindow/log/LogContainer.jsx +++ b/src/renderer/mainWindow/log/LogContainer.tsx @@ -1,19 +1,35 @@ -import PropTypes from 'prop-types'; -import { ipcRenderer } from 'electron'; // eslint-disable-line import/no-extraneous-dependencies -import moment from 'moment'; -import React, { Component } from 'react'; +import { Event, ipcRenderer } from 'electron'; // eslint-disable-line import/no-extraneous-dependencies +import moment, { Moment } from 'moment'; +import React, { ChangeEvent, Component, ReactNode } from 'react'; import { - AutoSizer, CellMeasurerCache, CellMeasurer, List, + AutoSizer, CellMeasurerCache, CellMeasurer, List, ListRowProps, } from 'react-virtualized'; +// TODO: Remove disable line comment when issue gets fixed (https://github.com/benmosher/eslint-plugin-import/pull/1304) +import { MessageType, ThemeProps } from '../../../util/types'; // eslint-disable-line import/named + import './log.css'; -const propTypes = { - theme: PropTypes.oneOf(['light', 'dark']).isRequired, -}; +interface Message { + type: MessageType; + message: string; + time: Moment; +} + +type Filter = '' | 'success' | 'failure'; + +function isFilter(filter: string): boolean { + return filter === '' || filter === 'success' || filter === 'failure'; +} + +interface State { + filter: Filter; + messages: Message[]; + filteredMessages: Message[]; +} -export default class LogContainer extends Component { - constructor(props) { +export default class LogContainer extends Component { + public constructor(props: ThemeProps) { super(props); this.state = { @@ -33,13 +49,15 @@ export default class LogContainer extends Component { this.updateMessages = this.updateMessages.bind(this); } - componentDidMount() { - ipcRenderer.on('updateMessages', (event, data) => this.updateMessages(data)); + public componentDidMount(): void { + ipcRenderer.on('updateMessages', (_: Event, data: Message[]) => this.updateMessages(data)); } - rowRenderer({ + private heightCache: CellMeasurerCache; + + private rowRenderer({ index, key, parent, style, - }) { + }: ListRowProps): ReactNode { const { filteredMessages } = this.state; const message = filteredMessages[index]; @@ -59,24 +77,26 @@ export default class LogContainer extends Component { ); } - clearMessages() { + private clearMessages(): void { this.heightCache.clearAll(); this.setState({ filter: '', messages: [], filteredMessages: [] }); } - updateFilter(event) { + private updateFilter(event: ChangeEvent): void { const { messages } = this.state; this.heightCache.clearAll(); - const newFilter = event.target.value; + const newFilter = event.target.nodeValue; + + if (!newFilter || !isFilter(newFilter)) return; this.setState({ - filter: newFilter, + filter: newFilter as Filter, filteredMessages: newFilter === '' ? messages.slice(0) : messages.filter(message => message.type === newFilter), }); } - updateMessages(messages) { + private updateMessages(messages: Message[]): void { const { filteredMessages, messages: thisMessages, filter } = this.state; const currentMessages = thisMessages; const currentFilteredMessages = filteredMessages; @@ -100,7 +120,7 @@ export default class LogContainer extends Component { }); } - render() { + public render(): ReactNode { const { theme } = this.props; const { filter, filteredMessages } = this.state; @@ -134,5 +154,3 @@ export default class LogContainer extends Component { ); } } - -LogContainer.propTypes = propTypes; diff --git a/src/renderer/mainWindow/log/log.css b/src/renderer/mainWindow/log/log.css index 58f5ab82..39d69072 100644 --- a/src/renderer/mainWindow/log/log.css +++ b/src/renderer/mainWindow/log/log.css @@ -1,4 +1,4 @@ -/* Defined styles for Log.js */ +/* Defined styles for Log.tsx */ .logContainer .row { display: flex; diff --git a/src/renderer/mainWindow/map/CachedTileLayer.js b/src/renderer/mainWindow/map/CachedTileLayer.js deleted file mode 100644 index e6efb979..00000000 --- a/src/renderer/mainWindow/map/CachedTileLayer.js +++ /dev/null @@ -1,19 +0,0 @@ -import { GridLayer, withLeaflet } from 'react-leaflet'; - -import TileLayer from '../../../util/TileLayer.CachedTileLayer'; - -class CachedTileLayer extends GridLayer { - createLeafletElement(props) { - return new TileLayer(props.url, this.getOptions(props)); - } - - updateLeafletElement(fromProps, toProps) { - super.updateLeafletElement(fromProps, toProps); - - if (toProps.url !== fromProps.url) { - this.leafletElement.setUrl(toProps.url); - } - } -} - -export default withLeaflet(CachedTileLayer); diff --git a/src/renderer/mainWindow/map/CachedTileLayer.ts b/src/renderer/mainWindow/map/CachedTileLayer.ts new file mode 100644 index 00000000..3625e4df --- /dev/null +++ b/src/renderer/mainWindow/map/CachedTileLayer.ts @@ -0,0 +1,22 @@ +import { GridLayer, withLeaflet } from 'react-leaflet'; + +// We call with require statements since we do not want to deal with typechecking the JS file. +const TileLayer = require('../../../util/CachedTileLayer'); // eslint-disable-line @typescript-eslint/no-var-requires + +/* eslint-disable @typescript-eslint/no-explicit-any */ + +class CachedTileLayer extends GridLayer { + public createLeafletElement(props: any): any { + return new TileLayer(props.url, this.getOptions(props)); + } + + public updateLeafletElement(fromProps: any, toProps: any): void { + super.updateLeafletElement(fromProps, toProps); + + if (toProps.url !== fromProps.url) { + (this.leafletElement as any).setUrl(toProps.url); + } + } +} + +export default withLeaflet(CachedTileLayer); diff --git a/src/renderer/mainWindow/map/MapContainer.jsx b/src/renderer/mainWindow/map/MapContainer.jsx index 111c0fbe..829f8837 100644 --- a/src/renderer/mainWindow/map/MapContainer.jsx +++ b/src/renderer/mainWindow/map/MapContainer.jsx @@ -6,7 +6,7 @@ import { Map } from 'react-leaflet'; import { cache, locations, startLocation, -} from '../../../../resources/index'; +} from '../../../config/index'; import { updateVehicles } from '../../../util/util'; diff --git a/src/renderer/mainWindow/map/VehicleMarker.jsx b/src/renderer/mainWindow/map/VehicleMarker.jsx index de8276ee..c48e59c3 100644 --- a/src/renderer/mainWindow/map/VehicleMarker.jsx +++ b/src/renderer/mainWindow/map/VehicleMarker.jsx @@ -3,7 +3,7 @@ import PropTypes from 'prop-types'; import React, { PureComponent } from 'react'; import { Marker, Tooltip } from 'react-leaflet'; -import { images } from '../../../../resources/index'; +import { images } from '../../../config/index'; const propTypes = { sid: PropTypes.number.isRequired, diff --git a/src/util/TileLayer.CachedTileLayer.js b/src/util/CachedTileLayer.js similarity index 92% rename from src/util/TileLayer.CachedTileLayer.js rename to src/util/CachedTileLayer.js index 50ebefc6..ab205dd7 100644 --- a/src/util/TileLayer.CachedTileLayer.js +++ b/src/util/CachedTileLayer.js @@ -6,7 +6,7 @@ * No linting here as code is not written by us. */ -/* eslint-disable */ +/* eslint-disable no-underscore-dangle, no-param-reassign, no-plusplus, max-len */ import L from 'leaflet'; import PouchDB from 'pouchdb'; @@ -61,7 +61,7 @@ export default L.TileLayer.CachedTileLayer = L.TileLayer.extend({ createTile: function createTile(coords, done) { const tile = document.createElement('img'); - tile.onerror = L.bind(this._tileOnError, this, done, tile); + tile.onerror = L.Util.bind(this._tileOnError, this, done, tile); if (this.options.crossOrigin) { tile.crossOrigin = ''; @@ -75,7 +75,7 @@ export default L.TileLayer.CachedTileLayer = L.TileLayer.extend({ if (this.options.useCache) { this._db.get(tileUrl, { revs_info: true }, this._onCacheLookup(tile, tileUrl, done)); } else { - tile.onload = L.bind(this._tileOnLoad, this, done, tile); + tile.onload = L.Util.bind(this._tileOnLoad, this, done, tile); tile.src = tileUrl; } @@ -112,14 +112,14 @@ export default L.TileLayer.CachedTileLayer = L.TileLayer.extend({ if (Date.now() > data.timestamp + this.options.cacheMaxAge && !this.options.useOnlyCache) { if (this.options.saveToCache) { - tile.onload = L.bind(this._saveTile, this, tile, tileUrl, data._revs_info[0].rev, done); + tile.onload = L.Util.bind(this._saveTile, this, tile, tileUrl, data._revs_info[0].rev, done); } tile.crossOrigin = 'Anonymous'; tile.src = tileUrl; tile.onerror = () => { this.src = url; }; } else { - tile.onload = L.bind(this._tileOnLoad, this, done, tile); + tile.onload = L.Util.bind(this._tileOnLoad, this, done, tile); tile.src = url; } }); @@ -136,9 +136,9 @@ export default L.TileLayer.CachedTileLayer = L.TileLayer.extend({ tile.src = L.Util.emptyImageUrl; } else { if (this.options.saveToCache) { - tile.onload = L.bind(this._saveTile, this, tile, tileUrl, undefined, done); + tile.onload = L.Util.bind(this._saveTile, this, tile, tileUrl, undefined, done); } else { - tile.onload = L.bind(this._tileOnLoad, this, done, tile); + tile.onload = L.Util.bind(this._tileOnLoad, this, done, tile); } tile.crossOrigin = 'Anonymous'; @@ -204,7 +204,7 @@ export default L.TileLayer.CachedTileLayer = L.TileLayer.extend({ for (let j = tileBounds.min.y; j <= tileBounds.max.y; j++) { for (let i = tileBounds.min.x; i <= tileBounds.max.x; i++) { - const point = new L.point(i, j); + const point = new L.Point(i, j); point.z = z; queue.push(this._getTileUrl(point)); } @@ -240,7 +240,7 @@ export default L.TileLayer.CachedTileLayer = L.TileLayer.extend({ } zoom += this.options.zoomOffset; - return L.Util.template(this._url, L.extend({ + return L.Util.template(this._url, L.Util.extend({ r: this.options.detectRetina && L.Browser.retina && this.optiona.maxZoom > 0 ? '@2x' : '', s: this._getSubdomain(coords), x: coords.x, diff --git a/src/util/types.ts b/src/util/types.ts new file mode 100644 index 00000000..c2e3ba6a --- /dev/null +++ b/src/util/types.ts @@ -0,0 +1,15 @@ +// Props for React components. +export interface ThemeProps { + theme: 'light' | 'dark'; +} + +// Type guards. +export function isThemeProps({ theme }: { theme: string }): boolean { + return theme === 'light' || theme === 'dark'; +} + +// Typings for messages. +export type MessageType = 'success' | 'failure' | 'progress' | ''; + +export default { +}; diff --git a/src/util/util.js b/src/util/util.js index e6a5a8cf..e37df5d1 100644 --- a/src/util/util.js +++ b/src/util/util.js @@ -2,7 +2,7 @@ import { ipcRenderer } from 'electron'; // eslint-disable-line import/no-extrane import { vehicleInfos, vehicleStatuses, -} from '../../resources/index'; +} from '../config/index'; // Called by map container and vehicle container to update vehicles being shown. export function updateVehicles(component, vehicles) { diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 00000000..5008cd10 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "./node_modules/electron-webpack/tsconfig-base.json", + "compilerOptions": { + "jsx": "react", + + "noUnusedLocals": false, + + "resolveJsonModule": true, + "esModuleInterop": true + } +} diff --git a/typings/external.d.ts b/typings/external.d.ts new file mode 100644 index 00000000..7059e41b --- /dev/null +++ b/typings/external.d.ts @@ -0,0 +1,2 @@ +// Allows us to import PNG files. +declare module '*.png' diff --git a/webpack.config.js b/webpack.config.js index e7df76fe..acad33aa 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -1,4 +1,4 @@ -/* eslint-disable import/no-commonjs */ +/* eslint-disable @typescript-eslint/no-var-requires */ // Webpack configuration cannot be written with ES6 import/export so we disable no-commonjs. From cd02c35df2e3a98e8d2bdcc0219d72dc0c278cf9 Mon Sep 17 00:00:00 2001 From: Paul Matthew Barrameda Date: Tue, 26 Mar 2019 13:00:57 -0700 Subject: [PATCH 025/152] Added FOSSA to track dependencies as well as updated map container We will use https://github.com/yagajs/leaflet-cached-tile-layer for our Cached Tile Layer --- README.md | 3 +- src/main/index.ts | 2 +- src/renderer/mainWindow/log/LogContainer.tsx | 18 +- .../mainWindow/map/CachedTileLayer.ts | 41 ++- src/renderer/mainWindow/map/MapContainer.jsx | 6 +- src/renderer/mainWindow/map/map.css | 12 +- src/util/CachedTileLayer.js | 290 ------------------ 7 files changed, 44 insertions(+), 328 deletions(-) delete mode 100644 src/util/CachedTileLayer.js diff --git a/README.md b/README.md index dc6a7baa..cffbbe7e 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,7 @@ [![Build Status](https://travis-ci.com/NGCP/GCS.svg?branch=dev-2018)](https://travis-ci.com/NGCP/GCS) [![dependencies Status](https://david-dm.org/NGCP/GCS/status.svg)](https://david-dm.org/NGCP/GCS) [![devDependencies Status](https://david-dm.org/NGCP/GCS/dev-status.svg)](https://david-dm.org/NGCP/GCS?type=dev) +[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2FNGCP%2FGCS.svg?type=shield)](https://app.fossa.com/projects/git%2Bgithub.com%2FNGCP%2FGCS?ref=badge_shield) The [Northrop Grumman Collaboration Project] presents the Ground Control Station. This project's objective is to view and set missions for all autonomous vehicle platforms in the project. @@ -12,7 +13,7 @@ This is based on [Node.js], [React], [Webpack], and [Electron]. **:pencil2: Setting Things Up** Install [Node.js]. The program will not be able to run without it. - +Â Open up your command line application and clone this repository: ```sh diff --git a/src/main/index.ts b/src/main/index.ts index 51cdff06..dd435a18 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -197,7 +197,7 @@ const menu: MenuItemConstructorOptions[] = [ ], }, { - role: 'mainWindow', + role: 'window', submenu: [ { role: 'minimize' }, ], diff --git a/src/renderer/mainWindow/log/LogContainer.tsx b/src/renderer/mainWindow/log/LogContainer.tsx index bc99900e..de1808ba 100644 --- a/src/renderer/mainWindow/log/LogContainer.tsx +++ b/src/renderer/mainWindow/log/LogContainer.tsx @@ -53,12 +53,14 @@ export default class LogContainer extends Component { ipcRenderer.on('updateMessages', (_: Event, data: Message[]) => this.updateMessages(data)); } + // Must declare instance variables after componentDidMount function. See react/sort-comp. private heightCache: CellMeasurerCache; - private rowRenderer({ - index, key, parent, style, - }: ListRowProps): ReactNode { + private rowRenderer(props: ListRowProps): ReactNode { const { filteredMessages } = this.state; + const { + index, key, parent, style, + } = props; const message = filteredMessages[index]; return ( @@ -79,7 +81,12 @@ export default class LogContainer extends Component { private clearMessages(): void { this.heightCache.clearAll(); - this.setState({ filter: '', messages: [], filteredMessages: [] }); + + this.setState({ + filter: '', + messages: [], + filteredMessages: [], + }); } private updateFilter(event: ChangeEvent): void { @@ -102,8 +109,7 @@ export default class LogContainer extends Component { const currentFilteredMessages = filteredMessages; messages.forEach((message) => { - const msg = { - type: '', + const msg: Message = { time: moment(), ...message, }; diff --git a/src/renderer/mainWindow/map/CachedTileLayer.ts b/src/renderer/mainWindow/map/CachedTileLayer.ts index 3625e4df..df13aa94 100644 --- a/src/renderer/mainWindow/map/CachedTileLayer.ts +++ b/src/renderer/mainWindow/map/CachedTileLayer.ts @@ -1,22 +1,21 @@ -import { GridLayer, withLeaflet } from 'react-leaflet'; +// TODO: Use TileLayer from https://github.com/yagajs/leaflet-cached-tile-layer -// We call with require statements since we do not want to deal with typechecking the JS file. -const TileLayer = require('../../../util/CachedTileLayer'); // eslint-disable-line @typescript-eslint/no-var-requires - -/* eslint-disable @typescript-eslint/no-explicit-any */ - -class CachedTileLayer extends GridLayer { - public createLeafletElement(props: any): any { - return new TileLayer(props.url, this.getOptions(props)); - } - - public updateLeafletElement(fromProps: any, toProps: any): void { - super.updateLeafletElement(fromProps, toProps); - - if (toProps.url !== fromProps.url) { - (this.leafletElement as any).setUrl(toProps.url); - } - } -} - -export default withLeaflet(CachedTileLayer); +/* + * import { GridLayer, withLeaflet } from 'react-leaflet'; + * + * class CachedTileLayer extends GridLayer { + * public createLeafletElement(props: any): L.GridLayer { + * return new TileLayer(props.url, this.getOptions(props)); + * } + * + * public updateLeafletElement(fromProps: any, toProps: any): void { + * super.updateLeafletElement(fromProps, toProps); + * + * if (toProps.url !== fromProps.url) { + * (this.leafletElement as any).setUrl(toProps.url); + * } + * } + * } + * + * export default withLeaflet(CachedTileLayer); + */ diff --git a/src/renderer/mainWindow/map/MapContainer.jsx b/src/renderer/mainWindow/map/MapContainer.jsx index 829f8837..ffe32eba 100644 --- a/src/renderer/mainWindow/map/MapContainer.jsx +++ b/src/renderer/mainWindow/map/MapContainer.jsx @@ -2,7 +2,7 @@ import { ipcRenderer } from 'electron'; // eslint-disable-line import/no-extrane import fs from 'fs'; import PropTypes from 'prop-types'; import React, { Component, createRef, Fragment } from 'react'; -import { Map } from 'react-leaflet'; +import { Map, TileLayer } from 'react-leaflet'; import { cache, locations, startLocation, @@ -13,7 +13,7 @@ import { updateVehicles } from '../../../util/util'; import GeolocationControl from './control/GeolocationControl'; import ThemeControl from './control/ThemeControl'; -import CachedTileLayer from './CachedTileLayer'; +// import CachedTileLayer from './CachedTileLayer'; import VehicleMarker from './VehicleMarker'; import './map.css'; @@ -138,7 +138,7 @@ export default class MapContainer extends Component { > - + {markers} ); diff --git a/src/renderer/mainWindow/map/map.css b/src/renderer/mainWindow/map/map.css index 559dbd73..3529ad9c 100644 --- a/src/renderer/mainWindow/map/map.css +++ b/src/renderer/mainWindow/map/map.css @@ -10,11 +10,11 @@ } .geolocation-control { - background-image: url('../../../../resources/images/other/geolocation.png'); + background-image: url('../../../config/images/other/geolocation.png'); } .geolocation-control:hover { - background-image: url('../../../../resources/images/other/geolocation_hover.png'); + background-image: url('../../../config/images/other/geolocation_hover.png'); } /* Light theme */ @@ -22,11 +22,11 @@ .theme-control { cursor: pointer; background-size: 75%; - background-image: url('../../../../resources/images/other/moon.png'); + background-image: url('../../../config/images/other/moon.png'); } .theme-control:hover { - background-image: url('../../../../resources/images/other/moon_hover.png'); + background-image: url('../../../config/images/other/moon_hover.png'); } /* Dark theme */ @@ -34,9 +34,9 @@ .theme-control_dark { cursor: pointer; background-size: 75%; - background-image: url('../../../../resources/images/other/fullmoon.png'); + background-image: url('../../../config/images/other/fullmoon.png'); } .theme-control_dark:hover { - background-image: url('../../../../resources/images/other/fullmoon_hover.png'); + background-image: url('../../../config/images/other/fullmoon_hover.png'); } diff --git a/src/util/CachedTileLayer.js b/src/util/CachedTileLayer.js deleted file mode 100644 index ab205dd7..00000000 --- a/src/util/CachedTileLayer.js +++ /dev/null @@ -1,290 +0,0 @@ -/* - * Credits: - * MazeMap's Leaflet PouchDBCached: https://github.com/MazeMap/Leaflet.TileLayer.PouchDBCached/blob/master/L.TileLayer.PouchDBCached.js - * Leaflet's TileLayer: https://github.com/Leaflet/Leaflet/blob/master/src/layer/tile/TileLayer.js - * - * No linting here as code is not written by us. - */ - -/* eslint-disable no-underscore-dangle, no-param-reassign, no-plusplus, max-len */ - -import L from 'leaflet'; -import PouchDB from 'pouchdb'; - -export default L.TileLayer.CachedTileLayer = L.TileLayer.extend({ - options: { - useCache: false, - saveToCache: true, - useOnlyCache: false, - cacheMaxAge: 24 * 3600 * 1000, - }, - - /** - * Adds custom options to support options with caching to normal TileLayer. - * @override - */ - initialize: function initialize(url, options) { - this._url = url; - options = L.Util.setOptions(this, { ...this.options, ...options, cacheFormat: 'image/png' }); - - if (!options.useCache) { - this._db = null; - return; - } - - this._db = new PouchDB('offline-tiles'); - - if (options.detectRetina && L.Browser.retina && options.maxZoom > 0) { - options.tileSize = Math.floor(options.tileSize / 2); - if (!options.zoomReverse) { - options.zoomOffset++; - options.maxZoom--; - } else { - options.zoomOffset--; - options.minZoom++; - } - } - - if (typeof options.subdomains === 'string') { - options.subdomains = options.subdomains.split(''); - } - - if (!L.Browser.android) { - this.on('tileunload', this._onTileRemove); - } - }, - - /** - * Adds support to create cached tiles to a normal TileLayer. - * @override - */ - createTile: function createTile(coords, done) { - const tile = document.createElement('img'); - - tile.onerror = L.Util.bind(this._tileOnError, this, done, tile); - - if (this.options.crossOrigin) { - tile.crossOrigin = ''; - } - - tile.alt = ''; - - tile.setAttribute('role', 'presentation'); - - const tileUrl = this.getTileUrl(coords); - if (this.options.useCache) { - this._db.get(tileUrl, { revs_info: true }, this._onCacheLookup(tile, tileUrl, done)); - } else { - tile.onload = L.Util.bind(this._tileOnLoad, this, done, tile); - tile.src = tileUrl; - } - - return tile; - }, - - /** - * Returns a callback to be run when the database is finished with a fetch operation. - * @param {Object} tile tile description - * @param {string} tileUrl tile url - * @param {bool} done status - * @returns {Function} - */ - _onCacheLookup: function _onCacheLookup(tile, tileUrl, done) { - return (err, data) => { - if (err) {} // eslint-disable-line no-empty - - if (data) { - return this._onCacheHit(tile, tileUrl, data, done); - } - - return this._onCacheMiss(tile, tileUrl, done); - }; - }, - - _onCacheHit: function _onCacheHit(tile, tileUrl, data, done) { - this.fire('tilecachehit', { - tile, - url: tileUrl, - }); - - this._db.getAttachment(tileUrl, 'tile').then((blob) => { - const url = URL.createObjectURL(blob); - - if (Date.now() > data.timestamp + this.options.cacheMaxAge && !this.options.useOnlyCache) { - if (this.options.saveToCache) { - tile.onload = L.Util.bind(this._saveTile, this, tile, tileUrl, data._revs_info[0].rev, done); - } - - tile.crossOrigin = 'Anonymous'; - tile.src = tileUrl; - tile.onerror = () => { this.src = url; }; - } else { - tile.onload = L.Util.bind(this._tileOnLoad, this, done, tile); - tile.src = url; - } - }); - }, - - _onCacheMiss: function _onCacheMiss(tile, tileUrl, done) { - this.fire('tilecachemiss', { - tile, - url: tileUrl, - }); - - if (this.options.useOnlyCache) { - tile.onload = L.Util.falseFn; - tile.src = L.Util.emptyImageUrl; - } else { - if (this.options.saveToCache) { - tile.onload = L.Util.bind(this._saveTile, this, tile, tileUrl, undefined, done); - } else { - tile.onload = L.Util.bind(this._tileOnLoad, this, done, tile); - } - - tile.crossOrigin = 'Anonymous'; - tile.src = tileUrl; - } - }, - - /** - * Returns an event handler that runs when the tile is ready. - * The handler will delete the document from PouchDB if an existing revision is found, keeping - * latest valid copy of the image in the cache. - * @param {Object} tile tile description - * @param {string} tileUrl tile url - * @param {bool} existingRevision check in cache if tile is there - * @param {bool} done status - */ - _saveTile: function _saveTile(tile, tileUrl, existingRevision, done) { - if (!this.options.saveToCache) return; - - const canvas = document.createElement('canvas'); - canvas.width = tile.naturalWidth || tile.width; - canvas.height = tile.naturalHeight || tile.height; - - const context = canvas.getContext('2d'); - context.drawImage(tile, 0, 0); - - const format = this.options.cacheFormat; - - canvas.toBlob((blob) => { - this._db.put({ - _id: tileUrl, - _rev: existingRevision, - timestamp: Date.now(), - }) - .then(status => this._db.putAttachment(tileUrl, 'tile', status.rev, blob, format)) - .then(() => { - if (done) done(); - }) - .catch(() => { - if (done) done(); - }); - }); - }, - - /** - * Starts seeding the cache given a bounding box and a min/max zoom level. - * @param {L.LatLngBounds} bbox bounds - * @param {number} minZoom min zoom level - * @param {number} maxZoom max zoom level - */ - seed: function seed(bbox, minZoom, maxZoom) { - if (!this.options.useCache) return; - if (minZoom > maxZoom) return; - if (!this._map) return; - - const queue = []; - - for (let z = minZoom; z <= maxZoom; z++) { - const northEastPoint = this._map.project(bbox.getNorthEast(), z); - const southWestPoint = this._map.project(bbox.getSouthWest(), z); - - const tileBounds = this._pxBoundsToTileRange(L.bounds([northEastPoint, southWestPoint])); - - for (let j = tileBounds.min.y; j <= tileBounds.max.y; j++) { - for (let i = tileBounds.min.x; i <= tileBounds.max.x; i++) { - const point = new L.Point(i, j); - point.z = z; - queue.push(this._getTileUrl(point)); - } - } - } - - const seedData = { - bbox, - minZoom, - maxZoom, - queueLength: queue.length, - }; - - this.fire('seedstart', seedData); - const tile = this._createTile(); - tile._layer = this; - this._seedOneTile(tile, queue, seedData); - }, - - _createTile: function _createTile() { - return document.createElement('img'); - }, - - /** - * Custom getTileUrl function that uses coords instead of the maps current zoomlevel - * @param {Object} coords map coords - * @returns {string} - */ - _getTileUrl: function _getTileUrl(coords) { - let zoom = coords.z; - if (this.options.zoomReverse) { - zoom = this.options.maxZoom - zoom; - } - zoom += this.options.zoomOffset; - - return L.Util.template(this._url, L.Util.extend({ - r: this.options.detectRetina && L.Browser.retina && this.optiona.maxZoom > 0 ? '@2x' : '', - s: this._getSubdomain(coords), - x: coords.x, - y: this.options.tms ? this._globalTileRange.max.y - coords.y : coords.y, - z: this.options.maxNativeZoom ? Math.min(zoom, this.options.maxNativeZoom) : zoom, - }, this.options)); - }, - - /** - * Uses a defined tile to eat through one item in the queue and asynchrounously recursively call - * itself when the tile has finished loading. - * @param {Object} tile the tile to load - * @param {Array} remaining remaining tiles to load - * @param {Object} seedData data about tile to seed - */ - _seedOneTile: function _seedOneTile(tile, remaining, seedData) { - if (!remaining.length) { - this.fire('seedend', seedData); - return; - } - this.fire('seedprogress', { - bbox: seedData.bbox, - minZoom: seedData.minZoom, - maxZoom: seedData.maxZoom, - queueLength: seedData.queueLength, - remainingLength: remaining.length, - }); - - const url = remaining.shift(); - - this._db.get(url, (err, data) => { - if (err) {} // eslint-disable-line no-empty - - if (!data) { - tile.onload = () => { - this._saveTile(tile, url, null); - this._seedOneTile(tile, remaining, seedData); - }; - - tile.crossOrigin = 'Anonymous'; - tile.src = url; - } else { - this._seedOneTile(tile, remaining, seedData); - } - }); - }, -}); From e78bb61de7f4a525b221127af6745d9e5c07af6b Mon Sep 17 00:00:00 2001 From: Paul Matthew Barrameda Date: Tue, 26 Mar 2019 21:13:51 -0700 Subject: [PATCH 026/152] More TypeScript files changed Control TypeScript added, will continue on my PC Added react-leaflet-control to declaration files. Moved the declaration file to root of repository, named as global.d.ts. --- global.d.ts | 5 +++ src/renderer/mainWindow/log/LogContainer.tsx | 2 +- .../mainWindow/map/control/Control.jsx | 40 ------------------- .../mainWindow/map/control/Control.tsx | 34 ++++++++++++++++ typings/external.d.ts | 2 - 5 files changed, 40 insertions(+), 43 deletions(-) create mode 100644 global.d.ts delete mode 100644 src/renderer/mainWindow/map/control/Control.jsx create mode 100644 src/renderer/mainWindow/map/control/Control.tsx delete mode 100644 typings/external.d.ts diff --git a/global.d.ts b/global.d.ts new file mode 100644 index 00000000..66069341 --- /dev/null +++ b/global.d.ts @@ -0,0 +1,5 @@ +// Allows us to import PNG files. +declare module '*.png'; + +// Allows us to use react-leaflet-control +declare module 'react-leaflet-control'; diff --git a/src/renderer/mainWindow/log/LogContainer.tsx b/src/renderer/mainWindow/log/LogContainer.tsx index de1808ba..a9a1ac95 100644 --- a/src/renderer/mainWindow/log/LogContainer.tsx +++ b/src/renderer/mainWindow/log/LogContainer.tsx @@ -110,8 +110,8 @@ export default class LogContainer extends Component { messages.forEach((message) => { const msg: Message = { - time: moment(), ...message, + time: moment(), }; if (filter === '' || msg.type === filter) { diff --git a/src/renderer/mainWindow/map/control/Control.jsx b/src/renderer/mainWindow/map/control/Control.jsx deleted file mode 100644 index 4a812ecb..00000000 --- a/src/renderer/mainWindow/map/control/Control.jsx +++ /dev/null @@ -1,40 +0,0 @@ -/* eslint-disable jsx-a11y/anchor-has-content, - jsx-a11y/click-events-have-key-events, - jsx-a11y/no-static-element-interactions, - jsx-a11y/anchor-is-valid, - react/prefer-stateless-function, -*/ - -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import LeafletControl from 'react-leaflet-control'; - -const propTypes = { - className: PropTypes.string.isRequired, - onClick: PropTypes.func.isRequired, - position: PropTypes.string.isRequired, - title: PropTypes.string.isRequired, -}; - -export default class Control extends Component { - render() { - const { - className, onClick, position, title, - } = this.props; - - return ( - - - - ); - } -} - -Control.propTypes = propTypes; diff --git a/src/renderer/mainWindow/map/control/Control.tsx b/src/renderer/mainWindow/map/control/Control.tsx new file mode 100644 index 00000000..94818ae3 --- /dev/null +++ b/src/renderer/mainWindow/map/control/Control.tsx @@ -0,0 +1,34 @@ +/* eslint-disable max-len, jsx-a11y/anchor-has-content, jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions, jsx-a11y/anchor-is-valid, react/prefer-stateless-function */ + +import React, { Component, ReactNode } from 'react'; +import LeafletControl from 'react-leaflet-control'; + +type ControlPosition = 'topleft' | 'topright' | 'bottomleft' | 'bottomright'; + +export interface ControlProps { + className: string; + onClick: () => void; + position: ControlPosition; + title: string; +} + +export default class Control extends Component { + public render(): ReactNode { + const { + className, onClick, position, title, + } = this.props; + + return ( + + + + ); + } +} diff --git a/typings/external.d.ts b/typings/external.d.ts deleted file mode 100644 index 7059e41b..00000000 --- a/typings/external.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -// Allows us to import PNG files. -declare module '*.png' From b8b06e3e7c98f58e719c12f32e886d07a1038c2a Mon Sep 17 00:00:00 2001 From: Paul Matthew Barrameda Date: Wed, 27 Mar 2019 01:10:16 -0700 Subject: [PATCH 027/152] Finished map container I got brain cancer from doing this --- global.d.ts | 5 +- src/main/index.ts | 6 +- .../fixtures/updateMessagesFixtures.ts | 5 +- .../fixtures/updateVehiclesFixtures.ts | 11 ++- src/renderer/mainWindow/log/LogContainer.tsx | 18 ++-- src/renderer/mainWindow/main.css | 2 +- .../{MapContainer.jsx => MapContainer.tsx} | 88 ++++++++++-------- .../{VehicleMarker.jsx => VehicleMarker.tsx} | 30 +++---- .../mainWindow/map/control/Control.tsx | 2 +- ...tionControl.jsx => GeolocationControl.tsx} | 6 +- .../{ThemeControl.jsx => ThemeControl.tsx} | 17 ++-- src/renderer/mainWindow/map/map.css | 2 +- src/util/types.ts | 89 +++++++++++++++++-- src/util/{util.js => util.ts} | 49 +++++----- tsconfig.json | 2 - 15 files changed, 211 insertions(+), 121 deletions(-) rename src/renderer/mainWindow/map/{MapContainer.jsx => MapContainer.tsx} (52%) rename src/renderer/mainWindow/map/{VehicleMarker.jsx => VehicleMarker.tsx} (50%) rename src/renderer/mainWindow/map/control/{GeolocationControl.jsx => GeolocationControl.tsx} (78%) rename src/renderer/mainWindow/map/control/{ThemeControl.jsx => ThemeControl.tsx} (60%) rename src/util/{util.js => util.ts} (68%) diff --git a/global.d.ts b/global.d.ts index 66069341..807ea0b2 100644 --- a/global.d.ts +++ b/global.d.ts @@ -1,5 +1,8 @@ // Allows us to import PNG files. -declare module '*.png'; +declare module '*.png' { + const value: string; + export = value; +} // Allows us to use react-leaflet-control declare module 'react-leaflet-control'; diff --git a/src/main/index.ts b/src/main/index.ts index dd435a18..31872c39 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -15,12 +15,10 @@ import moment from 'moment'; import { images, locations as locationsConfig } from '../config/index'; -interface LocationLabel { - [key: string]: { lat: number; lng: number; zoom: number }; -} +import { LocationSignature } from '../util/types'; // Recast locations to LocationLabel to allow us to use keys properly. -const locations: LocationLabel = locationsConfig; +const locations: LocationSignature = locationsConfig; /* * This key is required to enable geolocation in the application. diff --git a/src/renderer/fixtures/updateMessagesFixtures.ts b/src/renderer/fixtures/updateMessagesFixtures.ts index f4a4b70b..231e2195 100644 --- a/src/renderer/fixtures/updateMessagesFixtures.ts +++ b/src/renderer/fixtures/updateMessagesFixtures.ts @@ -1,6 +1,9 @@ import { ipcRenderer } from 'electron'; // eslint-disable-line import/no-extraneous-dependencies -const fixtures = [ +// TODO: Remove disable line comment when issue gets fixed (https://github.com/benmosher/eslint-plugin-import/pull/1304) +import { MessageType } from '../../util/types'; // eslint-disable-line import/named + +const fixtures: { type?: MessageType; message: string }[] = [ { type: 'failure', message: 'Test failure message', diff --git a/src/renderer/fixtures/updateVehiclesFixtures.ts b/src/renderer/fixtures/updateVehiclesFixtures.ts index ecba6b83..99e891a8 100644 --- a/src/renderer/fixtures/updateVehiclesFixtures.ts +++ b/src/renderer/fixtures/updateVehiclesFixtures.ts @@ -1,21 +1,28 @@ import { ipcRenderer } from 'electron'; // eslint-disable-line import/no-extraneous-dependencies + import { vehicleStatuses } from '../../config/index'; -let fixtures = [ +// TODO: Remove disable line comment when issue gets fixed (https://github.com/benmosher/eslint-plugin-import/pull/1304) +import { Vehicle, VehicleStatus } from '../../util/types'; // eslint-disable-line import/named + +let fixtures: Vehicle[] = [ { sid: 100, lat: 34.056482, lng: -117.823912, + status: 'disconnected', }, { sid: 400, lat: 34.053095, lng: -117.821970, + status: 'disconnected', }, { sid: 200, lat: 34.053509, lng: -117.818452, + status: 'disconnected', }, ]; @@ -26,7 +33,7 @@ export default function updateVehicles(): void { ...fixture, lat: fixture.lat + (Math.random() / 5000) - 0.0001, lng: fixture.lng + (Math.random() / 5000) - 0.0001, - status: status[Math.floor(Math.random() * status.length)], + status: status[Math.floor(Math.random() * status.length)] as VehicleStatus, })); fixtures = newFixtures; diff --git a/src/renderer/mainWindow/log/LogContainer.tsx b/src/renderer/mainWindow/log/LogContainer.tsx index a9a1ac95..aefc5262 100644 --- a/src/renderer/mainWindow/log/LogContainer.tsx +++ b/src/renderer/mainWindow/log/LogContainer.tsx @@ -11,19 +11,13 @@ import { MessageType, ThemeProps } from '../../../util/types'; // eslint-disable import './log.css'; interface Message { - type: MessageType; + type?: MessageType; message: string; time: Moment; } -type Filter = '' | 'success' | 'failure'; - -function isFilter(filter: string): boolean { - return filter === '' || filter === 'success' || filter === 'failure'; -} - interface State { - filter: Filter; + filter: MessageType; messages: Message[]; filteredMessages: Message[]; } @@ -50,7 +44,7 @@ export default class LogContainer extends Component { } public componentDidMount(): void { - ipcRenderer.on('updateMessages', (_: Event, data: Message[]) => this.updateMessages(data)); + ipcRenderer.on('updateMessages', (_: Event, messages: Message[]) => this.updateMessages(messages)); } // Must declare instance variables after componentDidMount function. See react/sort-comp. @@ -95,10 +89,8 @@ export default class LogContainer extends Component { this.heightCache.clearAll(); const newFilter = event.target.nodeValue; - if (!newFilter || !isFilter(newFilter)) return; - this.setState({ - filter: newFilter as Filter, + filter: newFilter as MessageType, filteredMessages: newFilter === '' ? messages.slice(0) : messages.filter(message => message.type === newFilter), }); } @@ -110,6 +102,7 @@ export default class LogContainer extends Component { messages.forEach((message) => { const msg: Message = { + type: message.type || '', ...message, time: moment(), }; @@ -152,6 +145,7 @@ export default class LogContainer extends Component { diff --git a/src/renderer/mainWindow/main.css b/src/renderer/mainWindow/main.css index a4f5e817..6029f7fb 100644 --- a/src/renderer/mainWindow/main.css +++ b/src/renderer/mainWindow/main.css @@ -1,4 +1,4 @@ -/* Defined styles for index.js */ +/* Styles for main window */ .logContainer { grid-area: log; diff --git a/src/renderer/mainWindow/map/MapContainer.jsx b/src/renderer/mainWindow/map/MapContainer.tsx similarity index 52% rename from src/renderer/mainWindow/map/MapContainer.jsx rename to src/renderer/mainWindow/map/MapContainer.tsx index ffe32eba..cea0a8cb 100644 --- a/src/renderer/mainWindow/map/MapContainer.jsx +++ b/src/renderer/mainWindow/map/MapContainer.tsx @@ -1,13 +1,18 @@ -import { ipcRenderer } from 'electron'; // eslint-disable-line import/no-extraneous-dependencies +import { ipcRenderer, Event } from 'electron'; // eslint-disable-line import/no-extraneous-dependencies import fs from 'fs'; -import PropTypes from 'prop-types'; -import React, { Component, createRef, Fragment } from 'react'; -import { Map, TileLayer } from 'react-leaflet'; +import { LocationEvent } from 'leaflet'; +import React, { + Component, createRef, Fragment, ReactNode, RefObject, +} from 'react'; +import { Map, TileLayer, Viewport } from 'react-leaflet'; import { - cache, locations, startLocation, + cache, locations as locationsConfig, startLocation, } from '../../../config/index'; +import { + FileLoadOptions, FileSaveOptions, LatLngZoom, LocationSignature, ThemeProps, Vehicle, VehicleUI, +} from '../../../util/types'; import { updateVehicles } from '../../../util/util'; import GeolocationControl from './control/GeolocationControl'; @@ -18,6 +23,8 @@ import VehicleMarker from './VehicleMarker'; import './map.css'; +const locations: LocationSignature = locationsConfig; + const mapOptions = { attribution: 'Map data © OpenStreetMap contributors,
CC-BY-SA, Imagery © Mapbox', url: 'https://api.tiles.mapbox.com/v4/{id}/{z}/{x}/{y}.png?access_token={accessToken}', @@ -33,12 +40,13 @@ if (startLocation && locations[startLocation]) { start = { ...start, ...locations[startLocation] }; } -const propTypes = { - theme: PropTypes.oneOf(['light', 'dark']).isRequired, -}; +interface State extends LatLngZoom { + vehicles: { [sid: string]: VehicleUI }; + viewport: Viewport; +} -export default class MapContainer extends Component { - constructor(props) { +export default class MapContainer extends Component { + public constructor(props: ThemeProps) { super(props); this.state = { @@ -53,6 +61,7 @@ export default class MapContainer extends Component { this.ref = createRef(); this.onViewportChanged = this.onViewportChanged.bind(this); + this.onlocationfound = this.onlocationfound.bind(this); this.setMapToUserLocation = this.setMapToUserLocation.bind(this); this.loadConfig = this.loadConfig.bind(this); this.saveConfig = this.saveConfig.bind(this); @@ -60,54 +69,61 @@ export default class MapContainer extends Component { this.updateMapLocation = this.updateMapLocation.bind(this); } - componentDidMount() { - ipcRenderer.on('loadConfig', (event, data) => this.loadConfig(data)); - ipcRenderer.on('saveConfig', (event, file) => this.saveConfig(file)); + public componentDidMount(): void { + ipcRenderer.on('loadConfig', (_: Event, data: FileLoadOptions) => this.loadConfig(data)); + ipcRenderer.on('saveConfig', (_: Event, data: FileSaveOptions) => this.saveConfig(data)); - ipcRenderer.on('centerMapToVehicle', (event, data) => this.centerMapToVehicle(data)); + ipcRenderer.on('centerMapToVehicle', (_: Event, vehicle: VehicleUI) => this.centerMapToVehicle(vehicle)); ipcRenderer.on('setMapToUserLocation', this.setMapToUserLocation); - ipcRenderer.on('updateMapLocation', (event, data) => this.updateMapLocation(data)); - ipcRenderer.on('updateVehicles', (event, data) => updateVehicles(this, data)); + ipcRenderer.on('updateMapLocation', (_: Event, location: LatLngZoom) => this.updateMapLocation(location)); + ipcRenderer.on('updateVehicles', (_: Event, vehicles: Vehicle[]) => updateVehicles(this, vehicles)); } - onViewportChanged(viewport) { + private onViewportChanged(viewport: Viewport): void { this.setState({ viewport }); } - setMapToUserLocation() { + private onlocationfound(location: LocationEvent): void { + this.updateMapLocation(location.latlng); + } + + private setMapToUserLocation(): void { const map = this.ref.current; if (map) { map.leafletElement.locate(); } } - loadConfig(data) { - this.updateMapLocation(data); + private ref: RefObject; + + private loadConfig({ map }: FileLoadOptions): void { + this.updateMapLocation(map); } - saveConfig(file) { - // performs a hard copy of this.state, deletes vehicles from it, and saves it to file + private saveConfig(file: FileSaveOptions): void { + // Performs a hard copy of this.state, deletes vehicles from it, and saves it to file. const data = { ...this.state }; delete data.vehicles; - fs.writeFileSync(file.filePath, JSON.stringify({ ...file.data, ...data }, null, 2)); + fs.writeFileSync(file.filePath, JSON.stringify({ + ...file.data, + map: data, + }, null, 2)); } - centerMapToVehicle(vehicle) { + private centerMapToVehicle(vehicle: VehicleUI): void { this.updateMapLocation(vehicle); } - updateMapLocation(location) { - const { zoom } = location; - - // we default to (lat, lng), however geolocation api calls provide (latitude, longitude) - const lat = location.lat || location.latitude; - const lng = location.lng || location.longitude; + public updateMapLocation(location: LatLngZoom): void { + const { lat, lng, zoom } = location; const { viewport } = this.state; this.setState({ - lat, lng, zoom: zoom || viewport.zoom, + lat, + lng, + zoom: zoom || (viewport.zoom as number | undefined), }); this.onViewportChanged({ @@ -116,7 +132,7 @@ export default class MapContainer extends Component { }); } - render() { + public render(): ReactNode { const { theme } = this.props; const { viewport, vehicles } = this.state; @@ -130,10 +146,10 @@ export default class MapContainer extends Component { return ( @@ -144,5 +160,3 @@ export default class MapContainer extends Component { ); } } - -MapContainer.propTypes = propTypes; diff --git a/src/renderer/mainWindow/map/VehicleMarker.jsx b/src/renderer/mainWindow/map/VehicleMarker.tsx similarity index 50% rename from src/renderer/mainWindow/map/VehicleMarker.jsx rename to src/renderer/mainWindow/map/VehicleMarker.tsx index c48e59c3..d43bc147 100644 --- a/src/renderer/mainWindow/map/VehicleMarker.jsx +++ b/src/renderer/mainWindow/map/VehicleMarker.tsx @@ -1,24 +1,20 @@ import L from 'leaflet'; -import PropTypes from 'prop-types'; -import React, { PureComponent } from 'react'; +import React, { PureComponent, ReactNode } from 'react'; import { Marker, Tooltip } from 'react-leaflet'; import { images } from '../../../config/index'; -const propTypes = { - sid: PropTypes.number.isRequired, - name: PropTypes.string.isRequired, - lat: PropTypes.number.isRequired, - lng: PropTypes.number.isRequired, - type: PropTypes.string.isRequired, - status: PropTypes.shape({ - type: PropTypes.oneOf(['failure', 'progress', 'success']).isRequired, - message: PropTypes.string.isRequired, - }).isRequired, -}; +// TODO: Remove disable line comment when issue gets fixed (https://github.com/benmosher/eslint-plugin-import/pull/1304) +import { VehicleUI } from '../../../util/types'; // eslint-disable-line import/named -export default class VehicleMarker extends PureComponent { - render() { +interface VehicleSignature { + [key: string]: string; +} + +const { vehicles }: { vehicles: VehicleSignature } = images.markers; + +export default class VehicleMarker extends PureComponent { + public render(): ReactNode { const { sid, name, lat, lng, type, status, } = this.props; @@ -27,7 +23,7 @@ export default class VehicleMarker extends PureComponent { void; position: ControlPosition; - title: string; + title?: string; } export default class Control extends Component { diff --git a/src/renderer/mainWindow/map/control/GeolocationControl.jsx b/src/renderer/mainWindow/map/control/GeolocationControl.tsx similarity index 78% rename from src/renderer/mainWindow/map/control/GeolocationControl.jsx rename to src/renderer/mainWindow/map/control/GeolocationControl.tsx index cdba3593..3bf56304 100644 --- a/src/renderer/mainWindow/map/control/GeolocationControl.jsx +++ b/src/renderer/mainWindow/map/control/GeolocationControl.tsx @@ -1,14 +1,14 @@ import { ipcRenderer } from 'electron'; // eslint-disable-line import/no-extraneous-dependencies -import React, { Component } from 'react'; +import React, { Component, ReactNode } from 'react'; import Control from './Control'; export default class GeolocationControl extends Component { - static onClick() { + private static onClick(): void{ ipcRenderer.send('post', 'setMapToUserLocation'); } - render() { + public render(): ReactNode { return ( { + private static onClick(): void { ipcRenderer.send('post', 'toggleTheme'); } - render() { + public render(): ReactNode { const { theme } = this.props; return ( @@ -26,5 +23,3 @@ export default class ThemeControl extends Component { ); } } - -ThemeControl.propTypes = propTypes; diff --git a/src/renderer/mainWindow/map/map.css b/src/renderer/mainWindow/map/map.css index 3529ad9c..80c44a71 100644 --- a/src/renderer/mainWindow/map/map.css +++ b/src/renderer/mainWindow/map/map.css @@ -1,4 +1,4 @@ -/* Defined styles for Map.js */ +/* Styles for map container */ .mapContainer p { margin: 0; diff --git a/src/util/types.ts b/src/util/types.ts index c2e3ba6a..da3b880f 100644 --- a/src/util/types.ts +++ b/src/util/types.ts @@ -1,15 +1,90 @@ -// Props for React components. +// Typings for props for React components. export interface ThemeProps { theme: 'light' | 'dark'; } -// Type guards. -export function isThemeProps({ theme }: { theme: string }): boolean { - return theme === 'light' || theme === 'dark'; +// Type guard for ThemeProps. +export function isThemeProps(props: { theme: string }): boolean { + return props.theme === 'light' || props.theme === 'dark'; } // Typings for messages. -export type MessageType = 'success' | 'failure' | 'progress' | ''; +export type MessageType = '' | 'success' | 'failure' | 'progress'; -export default { -}; +// Type guard for MessageType. +export function isMessageType(type: string): boolean { + return type === '' || type === 'success' || type === 'failure' || type === 'progress'; +} + +// Typings for vehicles. +export type VehicleStatus = 'ready' | 'error' | 'disconnected' | 'waiting' | 'running' | 'paused'; + +// Type guard for VehicleStatus. +export function isVehicleStatus(status: string): boolean { + return status === 'ready' || status === 'error' || status === 'disconnected' || status === 'waiting' || status === 'running' || status === 'paused'; +} + +// Vehicle objects being stored. Probably will move to Orchestrator classes. +export interface Vehicle { + sid: number; + lat: number; + lng: number; + alt?: number; + heading?: number; + battery?: number; + status: VehicleStatus; + errorMessage?: string; +} + +// Vehicle objects being stored in React components. +export interface VehicleUI { + sid: number; + name: string; + type: string; + lat: number; + lng: number; + alt?: number; + heading?: number; + battery?: number; + status: { + type: MessageType; + message: string; + }; + errorMessage?: string; +} + +export interface LatLngZoom { + lat: number; + lng: number; + zoom?: number; +} + +// Types for save/load configs. +export interface FileSaveOptions { + filePath: string; + data: {}; +} + +export interface FileLoadOptions { + map: LatLngZoom; +} + +// Signatures that are being used in more than one file, makes more sense to have it here to import. +export interface LocationSignature { + [key: string]: { lat: number; lng: number; zoom: number }; +} + +export interface VehicleInfoSignature { + [key: string]: { + macAddress: string; + name: string; + type: string; + }; +} + +export interface VehicleStatusSignature { + [key: string]: { + type: string; + message: string; + }; +} diff --git a/src/util/util.js b/src/util/util.ts similarity index 68% rename from src/util/util.js rename to src/util/util.ts index e37df5d1..ea2ff7ce 100644 --- a/src/util/util.js +++ b/src/util/util.ts @@ -1,11 +1,26 @@ import { ipcRenderer } from 'electron'; // eslint-disable-line import/no-extraneous-dependencies +import { Component } from 'react'; import { - vehicleInfos, vehicleStatuses, + vehicleInfos as vehicleInfosConfig, vehicleStatuses as vehicleStatusesConfig, } from '../config/index'; +// TODO: Remove disable line comment when issue gets fixed (https://github.com/benmosher/eslint-plugin-import/pull/1304) +import { + MessageType, // eslint-disable-line import/named + Vehicle, + VehicleInfoSignature, + VehicleStatusSignature, + VehicleUI, +} from './types'; + +const vehicleInfos: VehicleInfoSignature = vehicleInfosConfig; +const vehicleStatuses: VehicleStatusSignature = vehicleStatusesConfig; + // Called by map container and vehicle container to update vehicles being shown. -export function updateVehicles(component, vehicles) { +export function updateVehicles( + component: Component<{}, { vehicles: { [sid: string]: VehicleUI } }>, vehicles: Vehicle[], +): void { const { vehicles: thisVehicles } = component.state; const currentVehicles = thisVehicles; @@ -13,7 +28,7 @@ export function updateVehicles(component, vehicles) { currentVehicles[vehicle.sid] = { ...vehicle, ...vehicleInfos[vehicle.sid], - status: vehicleStatuses[vehicle.status], + status: vehicleStatuses[vehicle.status] as { type: MessageType; message: string }, }; }); @@ -29,15 +44,15 @@ export function updateVehicles(component, vehicles) { * function that sends out this notification. */ -function sendStartMission() { +function sendStartMission(): void { ipcRenderer.send('post', 'startMission'); } -function sendStopMission() { +function sendStopMission(): void { ipcRenderer.send('post', 'stopMission'); } -function sendCompleteMission(index) { +function sendCompleteMission(index: number): void { ipcRenderer.send('post', 'completeMission', index); } @@ -48,22 +63,22 @@ export const mission = { }; // We can have this or directly call function from Orchestrator. -function sendStartJob(data) { +function sendStartJob(data: { jobType: string; missionInfo: {} }): void { ipcRenderer.send('post', 'startJob', data); } // We can have this or directly call function from Orchestrator. -function sendPauseJob() { +function sendPauseJob(): void { ipcRenderer.send('post', 'pauseJob'); } // We can have this or directly call function from Orchestrator. -function sendResumeJob() { +function sendResumeJob(): void { ipcRenderer.send('post', 'resumeJob'); } // Orchestrator should call this function, or send the notification directly. -function sendCompleteJob() { +function sendCompleteJob(): void { ipcRenderer.send('post', 'completeJob'); } @@ -81,14 +96,15 @@ export const job = { * We will use hex for all floats so these functions will be in help. */ -function toHexString(float) { - const getHex = i => `00${i.toString(16)}`.slice(-2); +function toHexString(float: number): string { + const getHex = (i: number): string => `00${i.toString(16)}`.slice(-2); + const view = new DataView(new ArrayBuffer(4)); view.setFloat32(0, float); return [0, 0, 0, 0].map((_, i) => getHex(view.getUint8(i))).join(''); } -function toFloat(hexString) { +function toFloat(hexString: string): number { const int = parseInt(hexString, 16); if (int > 0 || int < 0) { const sign = (int >>> 31) ? -1 : 1; @@ -111,10 +127,3 @@ function toFloat(hexString) { export const floatHex = { toHexString, toFloat }; /* eslint-enable no-bitwise */ - -export default { - updateVehicles, - job, - mission, - floatHex, -}; diff --git a/tsconfig.json b/tsconfig.json index 5008cd10..699fd7d2 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,8 +3,6 @@ "compilerOptions": { "jsx": "react", - "noUnusedLocals": false, - "resolveJsonModule": true, "esModuleInterop": true } From 8781027939a4405cdce6766f1f8bed2ffeb3ad3d Mon Sep 17 00:00:00 2001 From: Paul Matthew Barrameda Date: Fri, 29 Mar 2019 23:52:23 -0700 Subject: [PATCH 028/152] Added comments to code Helps with seeing what the variables are when hovering over them. CachedTileLayer doesnt work right now and will fix soon. --- package.json | 1 + src/config/config.json | 1 - src/config/index.ts | 16 +- src/main/index.ts | 85 +++++++++-- src/renderer/fixtures/completeJobFixture.ts | 3 + .../fixtures/updateMessagesFixtures.ts | 3 + .../fixtures/updateVehiclesFixtures.ts | 3 + src/renderer/mainWindow/log/LogContainer.tsx | 49 +++++- .../mainWindow/map/CachedTileLayer.ts | 20 +-- src/renderer/mainWindow/map/MapContainer.tsx | 31 +++- src/renderer/mainWindow/map/VehicleMarker.tsx | 7 +- .../mainWindow/map/control/Control.tsx | 15 ++ .../map/control/GeolocationControl.tsx | 3 + .../mainWindow/map/control/ThemeControl.tsx | 3 + src/util/types.ts | 143 ++++++++++++++++-- src/util/util.ts | 22 +-- 16 files changed, 345 insertions(+), 60 deletions(-) diff --git a/package.json b/package.json index f5dd9f26..65f02745 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "build": "electron-webpack && electron-builder" }, "dependencies": { + "@yaga/leaflet-cached-tile-layer": "^1.0.0", "leaflet": "^1.4.0", "moment": "^2.24.0", "pouchdb": "^7.0.0", diff --git a/src/config/config.json b/src/config/config.json index b1c74fe5..0e6c343d 100644 --- a/src/config/config.json +++ b/src/config/config.json @@ -1,5 +1,4 @@ { "fixtures": true, - "cache": false, "geolocation": true } diff --git a/src/config/index.ts b/src/config/index.ts index dee4ebae..69ced48d 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -25,12 +25,23 @@ import geolocation_img from './images/other/geolocation.png'; import moon_hover from './images/other/moon_hover.png'; import moon from './images/other/moon.png'; -import { cache, fixtures, geolocation } from './config.json'; +import { fixtures, geolocation } from './config.json'; import { startLocation, locations } from './locations.json'; import { vehicleIds, vehicleInfos, vehicleStatuses } from './vehicle.json'; +/* + * TODO: add typings to all variables before exporting (and remove the corresponding typings) + * in types.ts. + */ + export { - cache, fixtures, geolocation, startLocation, locations, vehicleIds, vehicleInfos, vehicleStatuses, + fixtures, + geolocation, + startLocation, + locations, + vehicleIds, + vehicleInfos, + vehicleStatuses, }; export const images = { @@ -60,7 +71,6 @@ export const images = { }; export default { - cache, fixtures, images, geolocation, diff --git a/src/main/index.ts b/src/main/index.ts index 31872c39..3f435ac9 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -17,35 +17,66 @@ import { images, locations as locationsConfig } from '../config/index'; import { LocationSignature } from '../util/types'; -// Recast locations to LocationLabel to allow us to use keys properly. +/** + * List of locations that are in the locations configuration file. + * Will be loaded under the locations column in the menu. + */ const locations: LocationSignature = locationsConfig; -/* +/** * This key is required to enable geolocation in the application. * Others cannot use this key outside of geolocation access so no need to hide it. */ process.env.GOOGLE_API_KEY = 'AIzaSyB1gepR_EONqgEcxuADmEZjizTuOU_cfnU'; +/** + * Filter constant for all configuration files. + */ const FILTER = { name: 'GCS Configuration', extensions: ['json'] }; + +/** + * Width of the application. Main window will have this width while mission window + * will have 1/3rd of it. + */ const WIDTH = 1024; + +/** + * Height of the application. Both main and mission windows will have this height. + */ const HEIGHT = 576; +/** + * Returns true if running as development (npm start) but false if running in build + * (the app that has come from npm build). + */ const isDevelopment = process.env.NODE_ENV !== 'production'; // TODO: Put icon tray back to macOS but resize it so that its not huge on macOS's menu. const icon = nativeImage.createFromDataURL(images.icon); -// Variable to keep track when the app will quit, which is different from hiding the app. +/** + *Variable to keep track when the app will quit, which is different from hiding the app. + */ let quitting = false; -// References to window objects. +/** + * Reference to the main window of the application. + */ let mainWindow: BrowserWindow | null; + +/** + * Reference to the mission window of the application. + */ let missionWindow: BrowserWindow | null; -// Tray object. +/** + * Reference to the tray object of the application. + */ let tray: Tray; -// Role added to menus to allow the user to quit the app. Shortcut is Ctrl/Cmd + Q. +/** + * Role added to menus to allow the user to quit the app. Shortcut is Ctrl/Cmd + Q. + */ const quitRole: MenuItemConstructorOptions = { label: 'Quit', accelerator: 'CommandOrControl+Q', @@ -55,7 +86,9 @@ const quitRole: MenuItemConstructorOptions = { }, }; -// Menu prepended to menu if application is running on a Darwin-based OS. +/** + * Menu prepended to menu if application is running on a Darwin-based OS. + */ const darwinMenu: MenuItemConstructorOptions = { label: 'NGCP Ground Control System', submenu: [ @@ -137,7 +170,9 @@ function loadConfig(): void { } } -// Menu displayed on main window. +/** + * Reference to the menu displayed on main window. + */ const menu: MenuItemConstructorOptions[] = [ { label: 'File', @@ -219,11 +254,14 @@ function setLocationMenu(): void { const location = menu.find(m => m.label === 'Locations'); if (!location) return; - const locationMenu = location.submenu; - if (!locationMenu) return; + const { submenu } = location; + if (!submenu) return; + + // Cast the submenu variable to locationMenu as a MenuItemContsturctorOptions array. + const locationMenu: MenuItemConstructorOptions[] = submenu as MenuItemConstructorOptions[]; if (!locations || Object.keys(locations).length === 0) { - (locationMenu as MenuItemConstructorOptions[]).push({ + locationMenu.push({ label: 'No locations defined', enabled: false, }); @@ -231,7 +269,7 @@ function setLocationMenu(): void { } Object.keys(locations).forEach((label) => { - (locationMenu as MenuItemConstructorOptions[]).push({ + locationMenu.push({ label, click(menuItem) { if (mainWindow) { @@ -242,6 +280,9 @@ function setLocationMenu(): void { }); } +/** + * Hides the mission window. + */ function hideMissionWindow(): void { if (mainWindow) { mainWindow.webContents.send('setSelectedMission', -1); @@ -332,7 +373,10 @@ function createMissionWindow(): void { }); } -function showWindow(): void { +/** + * Shows the main window. + */ +function showMainWindow(): void { if (!mainWindow) { createMainWindow(); } else { @@ -340,6 +384,9 @@ function showWindow(): void { } } +/** + * Shows the mission window. + */ function showMissionWindow(): void { if (!missionWindow) { createMissionWindow(); @@ -354,12 +401,15 @@ function showMissionWindow(): void { const trayMenu: MenuItemConstructorOptions[] = [ { label: 'NGCP Ground Control Station', - click() { showWindow(); }, + click() { showMainWindow(); }, }, { type: 'separator' }, quitRole, ]; +/** + * Creates the menu object by adding locations and other platform specific menu to it. + */ function createMenu(): void { setLocationMenu(); @@ -372,15 +422,18 @@ function createMenu(): void { Menu.setApplicationMenu(Menu.buildFromTemplate(menu)); } +/** + * Creates the tray object. + */ function createTray(): void { tray = new Tray(icon); tray.setContextMenu(Menu.buildFromTemplate(trayMenu)); - tray.on('click', () => { showWindow(); }); + tray.on('click', () => { showMainWindow(); }); } -app.on('activate', showWindow); +app.on('activate', showMainWindow); app.on('ready', () => { createMainWindow(); diff --git a/src/renderer/fixtures/completeJobFixture.ts b/src/renderer/fixtures/completeJobFixture.ts index c1189f9c..0dbccd3c 100644 --- a/src/renderer/fixtures/completeJobFixture.ts +++ b/src/renderer/fixtures/completeJobFixture.ts @@ -1,5 +1,8 @@ import { ipcRenderer } from 'electron'; // eslint-disable-line import/no-extraneous-dependencies +/** + * Sends a completeJob notification 5 seconds after the startJob notification is received. + */ export default function completeJob(): void { // Sends completeJob notification after receiving startJob notification. ipcRenderer.on('startJob', () => { diff --git a/src/renderer/fixtures/updateMessagesFixtures.ts b/src/renderer/fixtures/updateMessagesFixtures.ts index 231e2195..de14a18a 100644 --- a/src/renderer/fixtures/updateMessagesFixtures.ts +++ b/src/renderer/fixtures/updateMessagesFixtures.ts @@ -21,6 +21,9 @@ const fixtures: { type?: MessageType; message: string }[] = [ }, ]; +/** + * Sends an updateMessages notification with random message fixtures. + */ export default function updateMessages(): void { const fixture = fixtures[Math.floor(Math.random() * fixtures.length)]; ipcRenderer.send('post', 'updateMessages', [fixture]); diff --git a/src/renderer/fixtures/updateVehiclesFixtures.ts b/src/renderer/fixtures/updateVehiclesFixtures.ts index 99e891a8..ed04ec31 100644 --- a/src/renderer/fixtures/updateVehiclesFixtures.ts +++ b/src/renderer/fixtures/updateVehiclesFixtures.ts @@ -28,6 +28,9 @@ let fixtures: Vehicle[] = [ const status = Object.keys(vehicleStatuses); +/** + * Sends an updateVehicle notification with random vehicle fixtures. + */ export default function updateVehicles(): void { const newFixtures = fixtures.map(fixture => ({ ...fixture, diff --git a/src/renderer/mainWindow/log/LogContainer.tsx b/src/renderer/mainWindow/log/LogContainer.tsx index aefc5262..0192d2fb 100644 --- a/src/renderer/mainWindow/log/LogContainer.tsx +++ b/src/renderer/mainWindow/log/LogContainer.tsx @@ -10,18 +10,51 @@ import { MessageType, ThemeProps } from '../../../util/types'; // eslint-disable import './log.css'; +/** + * Interface for a message in the log container. + */ interface Message { + /** + * The type of the message. Defines the color the message will be printed in. + */ type?: MessageType; + + /** + * The content of the message. + */ message: string; + + /** + * The time was received and logged. + */ time: Moment; } interface State { + /** + * The current filter being applied to messages. If the filter is not "", then only messages + * of the same type as the filter will be shown. + */ filter: MessageType; + + /** + * All messages that have been logged. This includes message that are being hidden + * if a filter is being applied. + */ messages: Message[]; + + /** + * All messages that are being shown. If there's no filter, then this is the same + * as messages. We have this as it improves performance (prevents having to filter + * message every time the component is re-rendered) for a space (a duplicate array + * of messages). + */ filteredMessages: Message[]; } +/** + * Container that displays messages regarding status, error, etc. + */ export default class LogContainer extends Component { public constructor(props: ThemeProps) { super(props); @@ -47,9 +80,14 @@ export default class LogContainer extends Component { ipcRenderer.on('updateMessages', (_: Event, messages: Message[]) => this.updateMessages(messages)); } - // Must declare instance variables after componentDidMount function. See react/sort-comp. + /** + * Cache that stores the height for all log messages. Allows the messages to have proper height. + */ private heightCache: CellMeasurerCache; + /** + * Custom function to render a row in the list. + */ private rowRenderer(props: ListRowProps): ReactNode { const { filteredMessages } = this.state; const { @@ -73,6 +111,9 @@ export default class LogContainer extends Component { ); } + /** + * Clears all the messages in the log. + */ private clearMessages(): void { this.heightCache.clearAll(); @@ -83,6 +124,9 @@ export default class LogContainer extends Component { }); } + /** + * Changes the filter applied to the log. + */ private updateFilter(event: ChangeEvent): void { const { messages } = this.state; @@ -95,6 +139,9 @@ export default class LogContainer extends Component { }); } + /** + * Updates the messages in the log. Will update filtered messages accordingly. + */ private updateMessages(messages: Message[]): void { const { filteredMessages, messages: thisMessages, filter } = this.state; const currentMessages = thisMessages; diff --git a/src/renderer/mainWindow/map/CachedTileLayer.ts b/src/renderer/mainWindow/map/CachedTileLayer.ts index df13aa94..befb2999 100644 --- a/src/renderer/mainWindow/map/CachedTileLayer.ts +++ b/src/renderer/mainWindow/map/CachedTileLayer.ts @@ -1,18 +1,20 @@ -// TODO: Use TileLayer from https://github.com/yagajs/leaflet-cached-tile-layer - /* - * import { GridLayer, withLeaflet } from 'react-leaflet'; + * import { CachedTileLayer as LeafletCachedTileLayer } from '@yaga/leaflet-cached-tile-layer' + * import { GridLayer, GridLayerProps, withLeaflet } from 'react-leaflet'; * - * class CachedTileLayer extends GridLayer { - * public createLeafletElement(props: any): L.GridLayer { - * return new TileLayer(props.url, this.getOptions(props)); + * type LeafletElement = LeafletCachedTileLayer; + * type Props = { url: string } & GridLayerProps + * + * class CachedTileLayer extends GridLayer { + * public createLeafletElement(props: Props): LeafletElement { + * return new CachedTileLayer(props.url, this.getOptions(props)); * } * - * public updateLeafletElement(fromProps: any, toProps: any): void { + * public updateLafletElement(fromProps: Props, toProps: Props): void { * super.updateLeafletElement(fromProps, toProps); * - * if (toProps.url !== fromProps.url) { - * (this.leafletElement as any).setUrl(toProps.url); + * if(toProps.url !== fromProps.url) { + * this.leafletElement.setUrl(toProps.url); * } * } * } diff --git a/src/renderer/mainWindow/map/MapContainer.tsx b/src/renderer/mainWindow/map/MapContainer.tsx index cea0a8cb..ca792e12 100644 --- a/src/renderer/mainWindow/map/MapContainer.tsx +++ b/src/renderer/mainWindow/map/MapContainer.tsx @@ -7,7 +7,7 @@ import React, { import { Map, TileLayer, Viewport } from 'react-leaflet'; import { - cache, locations as locationsConfig, startLocation, + locations as locationsConfig, startLocation, } from '../../../config/index'; import { @@ -29,12 +29,9 @@ const mapOptions = { attribution: 'Map data © OpenStreetMap contributors, CC-BY-SA, Imagery © Mapbox', url: 'https://api.tiles.mapbox.com/v4/{id}/{z}/{x}/{y}.png?access_token={accessToken}', id: 'mapbox.satellite', - accessToken: process.env.MAPBOX_TOKEN, - useCache: cache, - crossOrigin: true, }; -// default location is (0, 0) unless there is a locations file defined already +// Default location is (0, 0) unless there is a locations file defined already. let start = { lat: 0, lng: 0, zoom: 18 }; if (startLocation && locations[startLocation]) { start = { ...start, ...locations[startLocation] }; @@ -79,14 +76,23 @@ export default class MapContainer extends Component { ipcRenderer.on('updateVehicles', (_: Event, vehicles: Vehicle[]) => updateVehicles(this, vehicles)); } + /** + * Callback to whenever map viewport is changed. + */ private onViewportChanged(viewport: Viewport): void { this.setState({ viewport }); } + /** + * Callback to whenever geolocation call is successful. + */ private onlocationfound(location: LocationEvent): void { this.updateMapLocation(location.latlng); } + /** + * Centers map to user location using geolocation (if possible). + */ private setMapToUserLocation(): void { const map = this.ref.current; if (map) { @@ -94,12 +100,21 @@ export default class MapContainer extends Component { } } + /** + * Reference to map (to access its leaflet element). + */ private ref: RefObject; + /** + * Callback whenever a "loadConfig" notification is received. + */ private loadConfig({ map }: FileLoadOptions): void { this.updateMapLocation(map); } + /** + * Callback whenever a "saveConfig" notification is received. + */ private saveConfig(file: FileSaveOptions): void { // Performs a hard copy of this.state, deletes vehicles from it, and saves it to file. const data = { ...this.state }; @@ -111,10 +126,16 @@ export default class MapContainer extends Component { }, null, 2)); } + /** + * Centers map around a certain vehicle. + */ private centerMapToVehicle(vehicle: VehicleUI): void { this.updateMapLocation(vehicle); } + /** + * Callback to whenever the map location has been changed. + */ public updateMapLocation(location: LatLngZoom): void { const { lat, lng, zoom } = location; diff --git a/src/renderer/mainWindow/map/VehicleMarker.tsx b/src/renderer/mainWindow/map/VehicleMarker.tsx index d43bc147..d2c541d7 100644 --- a/src/renderer/mainWindow/map/VehicleMarker.tsx +++ b/src/renderer/mainWindow/map/VehicleMarker.tsx @@ -13,7 +13,12 @@ interface VehicleSignature { const { vehicles }: { vehicles: VehicleSignature } = images.markers; -export default class VehicleMarker extends PureComponent { +type VehicleMarkerProps = VehicleUI; + +/** + * Marker that will represent a vehicle. Used by the map container. + */ +export default class VehicleMarker extends PureComponent { public render(): ReactNode { const { sid, name, lat, lng, type, status, diff --git a/src/renderer/mainWindow/map/control/Control.tsx b/src/renderer/mainWindow/map/control/Control.tsx index 95995d7c..07a715a8 100644 --- a/src/renderer/mainWindow/map/control/Control.tsx +++ b/src/renderer/mainWindow/map/control/Control.tsx @@ -6,9 +6,24 @@ import LeafletControl from 'react-leaflet-control'; type ControlPosition = 'topleft' | 'topright' | 'bottomleft' | 'bottomright'; export interface ControlProps { + /** + * Name of the control. + */ className: string; + + /** + * Callback function to whenever the control is clicked. + */ onClick: () => void; + + /** + * Position in the map the control is in. + */ position: ControlPosition; + + /** + * Title to display when hovering over the control. + */ title?: string; } diff --git a/src/renderer/mainWindow/map/control/GeolocationControl.tsx b/src/renderer/mainWindow/map/control/GeolocationControl.tsx index 3bf56304..a07f0144 100644 --- a/src/renderer/mainWindow/map/control/GeolocationControl.tsx +++ b/src/renderer/mainWindow/map/control/GeolocationControl.tsx @@ -3,6 +3,9 @@ import React, { Component, ReactNode } from 'react'; import Control from './Control'; +/** + * Control that centers map when clicked. + */ export default class GeolocationControl extends Component { private static onClick(): void{ ipcRenderer.send('post', 'setMapToUserLocation'); diff --git a/src/renderer/mainWindow/map/control/ThemeControl.tsx b/src/renderer/mainWindow/map/control/ThemeControl.tsx index 53e221ba..02b466ec 100644 --- a/src/renderer/mainWindow/map/control/ThemeControl.tsx +++ b/src/renderer/mainWindow/map/control/ThemeControl.tsx @@ -5,6 +5,9 @@ import { ThemeProps } from '../../../../util/types'; import Control from './Control'; +/** + * Control that toggles theme when clicked. + */ export default class ThemeControl extends Component { private static onClick(): void { ipcRenderer.send('post', 'toggleTheme'); diff --git a/src/util/types.ts b/src/util/types.ts index da3b880f..179226ea 100644 --- a/src/util/types.ts +++ b/src/util/types.ts @@ -1,79 +1,191 @@ -// Typings for props for React components. +/** + * Props with theme as its child. Feel free to extend this prop. + */ export interface ThemeProps { + /** + * "Light" for light theme, "dark" for dark theme. + */ theme: 'light' | 'dark'; } -// Type guard for ThemeProps. +/** + * Type guard to ThemeProps. Checks if the theme property is "light" or "dark". + */ export function isThemeProps(props: { theme: string }): boolean { return props.theme === 'light' || props.theme === 'dark'; } -// Typings for messages. +/** + * Types for all messages. Consists of "success", "failure", "progress", or "". + */ export type MessageType = '' | 'success' | 'failure' | 'progress'; -// Type guard for MessageType. +/** + * Type guard to MessageType. checks if the type is "success", "failure", "progress", or "". + */ export function isMessageType(type: string): boolean { return type === '' || type === 'success' || type === 'failure' || type === 'progress'; } -// Typings for vehicles. +/** + * Types for all vehicle status. These statuses are generated by the vehicle depending on the + * mission or its status, generated by the Orchestrator. + */ export type VehicleStatus = 'ready' | 'error' | 'disconnected' | 'waiting' | 'running' | 'paused'; -// Type guard for VehicleStatus. +/** + * Type guard for VehicleStatus. + */ export function isVehicleStatus(status: string): boolean { return status === 'ready' || status === 'error' || status === 'disconnected' || status === 'waiting' || status === 'running' || status === 'paused'; } -// Vehicle objects being stored. Probably will move to Orchestrator classes. +/** + * Actual vehicle values. This can change as Orchestrator is re-implemented into TypeScript. + */ export interface Vehicle { + /** + * The ID of the vehicle. + */ sid: number; + + /** + * Latitude location of the vehicle. + */ lat: number; + + /** + * Longitude location of the vehicle. + */ lng: number; + + /** + * Altitude of the vehicle. + */ alt?: number; + + /** + * Current vehicle heading. + */ heading?: number; + + /** + * Current battery of the vehicle, expressed as a decimal. Will vary from 0 to 1. + */ battery?: number; + + /** + * Status of the vehicle. Generated by the vehicles themselves or by the Orchestrator. + */ status: VehicleStatus; errorMessage?: string; } -// Vehicle objects being stored in React components. +/** + * Vehicle object typings that are used in the user interface (mainly map and vehicle container). + */ export interface VehicleUI { + /** + * The ID of the vehicle. + */ sid: number; + + /** + * The name of the vehicle. + */ name: string; + + /** + * The type of the vehicle. + */ type: string; + + /** + * Latitude location of the vehicle. + */ lat: number; + + /** + * Longitude location of the vehicle. + */ lng: number; + + /** + * Altitude of the vehicle. + */ alt?: number; + + /** + * Current vehicle heading. + */ heading?: number; + + /** + * Current battery of the vehicle, expressed as a decimal. Will vary from 0 to 1. + */ battery?: number; + + /** + * Status of the vehicle. This is the way the status is displayed on the user interface. + */ status: { + /** + * The type of status current status is. + */ type: MessageType; + + /** + * The content of the message of the status. + */ message: string; }; errorMessage?: string; } +/** + * Types for maps to use. + */ export interface LatLngZoom { lat: number; lng: number; zoom?: number; } -// Types for save/load configs. +/** + * Data contents for information that is loaded from a configuration file. + */ +export interface FileLoadOptions { + /** + * Information related to the map. + */ + map: LatLngZoom; +} + +/** + * Object structure for information stored into a configuration file. + */ export interface FileSaveOptions { + /** + * Filepath of the configuration file being saved. + */ filePath: string; - data: {}; -} -export interface FileLoadOptions { - map: LatLngZoom; + /** + * Data contents. Will be modified by classes through the "loadConfig" notification. + */ + data: FileLoadOptions; } -// Signatures that are being used in more than one file, makes more sense to have it here to import. +/** + * Signature to the locations variable in locations.json. + */ export interface LocationSignature { [key: string]: { lat: number; lng: number; zoom: number }; } +/** + * Signature to the vehicleInfos variable in vehicle.json. + */ export interface VehicleInfoSignature { [key: string]: { macAddress: string; @@ -82,6 +194,9 @@ export interface VehicleInfoSignature { }; } +/** + * Signature to the vehicleStatuses variable in vehicle.json. + */ export interface VehicleStatusSignature { [key: string]: { type: string; diff --git a/src/util/util.ts b/src/util/util.ts index ea2ff7ce..32dac0d3 100644 --- a/src/util/util.ts +++ b/src/util/util.ts @@ -17,7 +17,9 @@ import { const vehicleInfos: VehicleInfoSignature = vehicleInfosConfig; const vehicleStatuses: VehicleStatusSignature = vehicleStatusesConfig; -// Called by map container and vehicle container to update vehicles being shown. +/** + * Updates vehicles being shown. + */ export function updateVehicles( component: Component<{}, { vehicles: { [sid: string]: VehicleUI } }>, vehicles: Vehicle[], ): void { @@ -62,22 +64,21 @@ export const mission = { sendCompleteMission, }; -// We can have this or directly call function from Orchestrator. -function sendStartJob(data: { jobType: string; missionInfo: {} }): void { +function sendStartJob(data: { + jobType: string; + missionInfo: {}; +}): void { ipcRenderer.send('post', 'startJob', data); } -// We can have this or directly call function from Orchestrator. function sendPauseJob(): void { ipcRenderer.send('post', 'pauseJob'); } -// We can have this or directly call function from Orchestrator. function sendResumeJob(): void { ipcRenderer.send('post', 'resumeJob'); } -// Orchestrator should call this function, or send the notification directly. function sendCompleteJob(): void { ipcRenderer.send('post', 'completeJob'); } @@ -91,11 +92,9 @@ export const job = { /* eslint-disable no-bitwise */ -/* - * Bitshifting allows us to switch between hex and float. - * We will use hex for all floats so these functions will be in help. +/** + * Converts a float number to a hex string. */ - function toHexString(float: number): string { const getHex = (i: number): string => `00${i.toString(16)}`.slice(-2); @@ -104,6 +103,9 @@ function toHexString(float: number): string { return [0, 0, 0, 0].map((_, i) => getHex(view.getUint8(i))).join(''); } +/** + * Converts a hex string to a float number. + */ function toFloat(hexString: string): number { const int = parseInt(hexString, 16); if (int > 0 || int < 0) { From 39e74bdb9de8409daa1f828d7c6400b6c941c973 Mon Sep 17 00:00:00 2001 From: Paul Matthew Barrameda Date: Sat, 30 Mar 2019 02:46:39 -0700 Subject: [PATCH 029/152] Finished main window typescript conversion --- .../{MainWindow.jsx => MainWindow.tsx} | 16 +-- src/renderer/mainWindow/log/LogContainer.tsx | 9 +- src/renderer/mainWindow/map/MapContainer.tsx | 30 ++++- ...sionContainer.jsx => MissionContainer.tsx} | 55 +++++----- .../{MissionTable.jsx => MissionTable.tsx} | 103 ++++++++++++------ .../mainWindow/vehicle/VehicleContainer.jsx | 40 ------- .../mainWindow/vehicle/VehicleContainer.tsx | 50 +++++++++ .../{VehicleTable.jsx => VehicleTable.tsx} | 87 ++++++++++----- .../vehicle/XbeeConnectContainer.jsx | 5 - .../vehicle/XbeeConnectContainer.tsx | 5 + src/util/types.ts | 41 ++++++- src/util/util.ts | 2 +- 12 files changed, 295 insertions(+), 148 deletions(-) rename src/renderer/mainWindow/{MainWindow.jsx => MainWindow.tsx} (68%) rename src/renderer/mainWindow/mission/{MissionContainer.jsx => MissionContainer.tsx} (85%) rename src/renderer/mainWindow/mission/{MissionTable.jsx => MissionTable.tsx} (55%) delete mode 100644 src/renderer/mainWindow/vehicle/VehicleContainer.jsx create mode 100644 src/renderer/mainWindow/vehicle/VehicleContainer.tsx rename src/renderer/mainWindow/vehicle/{VehicleTable.jsx => VehicleTable.tsx} (59%) delete mode 100644 src/renderer/mainWindow/vehicle/XbeeConnectContainer.jsx create mode 100644 src/renderer/mainWindow/vehicle/XbeeConnectContainer.tsx diff --git a/src/renderer/mainWindow/MainWindow.jsx b/src/renderer/mainWindow/MainWindow.tsx similarity index 68% rename from src/renderer/mainWindow/MainWindow.jsx rename to src/renderer/mainWindow/MainWindow.tsx index 2383e7eb..8e3751bb 100644 --- a/src/renderer/mainWindow/MainWindow.jsx +++ b/src/renderer/mainWindow/MainWindow.tsx @@ -1,18 +1,20 @@ -import PropTypes from 'prop-types'; -import React from 'react'; +import React, { ReactNode } from 'react'; import LogContainer from './log/LogContainer'; import MapContainer from './map/MapContainer'; import MissionContainer from './mission/MissionContainer'; import VehicleContainer from './vehicle/VehicleContainer'; +import { ThemeProps } from '../../util/types'; + import './main.css'; -const propTypes = { - theme: PropTypes.oneOf(['light', 'dark']).isRequired, -}; +/** + * Main window component. + */ +export default function MainWindow(props: ThemeProps): ReactNode { + const { theme } = props; -export default function MainWindow({ theme }) { return (
@@ -22,5 +24,3 @@ export default function MainWindow({ theme }) {
); } - -MainWindow.propTypes = propTypes; diff --git a/src/renderer/mainWindow/log/LogContainer.tsx b/src/renderer/mainWindow/log/LogContainer.tsx index 0192d2fb..d3a9ad9d 100644 --- a/src/renderer/mainWindow/log/LogContainer.tsx +++ b/src/renderer/mainWindow/log/LogContainer.tsx @@ -2,7 +2,11 @@ import { Event, ipcRenderer } from 'electron'; // eslint-disable-line import/no- import moment, { Moment } from 'moment'; import React, { ChangeEvent, Component, ReactNode } from 'react'; import { - AutoSizer, CellMeasurerCache, CellMeasurer, List, ListRowProps, + AutoSizer, + CellMeasurerCache, + CellMeasurer, + List, + ListRowProps, } from 'react-virtualized'; // TODO: Remove disable line comment when issue gets fixed (https://github.com/benmosher/eslint-plugin-import/pull/1304) @@ -30,6 +34,9 @@ interface Message { time: Moment; } +/** + * State of the log container. + */ interface State { /** * The current filter being applied to messages. If the filter is not "", then only messages diff --git a/src/renderer/mainWindow/map/MapContainer.tsx b/src/renderer/mainWindow/map/MapContainer.tsx index ca792e12..7bf4804d 100644 --- a/src/renderer/mainWindow/map/MapContainer.tsx +++ b/src/renderer/mainWindow/map/MapContainer.tsx @@ -1,8 +1,12 @@ -import { ipcRenderer, Event } from 'electron'; // eslint-disable-line import/no-extraneous-dependencies +import { Event, ipcRenderer } from 'electron'; // eslint-disable-line import/no-extraneous-dependencies import fs from 'fs'; import { LocationEvent } from 'leaflet'; import React, { - Component, createRef, Fragment, ReactNode, RefObject, + Component, + createRef, + Fragment, + ReactNode, + RefObject, } from 'react'; import { Map, TileLayer, Viewport } from 'react-leaflet'; @@ -29,19 +33,37 @@ const mapOptions = { attribution: 'Map data © OpenStreetMap contributors, CC-BY-SA, Imagery © Mapbox', url: 'https://api.tiles.mapbox.com/v4/{id}/{z}/{x}/{y}.png?access_token={accessToken}', id: 'mapbox.satellite', + accessToken: process.env.MAPBOX_TOKEN, }; -// Default location is (0, 0) unless there is a locations file defined already. -let start = { lat: 0, lng: 0, zoom: 18 }; +/* + * Logic to set up the start location for the map. + * Default location is (0, 0) unless there is a locations file defined already. + */ +let start: LatLngZoom = { lat: 0, lng: 0, zoom: 18 }; if (startLocation && locations[startLocation]) { start = { ...start, ...locations[startLocation] }; } +/** + * State of the map container. + */ interface State extends LatLngZoom { + /** + * Object of vehicles displayed in the user interface. + */ vehicles: { [sid: string]: VehicleUI }; + + /** + * Viewport storing locations on location of map. This is more precise than + * the (lat, lng, zoom) coordinates. + */ viewport: Viewport; } +/** + * Container that displays all vehicles and mission related info in a map. + */ export default class MapContainer extends Component { public constructor(props: ThemeProps) { super(props); diff --git a/src/renderer/mainWindow/mission/MissionContainer.jsx b/src/renderer/mainWindow/mission/MissionContainer.tsx similarity index 85% rename from src/renderer/mainWindow/mission/MissionContainer.jsx rename to src/renderer/mainWindow/mission/MissionContainer.tsx index 46d49945..bf18e2b8 100644 --- a/src/renderer/mainWindow/mission/MissionContainer.jsx +++ b/src/renderer/mainWindow/mission/MissionContainer.tsx @@ -7,19 +7,16 @@ * container can provide this information. */ -import { ipcRenderer } from 'electron'; // eslint-disable-line import/no-extraneous-dependencies -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; +import { Event, ipcRenderer } from 'electron'; // eslint-disable-line import/no-extraneous-dependencies +import React, { Component, ReactNode } from 'react'; import MissionTable from './MissionTable'; -import './mission.css'; +import { Mission, MissionStatus, ThemeProps } from '../../../util/types'; -const propTypes = { - theme: PropTypes.oneOf(['light', 'dark']).isRequired, -}; +import './mission.css'; -const statusTypes = { +const statusTypes: { [key: string]: MissionStatus } = { notStarted: { name: 'notStarted', message: 'Not started', @@ -42,23 +39,31 @@ const statusTypes = { }, }; -export default class MissionContainer extends Component { - constructor(props) { +/** + * State of the mission container. + */ +interface State { + /** + * Index of the mission that is currently opened in mission window. + * Value of -1 means that no mission is currently opened. + */ + openedMissionIndex: number; + + /** + * Array of missions that needs to be performed. + */ + missions: Mission[]; +} + +export default class MissionContainer extends Component { + public constructor(props: ThemeProps) { super(props); this.state = { - /* - * Index of the mission that is currently opened in mission window. - * Value of -1 means that no mission is currently opened. - */ openedMissionIndex: -1, /* - * We have four missions in the mission state, with each mission having a certain - * number of jobs required to accomplish that mission. - * - * The name field allows the mission window to determine which layout to load for which - * mission. The description and status are for the mission container to render. + * Missions to perform: * * 1) isrSearch: UAV will search for the target. The jobs are the following: * step 1: Assign `takeoff` to UAV. @@ -101,8 +106,8 @@ export default class MissionContainer extends Component { this.updateMission = this.updateMission.bind(this); } - componentDidMount() { - ipcRenderer.on('setSelectedMission', (event, index) => this.setSelectedMission(index)); + public componentDidMount(): void { + ipcRenderer.on('setSelectedMission', (_: Event, index: number) => this.setSelectedMission(index)); ipcRenderer.on('startMission', () => this.updateMission('started')); ipcRenderer.on('stopMission', () => this.updateMission('notStarted')); @@ -122,7 +127,7 @@ export default class MissionContainer extends Component { * * @param {index} number index that points to mission to open, -1 means mission window was closed. */ - setSelectedMission(index) { + private setSelectedMission(index: number): void { const { missions, openedMissionIndex } = this.state; // Opens mission window for specified mission. @@ -184,7 +189,7 @@ export default class MissionContainer extends Component { * @param {string} type Consists of notStarted/started/paused. See the componentDidMount * function to see which name corresponds with which notification. */ - updateMission(statusName) { + private updateMission(statusName: 'notStarted' |'started' | 'paused' | 'completed'): void { const { missions, openedMissionIndex } = this.state; const newMissions = missions; @@ -210,7 +215,7 @@ export default class MissionContainer extends Component { this.setState({ missions: newMissions }); } - render() { + public render(): ReactNode { const { theme } = this.props; const { missions } = this.state; @@ -221,5 +226,3 @@ export default class MissionContainer extends Component { ); } } - -MissionContainer.propTypes = propTypes; diff --git a/src/renderer/mainWindow/mission/MissionTable.jsx b/src/renderer/mainWindow/mission/MissionTable.tsx similarity index 55% rename from src/renderer/mainWindow/mission/MissionTable.jsx rename to src/renderer/mainWindow/mission/MissionTable.tsx index 95dd94f9..f1480ce2 100644 --- a/src/renderer/mainWindow/mission/MissionTable.jsx +++ b/src/renderer/mainWindow/mission/MissionTable.tsx @@ -1,40 +1,60 @@ import { ipcRenderer } from 'electron'; // eslint-disable-line import/no-extraneous-dependencies -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; +import React, { Component, ReactNode } from 'react'; import { - AutoSizer, CellMeasurer, CellMeasurerCache, Column, Table, + AutoSizer, + CellMeasurer, + CellMeasurerCache, + Column, + Index, + RowMouseEventHandlerParams, + Table, + TableCellProps, } from 'react-virtualized'; -const width = { +import { Mission, ThemeProps } from '../../../util/types'; + +interface WidthSignature { + [key: string]: number; +} + +const width: WidthSignature = { description: 0.6, status: 0.4, }; -const propTypes = { - theme: PropTypes.oneOf(['light', 'dark']).isRequired, - missions: PropTypes.arrayOf( - PropTypes.shape({ - description: PropTypes.string.isRequired, - status: PropTypes.shape({ - type: PropTypes.oneOf(['failure', 'progress', 'success']).isRequired, - message: PropTypes.string.isRequired, - }).isRequired, - }), - ).isRequired, -}; +/** + * Props for the misson table. + */ +export interface MissionTableProps extends ThemeProps { + /** + * Array of mission to display. + */ + missions: Mission[]; +} -export default class MissionTable extends Component { - static statusRenderer({ dataKey, rowData }) { +/** + * Table to render all missions in the mission container. + */ +export default class MissionTable extends Component { + /** + * Renderer for the status column. + */ + private static statusRenderer(props: TableCellProps): ReactNode { + const { dataKey, rowData } = props; const { type, message } = rowData.status; return {message}; } - static onRowClick({ index }) { + /** + * Callback when a row is clicked in the table. + */ + private static onRowClick(info: RowMouseEventHandlerParams): void { + const { index } = info; ipcRenderer.send('post', 'setSelectedMission', index); } - constructor(props) { + public constructor(props: MissionTableProps) { super(props); this.width = width; @@ -49,13 +69,28 @@ export default class MissionTable extends Component { this.descriptionRenderer = this.descriptionRenderer.bind(this); } - rowGetter({ index }) { - const { missions } = this.props; + /** + * Width for columns in table. + */ + private width: WidthSignature; + + /** + * Cache that stores the height for rows. Allows the messages to have proper height. + */ + private heightCache: CellMeasurerCache; + /** + * Callback to obtain data for a certain row in the table. + */ + private rowGetter({ index }: Index): Mission { + const { missions } = this.props; return missions[index]; } - rowClassName({ index }) { + /** + * Callback to generate classname for a certain row in the table. + */ + private rowClassName({ index }: Index): string { const { theme } = this.props; if (theme === 'dark' && index === -1) { @@ -66,9 +101,17 @@ export default class MissionTable extends Component { return ''; } - descriptionRenderer({ - dataKey, parent, rowIndex, cellData, - }) { + /** + * Callback to render data for the description column. + */ + private descriptionRenderer(props: TableCellProps): ReactNode { + const { + cellData, + dataKey, + parent, + rowIndex, + } = props; + return (
)} @@ -121,5 +162,3 @@ export default class MissionTable extends Component { ); } } - -MissionTable.propTypes = propTypes; diff --git a/src/renderer/mainWindow/vehicle/VehicleContainer.jsx b/src/renderer/mainWindow/vehicle/VehicleContainer.jsx deleted file mode 100644 index 28e93627..00000000 --- a/src/renderer/mainWindow/vehicle/VehicleContainer.jsx +++ /dev/null @@ -1,40 +0,0 @@ -import { ipcRenderer } from 'electron'; // eslint-disable-line import/no-extraneous-dependencies -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; - -import VehicleTable from './VehicleTable'; - -import { updateVehicles } from '../../../util/util'; - -import './vehicle.css'; - -const propTypes = { - theme: PropTypes.oneOf(['light', 'dark']).isRequired, -}; - -export default class VehicleContainer extends Component { - constructor(props) { - super(props); - - this.state = { - vehicles: {}, - }; - } - - componentDidMount() { - ipcRenderer.on('updateVehicles', (event, data) => updateVehicles(this, data)); - } - - render() { - const { theme } = this.props; - const { vehicles } = this.state; - - return ( -
- -
- ); - } -} - -VehicleContainer.propTypes = propTypes; diff --git a/src/renderer/mainWindow/vehicle/VehicleContainer.tsx b/src/renderer/mainWindow/vehicle/VehicleContainer.tsx new file mode 100644 index 00000000..106dcf27 --- /dev/null +++ b/src/renderer/mainWindow/vehicle/VehicleContainer.tsx @@ -0,0 +1,50 @@ +import { Event, ipcRenderer } from 'electron'; // eslint-disable-line import/no-extraneous-dependencies +import React, { Component, ReactNode } from 'react'; + +import VehicleTable from './VehicleTable'; + +import { updateVehicles } from '../../../util/util'; +import { ThemeProps, Vehicle, VehicleUI } from '../../../util/types'; + +import './vehicle.css'; + +/** + * State of the mission container. + */ +interface State { + /** + * Object of vehicles. + */ + vehicles: { [key: string]: VehicleUI }; +} + +export default class VehicleContainer extends Component { + public constructor(props: ThemeProps) { + super(props); + + this.state = { + vehicles: {}, + }; + } + + public componentDidMount(): void { + ipcRenderer.on('updateVehicles', (_: Event, data: Vehicle[]) => updateVehicles(this, data)); + } + + public render(): ReactNode { + const { theme } = this.props; + const { vehicles } = this.state; + + /* + * TODO: Map vehicles to an array by sid to increase performance by preventing VehicleTable + * from remapping the table N times. + * Will improve the time complexity from O(N^2logN) to O(NlogN) + */ + + return ( +
+ +
+ ); + } +} diff --git a/src/renderer/mainWindow/vehicle/VehicleTable.jsx b/src/renderer/mainWindow/vehicle/VehicleTable.tsx similarity index 59% rename from src/renderer/mainWindow/vehicle/VehicleTable.jsx rename to src/renderer/mainWindow/vehicle/VehicleTable.tsx index 9f74e7f1..904169ab 100644 --- a/src/renderer/mainWindow/vehicle/VehicleTable.jsx +++ b/src/renderer/mainWindow/vehicle/VehicleTable.tsx @@ -1,34 +1,52 @@ import { ipcRenderer } from 'electron'; // eslint-disable-line import/no-extraneous-dependencies -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import { AutoSizer, Table, Column } from 'react-virtualized'; +import React, { Component, ReactNode } from 'react'; +import { + AutoSizer, + Column, + Index, + RowMouseEventHandlerParams, + Table, + TableCellProps, +} from 'react-virtualized'; + +import { ThemeProps, VehicleUI } from '../../../util/types'; + +interface WidthSignature { + [key: string]: number; +} -const width = { +/** + * Width for columns in table. + */ +const width: WidthSignature = { id: 0.2, name: 0.4, status: 0.4, }; -const propTypes = { - theme: PropTypes.oneOf(['light', 'dark']).isRequired, - vehicles: PropTypes.objectOf( - PropTypes.shape({ - sid: PropTypes.number.isRequired, - name: PropTypes.string.isRequired, - status: PropTypes.shape({ - type: PropTypes.oneOf(['failure', 'progress', 'success']).isRequired, - message: PropTypes.string.isRequired, - }).isRequired, - }), - ).isRequired, -}; +/** + * Props for the vehicle table. + */ +export interface VehicleTableProps extends ThemeProps { + /** + * Object of vehicles. + */ + vehicles: { [key: string]: VehicleUI }; +} -export default class VehicleTable extends Component { - static statusRenderer({ rowData }) { +/** + * Table to render all vehicles in the vehicle container. + */ +export default class VehicleTable extends Component { + /** + * Renderer for the status column. + */ + private static statusRenderer(props: TableCellProps): ReactNode { + const { rowData } = props; return {rowData.status.message}; } - constructor(props) { + public constructor(props: VehicleTableProps) { super(props); this.width = width; @@ -38,21 +56,35 @@ export default class VehicleTable extends Component { this.rowClassName = this.rowClassName.bind(this); } - - onRowClick({ rowData }) { + /** + * Callback when a row is clicked in the table. + */ + private onRowClick(info: RowMouseEventHandlerParams): void { + const { rowData } = info; const { vehicles } = this.props; ipcRenderer.send('post', 'centerMapToVehicle', vehicles[rowData.sid]); } - rowGetter({ index }) { + /** + * Width for columns in table. + */ + private width: WidthSignature; + + /** + * Callback to obtain data for a certain row in the table. + */ + private rowGetter({ index }: Index): VehicleUI { const { vehicles } = this.props; const v = Object.keys(vehicles).sort((a, b) => parseInt(a, 10) - parseInt(b, 10)); return vehicles[v[index]]; } - rowClassName({ index }) { + /** + * Callback to generate classname for a certain row in the table. + */ + private rowClassName({ index }: Index): string { const { theme } = this.props; if (theme === 'dark' && index === -1) { @@ -63,7 +95,7 @@ export default class VehicleTable extends Component { return ''; } - render() { + public render(): ReactNode { const { theme, vehicles } = this.props; return ( @@ -84,20 +116,17 @@ export default class VehicleTable extends Component { label="ID" dataKey="sid" width={tableWidth * this.width.id} - rowClassName={this.rowClassName} /> )} @@ -105,5 +134,3 @@ export default class VehicleTable extends Component { ); } } - -VehicleTable.propTypes = propTypes; diff --git a/src/renderer/mainWindow/vehicle/XbeeConnectContainer.jsx b/src/renderer/mainWindow/vehicle/XbeeConnectContainer.jsx deleted file mode 100644 index da3b60e4..00000000 --- a/src/renderer/mainWindow/vehicle/XbeeConnectContainer.jsx +++ /dev/null @@ -1,5 +0,0 @@ -import React from 'react'; - -export default function XbeeConnectContainer() { - return
; -} diff --git a/src/renderer/mainWindow/vehicle/XbeeConnectContainer.tsx b/src/renderer/mainWindow/vehicle/XbeeConnectContainer.tsx new file mode 100644 index 00000000..84159e09 --- /dev/null +++ b/src/renderer/mainWindow/vehicle/XbeeConnectContainer.tsx @@ -0,0 +1,5 @@ +import React, { ReactNode } from 'react'; + +export default function XbeeConnectContainer(): ReactNode { + return
; +} diff --git a/src/util/types.ts b/src/util/types.ts index 179226ea..5446158c 100644 --- a/src/util/types.ts +++ b/src/util/types.ts @@ -146,11 +146,50 @@ export interface VehicleUI { * Types for maps to use. */ export interface LatLngZoom { + /** + * Latitude of center of map. + */ lat: number; + + /** + * Longitude of center of map. + */ lng: number; + + /** + * Zoom level of map. + */ zoom?: number; } +/** + * Status of a mission. + */ +export interface MissionStatus { + name: 'notStarted' |'started' | 'paused' | 'completed'; + message: string; + type: MessageType; +} + +/** + * Structure of a typical mission. + */ +export interface Mission { + /** + * Name of the mission. + */ + name?: string; + /** + * Description of the mission. + */ + description: string; + + /** + * Status of the misison. + */ + status: MissionStatus; +} + /** * Data contents for information that is loaded from a configuration file. */ @@ -180,7 +219,7 @@ export interface FileSaveOptions { * Signature to the locations variable in locations.json. */ export interface LocationSignature { - [key: string]: { lat: number; lng: number; zoom: number }; + [key: string]: LatLngZoom; } /** diff --git a/src/util/util.ts b/src/util/util.ts index 32dac0d3..6d4060ed 100644 --- a/src/util/util.ts +++ b/src/util/util.ts @@ -21,7 +21,7 @@ const vehicleStatuses: VehicleStatusSignature = vehicleStatusesConfig; * Updates vehicles being shown. */ export function updateVehicles( - component: Component<{}, { vehicles: { [sid: string]: VehicleUI } }>, vehicles: Vehicle[], + component: Component<{}, { vehicles: { [key: string]: VehicleUI } }>, vehicles: Vehicle[], ): void { const { vehicles: thisVehicles } = component.state; const currentVehicles = thisVehicles; From 503ad14ec1964f6467a7ad47bf392dd8d3995f8f Mon Sep 17 00:00:00 2001 From: Paul Matthew Barrameda Date: Sat, 30 Mar 2019 04:07:46 -0700 Subject: [PATCH 030/152] Added job to types. I dont think I'll work on integrating TypeScript with mission window. I'll let @maquino123 and @tnleang handle that --- package.json | 1 + src/util/types.ts | 25 +++++++++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/package.json b/package.json index 65f02745..c553fdd8 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "@babel/preset-react": "^7.0.0", "@types/pouchdb": "^6.3.3", "@types/react": "^16.8.8", + "@types/react-dom": "^16.8.3", "@types/react-leaflet": "^2.2.1", "@types/react-virtualized": "^9.21.0", "@typescript-eslint/eslint-plugin": "^1.5.0", diff --git a/src/util/types.ts b/src/util/types.ts index 5446158c..5999040a 100644 --- a/src/util/types.ts +++ b/src/util/types.ts @@ -190,6 +190,31 @@ export interface Mission { status: MissionStatus; } +/** + * Structure of a typical job. + */ +export interface Job { + /** + * Name of the job. + */ + name: string; + + /** + * Description of the job. + */ + description: string; + + /** + * Whether or not the job can be skipped. + */ + optional?: boolean; + + /** + * Whether or not the user can pause the job. + */ + pausable?: boolean; +} + /** * Data contents for information that is loaded from a configuration file. */ From 744c703ac6131297b05978446523b0e10958bdaf Mon Sep 17 00:00:00 2001 From: Paul Matthew Barrameda Date: Sat, 30 Mar 2019 04:22:40 -0700 Subject: [PATCH 031/152] Added FOSSA to Travis testing --- .travis.yml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/.travis.yml b/.travis.yml index d7ab1db8..3ce41c59 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,3 +8,15 @@ node_js: # Cache node_modules after testing cache: npm + +# Install FOSSA to integrate this testing to Travis. +before_script: + - "curl -H 'Cache-Control: no-cache' https://raw.githubusercontent.com/fossas/fossa-cli/master/install.sh | sudo bash" + +# Run the following scripts to analyze build with FOSSA. +script: + - fossa init + - fossa analyze + +after_success: + - fossa test From c815e444d02338c8e90384e87f88e36dc0919cd1 Mon Sep 17 00:00:00 2001 From: Paul Matthew Barrameda Date: Sat, 30 Mar 2019 05:08:21 -0700 Subject: [PATCH 032/152] Updated README and testing to have FOSSA --- .travis.yml | 11 ++++++----- README.md | 5 ++++- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/.travis.yml b/.travis.yml index 3ce41c59..e382c222 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,6 +2,10 @@ language: node_js +# Install fossa-cli +before_script: + - "curl -H 'Cache-Control: no-cache' https://raw.githubusercontent.com/fossas/fossa-cli/master/install.sh | sudo bash" + # Use latest version of Node.js node_js: - node @@ -9,14 +13,11 @@ node_js: # Cache node_modules after testing cache: npm -# Install FOSSA to integrate this testing to Travis. -before_script: - - "curl -H 'Cache-Control: no-cache' https://raw.githubusercontent.com/fossas/fossa-cli/master/install.sh | sudo bash" - -# Run the following scripts to analyze build with FOSSA. +# Run analysis with fossa-cli script: - fossa init - fossa analyze +# Test with fossa-cli (after passing all other tests) after_success: - fossa test diff --git a/README.md b/README.md index cffbbe7e..429a42cb 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [![Build Status](https://travis-ci.com/NGCP/GCS.svg?branch=dev-2018)](https://travis-ci.com/NGCP/GCS) [![dependencies Status](https://david-dm.org/NGCP/GCS/status.svg)](https://david-dm.org/NGCP/GCS) [![devDependencies Status](https://david-dm.org/NGCP/GCS/dev-status.svg)](https://david-dm.org/NGCP/GCS?type=dev) -[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2FNGCP%2FGCS.svg?type=shield)](https://app.fossa.com/projects/git%2Bgithub.com%2FNGCP%2FGCS?ref=badge_shield) +[![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2FNGCP%2FGCS.svg?type=shield)](https://app.fossa.io/projects/git%2Bgithub.com%2FNGCP%2FGCS?ref=badge_shield) The [Northrop Grumman Collaboration Project] presents the Ground Control Station. This project's objective is to view and set missions for all autonomous vehicle platforms in the project. @@ -35,6 +35,9 @@ npm install npm start ``` +## License +[![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2FNGCP%2FGCS.svg?type=large)](https://app.fossa.io/projects/git%2Bgithub.com%2FNGCP%2FGCS?ref=badge_large) + [Northrop Grumman Collaboration Project]: http://www.ngcpcalpoly.com/about.html [Node.js]: https://github.com/nodejs/node [React]: https://github.com/facebook/react From 08217d3ca9301c459097ed1a51821451e330de39 Mon Sep 17 00:00:00 2001 From: Paul Matthew Barrameda Date: Sat, 30 Mar 2019 05:13:42 -0700 Subject: [PATCH 033/152] One more fix --- .travis.yml | 3 ++- README.md | 4 ++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index e382c222..3155a1e8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -18,6 +18,7 @@ script: - fossa init - fossa analyze -# Test with fossa-cli (after passing all other tests) +# Test application if analysis was a success after_success: - fossa test + - npm test diff --git a/README.md b/README.md index 429a42cb..2d64beb7 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,10 @@ npm start ``` ## License +[MIT](https://github.com/NGCP/GCS/blob/dev-2018/LICENSE) + +Licences of this project's dependencies are tracked by FOSSA: + [![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2FNGCP%2FGCS.svg?type=large)](https://app.fossa.io/projects/git%2Bgithub.com%2FNGCP%2FGCS?ref=badge_large) [Northrop Grumman Collaboration Project]: http://www.ngcpcalpoly.com/about.html From c76c4adc8ff330aa9262642543dc9f8a5b1f1617 Mon Sep 17 00:00:00 2001 From: Paul Matthew Barrameda Date: Mon, 1 Apr 2019 19:53:42 -0700 Subject: [PATCH 034/152] Version bump Fixed README too --- .travis.yml | 7 ++----- README.md | 2 +- package.json | 34 ++++++++++++++-------------------- 3 files changed, 17 insertions(+), 26 deletions(-) diff --git a/.travis.yml b/.travis.yml index 3155a1e8..84bd7da5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,12 +13,9 @@ node_js: # Cache node_modules after testing cache: npm -# Run analysis with fossa-cli +# Test application and run analysis with fossa-cli afterwards script: + - npm test - fossa init - fossa analyze - -# Test application if analysis was a success -after_success: - fossa test - - npm test diff --git a/README.md b/README.md index 2d64beb7..d6a4a18e 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ This is based on [Node.js], [React], [Webpack], and [Electron]. **:pencil2: Setting Things Up** Install [Node.js]. The program will not be able to run without it. -Â + Open up your command line application and clone this repository: ```sh diff --git a/package.json b/package.json index c553fdd8..cdbf1172 100644 --- a/package.json +++ b/package.json @@ -1,11 +1,15 @@ { "name": "GCS", - "version": "0.5.2", + "version": "0.6.1", + "license": "MIT", + "author": "Northrop Grumman Collaboration Project", "description": "Ground Control Station for autonomous vehicle platforms in NGCP", + "repository": "https://github.com/NGCP/GCS", "scripts": { "postinstall": "electron-builder install-app-deps", - "test": "npm run linttest", - "linttest": "eslint --ext .js,.jsx,.ts,.tsx \".\" && stylelint \"**/*.css\"", + "test": "npm run test:lint && npm run test:types", + "test:lint": "eslint --ext .js,.jsx,.ts,.tsx \".\" && stylelint \"**/*.css\"", + "test:types": "tsc --project . --outDir dist --pretty", "lint": "eslint --fix --ext .js,.jsx,.ts,.tsx \".\" && stylelint \"**/*.css\" --fix", "start": "electron-webpack dev", "build": "electron-webpack && electron-builder" @@ -16,8 +20,8 @@ "moment": "^2.24.0", "pouchdb": "^7.0.0", "prop-types": "^15.7.2", - "react": "^16.8.5", - "react-dom": "^16.8.5", + "react": "^16.8.6", + "react-dom": "^16.8.6", "react-leaflet": "^2.2.1", "react-leaflet-control": "^2.1.1", "react-virtualized": "^9.21.0", @@ -28,7 +32,7 @@ "devDependencies": { "@babel/preset-react": "^7.0.0", "@types/pouchdb": "^6.3.3", - "@types/react": "^16.8.8", + "@types/react": "^16.8.10", "@types/react-dom": "^16.8.3", "@types/react-leaflet": "^2.2.1", "@types/react-virtualized": "^9.21.0", @@ -36,11 +40,11 @@ "@typescript-eslint/parser": "^1.5.0", "babel-eslint": "^10.0.1", "dotenv-webpack": "^1.7.0", - "electron": "^3.0.2", + "electron": "^3.1.8", "electron-builder": "^20.39.0", "electron-webpack": "^2.6.2", "electron-webpack-ts": "^3.1.1", - "eslint": "^5.15.3", + "eslint": "^5.16.0", "eslint-config-airbnb": "^17.1.0", "eslint-import-resolver-typescript": "^1.1.1", "eslint-plugin-import": "^2.16.0", @@ -49,7 +53,7 @@ "stylelint": "^9.10.1", "stylelint-config-standard": "^18.2.0", "stylelint-order": "^2.1.0", - "typescript": "^3.3.4000", + "typescript": "^3.4.1", "webpack": "^4.29.6" }, "electronWebpack": { @@ -59,15 +63,5 @@ "renderer": { "webpackConfig": "webpack.config.js" } - }, - "repository": { - "type": "git", - "url": "git+https://github.com/NGCP/GCS.git" - }, - "author": "Northrop Grumman Collaboration Project", - "license": "MIT", - "bugs": { - "url": "https://github.com/NGCP/GCS/issues" - }, - "homepage": "https://github.com/NGCP/GCS" + } } From a2d9baf2a5b767d1488f47badb68c2d6610a9fd4 Mon Sep 17 00:00:00 2001 From: Paul Matthew Barrameda Date: Mon, 1 Apr 2019 20:10:44 -0700 Subject: [PATCH 035/152] Fixed bug with log container not updating filter --- src/renderer/mainWindow/log/LogContainer.tsx | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/renderer/mainWindow/log/LogContainer.tsx b/src/renderer/mainWindow/log/LogContainer.tsx index d3a9ad9d..ba5d312f 100644 --- a/src/renderer/mainWindow/log/LogContainer.tsx +++ b/src/renderer/mainWindow/log/LogContainer.tsx @@ -1,6 +1,6 @@ import { Event, ipcRenderer } from 'electron'; // eslint-disable-line import/no-extraneous-dependencies import moment, { Moment } from 'moment'; -import React, { ChangeEvent, Component, ReactNode } from 'react'; +import React, { Component, FormEvent, ReactNode } from 'react'; import { AutoSizer, CellMeasurerCache, @@ -10,7 +10,7 @@ import { } from 'react-virtualized'; // TODO: Remove disable line comment when issue gets fixed (https://github.com/benmosher/eslint-plugin-import/pull/1304) -import { MessageType, ThemeProps } from '../../../util/types'; // eslint-disable-line import/named +import { isMessageType, MessageType, ThemeProps } from '../../../util/types'; // eslint-disable-line import/named import './log.css'; @@ -134,11 +134,14 @@ export default class LogContainer extends Component { /** * Changes the filter applied to the log. */ - private updateFilter(event: ChangeEvent): void { + private updateFilter(event: FormEvent): void { const { messages } = this.state; this.heightCache.clearAll(); - const newFilter = event.target.nodeValue; + const newFilter = event.currentTarget.value; + + // Ensures our new value has a type of MessageType. + if ((!newFilter && newFilter !== '') || !isMessageType(newFilter)) return; this.setState({ filter: newFilter as MessageType, From e7aa590efb831f2d786ca782a2a3157f0d3fa91c Mon Sep 17 00:00:00 2001 From: Paul Matthew Barrameda Date: Wed, 3 Apr 2019 03:31:25 -0700 Subject: [PATCH 036/152] Added basic file to ListDict (now called DictionaryList) DictionaryList makes more sense than ListDict, since this is a dictionary of lists, not a list of dictionaries. Also edited/fixed comments to help with the `atom-typescript` plugin when hovering over variables. Re-allocated declaration files. Turned on a check that will crash the program before loading if MAPBOX_TOKEN is not defined. --- .eslintrc.yml | 3 + src/main/index.ts | 8 ++ src/renderer/mainWindow/map/MapContainer.tsx | 8 +- src/renderer/mainWindow/map/VehicleMarker.tsx | 5 +- .../mainWindow/map/control/Control.tsx | 2 +- .../mainWindow/mission/MissionContainer.tsx | 21 +++--- .../mainWindow/mission/MissionTable.tsx | 4 +- .../mainWindow/vehicle/VehicleContainer.tsx | 5 +- .../mainWindow/vehicle/VehicleTable.tsx | 4 +- src/struct/DictionaryList.ts | 73 +++++++++++++++++++ global.d.ts => typings/image.d.ts | 3 - typings/react-leaflet-control.d.ts | 5 ++ 12 files changed, 119 insertions(+), 22 deletions(-) create mode 100644 src/struct/DictionaryList.ts rename global.d.ts => typings/image.d.ts (55%) create mode 100644 typings/react-leaflet-control.d.ts diff --git a/.eslintrc.yml b/.eslintrc.yml index 36f720ea..114f1e5c 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -26,8 +26,11 @@ settings: rules: linebreak-style: off + # Add strictness to type declarations in TypeScript "@typescript-eslint/explicit-function-return-type": error "@typescript-eslint/no-explicit-any": error + + # Enforce commenting styles multiline-comment-style: error # Modifying react/jsx-filename-extension to support TypeScript React files diff --git a/src/main/index.ts b/src/main/index.ts index 3f435ac9..4c211416 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -436,6 +436,14 @@ function createTray(): void { app.on('activate', showMainWindow); app.on('ready', () => { + /* + * Prevents the app from starting if MapBox token is not set up. + * This is necessary to run the map container. + */ + if (!process.env.MAPBOX_TOKEN) { + throw new Error('Set the MapBox token in .env before launching the application.'); + } + createMainWindow(); createMenu(); diff --git a/src/renderer/mainWindow/map/MapContainer.tsx b/src/renderer/mainWindow/map/MapContainer.tsx index 7bf4804d..52437655 100644 --- a/src/renderer/mainWindow/map/MapContainer.tsx +++ b/src/renderer/mainWindow/map/MapContainer.tsx @@ -15,7 +15,13 @@ import { } from '../../../config/index'; import { - FileLoadOptions, FileSaveOptions, LatLngZoom, LocationSignature, ThemeProps, Vehicle, VehicleUI, + FileLoadOptions, + FileSaveOptions, + LatLngZoom, + LocationSignature, + ThemeProps, + Vehicle, + VehicleUI, } from '../../../util/types'; import { updateVehicles } from '../../../util/util'; diff --git a/src/renderer/mainWindow/map/VehicleMarker.tsx b/src/renderer/mainWindow/map/VehicleMarker.tsx index d2c541d7..48665ef2 100644 --- a/src/renderer/mainWindow/map/VehicleMarker.tsx +++ b/src/renderer/mainWindow/map/VehicleMarker.tsx @@ -8,7 +8,10 @@ import { images } from '../../../config/index'; import { VehicleUI } from '../../../util/types'; // eslint-disable-line import/named interface VehicleSignature { - [key: string]: string; + /** + * Get the vehicle by its vehicle type and status. + */ + [typeStatus: string]: string; } const { vehicles }: { vehicles: VehicleSignature } = images.markers; diff --git a/src/renderer/mainWindow/map/control/Control.tsx b/src/renderer/mainWindow/map/control/Control.tsx index 07a715a8..82e909ad 100644 --- a/src/renderer/mainWindow/map/control/Control.tsx +++ b/src/renderer/mainWindow/map/control/Control.tsx @@ -14,7 +14,7 @@ export interface ControlProps { /** * Callback function to whenever the control is clicked. */ - onClick: () => void; + onClick(): void; /** * Position in the map the control is in. diff --git a/src/renderer/mainWindow/mission/MissionContainer.tsx b/src/renderer/mainWindow/mission/MissionContainer.tsx index bf18e2b8..f6a004a5 100644 --- a/src/renderer/mainWindow/mission/MissionContainer.tsx +++ b/src/renderer/mainWindow/mission/MissionContainer.tsx @@ -1,12 +1,3 @@ -/* - * The mission container will be in charge of managing which missions are not started, started, - * and completed. - * Any other code that wants to interact with missions must go through the mission container. - * For example, if the mission window wants to know which missions are not done or not done, - * the mission - * container can provide this information. - */ - import { Event, ipcRenderer } from 'electron'; // eslint-disable-line import/no-extraneous-dependencies import React, { Component, ReactNode } from 'react'; @@ -55,6 +46,14 @@ interface State { missions: Mission[]; } +/** + * Container that is in charge of managing which missions are not started, started, + * and completed. Any other code that wants to interact with missions must + * go through the mission container. + * + * For example, if the mission window wants to know which missions are not done or not done, + * the mission container can provide this information. + */ export default class MissionContainer extends Component { public constructor(props: ThemeProps) { super(props); @@ -125,7 +124,7 @@ export default class MissionContainer extends Component { * In other words, function will command the mission window to change which mission to display, * as well as update the mission container to show which mission is currently opened. * - * @param {index} number index that points to mission to open, -1 means mission window was closed. + * @param index Index that points to mission to open, -1 means mission window was closed. */ private setSelectedMission(index: number): void { const { missions, openedMissionIndex } = this.state; @@ -186,7 +185,7 @@ export default class MissionContainer extends Component { * Updates mission description in mission container whenever a start/stop/pause/resume * mission notification is sent. * - * @param {string} type Consists of notStarted/started/paused. See the componentDidMount + * @param statusName Consists of notStarted/started/paused. See the componentDidMount * function to see which name corresponds with which notification. */ private updateMission(statusName: 'notStarted' |'started' | 'paused' | 'completed'): void { diff --git a/src/renderer/mainWindow/mission/MissionTable.tsx b/src/renderer/mainWindow/mission/MissionTable.tsx index f1480ce2..12fc670b 100644 --- a/src/renderer/mainWindow/mission/MissionTable.tsx +++ b/src/renderer/mainWindow/mission/MissionTable.tsx @@ -14,7 +14,7 @@ import { import { Mission, ThemeProps } from '../../../util/types'; interface WidthSignature { - [key: string]: number; + [column: string]: number; } const width: WidthSignature = { @@ -27,7 +27,7 @@ const width: WidthSignature = { */ export interface MissionTableProps extends ThemeProps { /** - * Array of mission to display. + * Array of missions that needs to be displayed. */ missions: Mission[]; } diff --git a/src/renderer/mainWindow/vehicle/VehicleContainer.tsx b/src/renderer/mainWindow/vehicle/VehicleContainer.tsx index 106dcf27..33b4da53 100644 --- a/src/renderer/mainWindow/vehicle/VehicleContainer.tsx +++ b/src/renderer/mainWindow/vehicle/VehicleContainer.tsx @@ -15,9 +15,12 @@ interface State { /** * Object of vehicles. */ - vehicles: { [key: string]: VehicleUI }; + vehicles: { [sid: string]: VehicleUI }; } +/** + * Container that shows statuses of all vehicles that are connected to the GCS. + */ export default class VehicleContainer extends Component { public constructor(props: ThemeProps) { super(props); diff --git a/src/renderer/mainWindow/vehicle/VehicleTable.tsx b/src/renderer/mainWindow/vehicle/VehicleTable.tsx index 904169ab..d8a63926 100644 --- a/src/renderer/mainWindow/vehicle/VehicleTable.tsx +++ b/src/renderer/mainWindow/vehicle/VehicleTable.tsx @@ -12,7 +12,7 @@ import { import { ThemeProps, VehicleUI } from '../../../util/types'; interface WidthSignature { - [key: string]: number; + [column: string]: number; } /** @@ -31,7 +31,7 @@ export interface VehicleTableProps extends ThemeProps { /** * Object of vehicles. */ - vehicles: { [key: string]: VehicleUI }; + vehicles: { [sid: string]: VehicleUI }; } /** diff --git a/src/struct/DictionaryList.ts b/src/struct/DictionaryList.ts new file mode 100644 index 00000000..4df02721 --- /dev/null +++ b/src/struct/DictionaryList.ts @@ -0,0 +1,73 @@ +/** + * "Predicate" name and description is taken from official TypeScript definitions. + */ +type Predicate = (value: T, index: number, obj: T[]) => boolean; + +/** + * Modified array class with a modified shift function. + * + *Note: shift is JavaScript's convention of naming the Queue dequeue function. + */ +export class List extends Array { + /** + * Modified version of Array's shift function. Removes all elements from oldest to the element + * where predicate is true. If no element makes predicate true, then no elements are removed and + * undefined is returned. + * + * @param predicate Calls predicate once for each element of the array, in ascending + * order, until it finds one where predicate returns true. If such an element is found, + * array will remove all elements from oldest up to that element. Otherwise, will not + * remove any elements and returns undefined. + */ + public shiftToElement(predicate: Predicate): T[] | undefined { + // If array length is zero or length was modified to a negative number. + if (this.length <= 0) { + return undefined; + } + + const elementIndex = this.findIndex(predicate); + return elementIndex !== -1 ? this.splice(elementIndex) : undefined; + } +} + +/** + * Options for the DictionaryList class. + */ +export interface DictionaryListOptions { + predicate: Predicate; +} + +/** + * Structure that can store lists in a dictionary. Lists can be obtained with a key string, + * and can be modified with custom functions. + */ +export default class DictionaryList { + /** + * Object of string keys to generic type arrays for the class to use. + */ + private dictionary: { [key: string]: List } = {}; + + /** + * Total number of items between all lists in the dictionary. + */ + private numItems = 0; + + /** + * Custom callback function to obtain values for list. + */ + private predicate: Predicate | undefined = undefined; + + public constructor(options?: DictionaryListOptions) { + if (!options) return; + + this.predicate = options.predicate; + } + + /** + * Removes the oldest element from dictionary's list with the given key + * and returns it. + */ + public dequeue(key: string): T | undefined { + return this.dictionary[key].shift(); + } +} diff --git a/global.d.ts b/typings/image.d.ts similarity index 55% rename from global.d.ts rename to typings/image.d.ts index 807ea0b2..a973eed5 100644 --- a/global.d.ts +++ b/typings/image.d.ts @@ -3,6 +3,3 @@ declare module '*.png' { const value: string; export = value; } - -// Allows us to use react-leaflet-control -declare module 'react-leaflet-control'; diff --git a/typings/react-leaflet-control.d.ts b/typings/react-leaflet-control.d.ts new file mode 100644 index 00000000..53cb2319 --- /dev/null +++ b/typings/react-leaflet-control.d.ts @@ -0,0 +1,5 @@ +/* + * Allows us to use react-leaflet-control + * TODO: Add typings to this (and perhaps contribute it to the original repo). + */ +declare module 'react-leaflet-control'; From ae378e7da69d76febb272f26fd07c206b74f2479 Mon Sep 17 00:00:00 2001 From: Paul Matthew Barrameda Date: Wed, 3 Apr 2019 03:34:41 -0700 Subject: [PATCH 037/152] Downgraded TypeScript version to support @typescript-eslint From their repository: The version range of TypeScript currently supported by this parser is >=3.2.1 <3.4.0. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index cdbf1172..317f92ac 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,7 @@ "stylelint": "^9.10.1", "stylelint-config-standard": "^18.2.0", "stylelint-order": "^2.1.0", - "typescript": "^3.4.1", + "typescript": "^3.3.4000", "webpack": "^4.29.6" }, "electronWebpack": { From 276eb469ae9719732cbaef6a0632901f1009b668 Mon Sep 17 00:00:00 2001 From: Paul Matthew Barrameda Date: Wed, 3 Apr 2019 17:33:00 -0700 Subject: [PATCH 038/152] Working to fix TravisCI since I'm not getting any of the errors on my local system --- .travis.yml | 2 +- package.json | 6 +++--- src/struct/DictionaryList.ts | 7 +++---- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/.travis.yml b/.travis.yml index 84bd7da5..c32abd19 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,7 +13,7 @@ node_js: # Cache node_modules after testing cache: npm -# Test application and run analysis with fossa-cli afterwards +# Scripts to run to test application script: - npm test - fossa init diff --git a/package.json b/package.json index 317f92ac..a9872325 100644 --- a/package.json +++ b/package.json @@ -7,10 +7,10 @@ "repository": "https://github.com/NGCP/GCS", "scripts": { "postinstall": "electron-builder install-app-deps", - "test": "npm run test:lint && npm run test:types", - "test:lint": "eslint --ext .js,.jsx,.ts,.tsx \".\" && stylelint \"**/*.css\"", + "test": "npm run test:lint & npm run test:types", + "test:lint": "eslint --ext .js,.jsx,.ts,.tsx \".\" & stylelint \"**/*.css\"", "test:types": "tsc --project . --outDir dist --pretty", - "lint": "eslint --fix --ext .js,.jsx,.ts,.tsx \".\" && stylelint \"**/*.css\" --fix", + "lint": "eslint --fix --ext .js,.jsx,.ts,.tsx \".\" & stylelint \"**/*.css\" --fix", "start": "electron-webpack dev", "build": "electron-webpack && electron-builder" }, diff --git a/src/struct/DictionaryList.ts b/src/struct/DictionaryList.ts index 4df02721..a64be90f 100644 --- a/src/struct/DictionaryList.ts +++ b/src/struct/DictionaryList.ts @@ -6,13 +6,13 @@ type Predicate = (value: T, index: number, obj: T[]) => boolean; /** * Modified array class with a modified shift function. * - *Note: shift is JavaScript's convention of naming the Queue dequeue function. + * Note: shift is JavaScript's convention of naming the Queue's dequeue function. */ export class List extends Array { /** * Modified version of Array's shift function. Removes all elements from oldest to the element - * where predicate is true. If no element makes predicate true, then no elements are removed and - * undefined is returned. + * (inclusive) where predicate is true. If no element makes predicate true, then + * no elements are removed and undefined is returned. * * @param predicate Calls predicate once for each element of the array, in ascending * order, until it finds one where predicate returns true. If such an element is found, @@ -20,7 +20,6 @@ export class List extends Array { * remove any elements and returns undefined. */ public shiftToElement(predicate: Predicate): T[] | undefined { - // If array length is zero or length was modified to a negative number. if (this.length <= 0) { return undefined; } From 01ba9b975a4250149156a193feeffbe21488bb64 Mon Sep 17 00:00:00 2001 From: Paul Matthew Barrameda Date: Wed, 3 Apr 2019 17:42:06 -0700 Subject: [PATCH 039/152] Modified FOSSA badges to point to provided version of repo --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index d6a4a18e..907af015 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [![Build Status](https://travis-ci.com/NGCP/GCS.svg?branch=dev-2018)](https://travis-ci.com/NGCP/GCS) [![dependencies Status](https://david-dm.org/NGCP/GCS/status.svg)](https://david-dm.org/NGCP/GCS) [![devDependencies Status](https://david-dm.org/NGCP/GCS/dev-status.svg)](https://david-dm.org/NGCP/GCS?type=dev) -[![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2FNGCP%2FGCS.svg?type=shield)](https://app.fossa.io/projects/git%2Bgithub.com%2FNGCP%2FGCS?ref=badge_shield) +[![FOSSA Status](https://app.fossa.com/api/projects/custom%2B9570%2Fgithub.com%2FNGCP%2FGCS.svg?type=shield)](https://app.fossa.com/projects/custom%2B9570%2Fgithub.com%2FNGCP%2FGCS?ref=badge_shield) The [Northrop Grumman Collaboration Project] presents the Ground Control Station. This project's objective is to view and set missions for all autonomous vehicle platforms in the project. @@ -40,7 +40,7 @@ npm start Licences of this project's dependencies are tracked by FOSSA: -[![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2FNGCP%2FGCS.svg?type=large)](https://app.fossa.io/projects/git%2Bgithub.com%2FNGCP%2FGCS?ref=badge_large) +[![FOSSA Status](https://app.fossa.com/api/projects/custom%2B9570%2Fgithub.com%2FNGCP%2FGCS.svg?type=large)](https://app.fossa.com/projects/custom%2B9570%2Fgithub.com%2FNGCP%2FGCS?ref=badge_large) [Northrop Grumman Collaboration Project]: http://www.ngcpcalpoly.com/about.html [Node.js]: https://github.com/nodejs/node From 40200f4a8c2c2d278f7dde09a2cc0c60b14b670b Mon Sep 17 00:00:00 2001 From: Paul Matthew Barrameda Date: Wed, 3 Apr 2019 17:50:40 -0700 Subject: [PATCH 040/152] Updating parser version allows my system to see the errors now Will fix the issues in a little bit. This commit should fail the build test. --- package.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index a9872325..bc492fee 100644 --- a/package.json +++ b/package.json @@ -32,12 +32,12 @@ "devDependencies": { "@babel/preset-react": "^7.0.0", "@types/pouchdb": "^6.3.3", - "@types/react": "^16.8.10", + "@types/react": "^16.8.12", "@types/react-dom": "^16.8.3", "@types/react-leaflet": "^2.2.1", - "@types/react-virtualized": "^9.21.0", - "@typescript-eslint/eslint-plugin": "^1.5.0", - "@typescript-eslint/parser": "^1.5.0", + "@types/react-virtualized": "^9.21.1", + "@typescript-eslint/eslint-plugin": "^1.6.0", + "@typescript-eslint/parser": "^1.6.0", "babel-eslint": "^10.0.1", "dotenv-webpack": "^1.7.0", "electron": "^3.1.8", From 1eb7b8f2303fdd50653821019aac17bc6f11f360 Mon Sep 17 00:00:00 2001 From: Paul Matthew Barrameda Date: Wed, 3 Apr 2019 18:52:34 -0700 Subject: [PATCH 041/152] Fixed all eslint errors - Adding return types to all functions (seems to finally show up to my system after upgrading to typescript-eslint v1.6 - Added functions to DictionaryList so that tsc does not throw an error - Fixed some comments --- src/main/index.ts | 30 +++++++------- src/renderer/fixtures/completeJobFixture.ts | 4 +- src/renderer/fixtures/index.ts | 2 +- .../fixtures/updateVehiclesFixtures.ts | 2 +- src/renderer/mainWindow/log/LogContainer.tsx | 8 ++-- src/renderer/mainWindow/log/log.css | 2 +- src/renderer/mainWindow/map/MapContainer.tsx | 12 +++--- .../mainWindow/mission/MissionContainer.tsx | 12 +++--- .../mainWindow/mission/MissionTable.tsx | 4 +- src/renderer/mainWindow/mission/mission.css | 5 ++- .../mainWindow/vehicle/VehicleContainer.tsx | 2 +- .../mainWindow/vehicle/VehicleTable.tsx | 4 +- src/renderer/mainWindow/vehicle/vehicle.css | 2 +- src/struct/DictionaryList.ts | 39 ++++++++++++++----- src/util/util.ts | 6 +-- 15 files changed, 77 insertions(+), 57 deletions(-) diff --git a/src/main/index.ts b/src/main/index.ts index 4c211416..b4d7e9a2 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -180,7 +180,7 @@ const menu: MenuItemConstructorOptions[] = [ { label: 'Open File...', accelerator: 'CommandOrControl+O', - click() { loadConfig(); }, + click(): void { loadConfig(); }, }, { type: 'separator' }, { role: 'close' }, @@ -188,7 +188,7 @@ const menu: MenuItemConstructorOptions[] = [ { label: 'Save As...', accelerator: 'CommandOrControl+S', - click() { saveConfig(); }, + click(): void { saveConfig(); }, }, ], }, @@ -220,7 +220,7 @@ const menu: MenuItemConstructorOptions[] = [ submenu: [ { label: 'My Location', - click() { + click(): void { if (mainWindow) { mainWindow.webContents.send('setMapToUserLocation'); } @@ -240,7 +240,7 @@ const menu: MenuItemConstructorOptions[] = [ submenu: [ { label: 'Help', - click() { shell.openExternal('https://github.com/NGCP/missioncontrol'); }, + click(): void { shell.openExternal('https://github.com/NGCP/missioncontrol'); }, }, ], }, @@ -251,7 +251,7 @@ const menu: MenuItemConstructorOptions[] = [ * The list of locations comes from ../../resources/locations.json. */ function setLocationMenu(): void { - const location = menu.find(m => m.label === 'Locations'); + const location = menu.find((m): boolean => m.label === 'Locations'); if (!location) return; const { submenu } = location; @@ -268,10 +268,10 @@ function setLocationMenu(): void { return; } - Object.keys(locations).forEach((label) => { + Object.keys(locations).forEach((label): void => { locationMenu.push({ label, - click(menuItem) { + click(menuItem): void { if (mainWindow) { mainWindow.webContents.send('updateMapLocation', locations[menuItem.label]); } @@ -312,11 +312,11 @@ function createMainWindow(): void { mainWindow.loadURL(`file:///${__dirname}/index.html#main`); } - mainWindow.on('ready-to-show', () => { + mainWindow.on('ready-to-show', (): void => { if (mainWindow) mainWindow.show(); }); - mainWindow.on('close', (event) => { + mainWindow.on('close', (event): void => { if (!quitting) { event.preventDefault(); if (mainWindow) { @@ -362,7 +362,7 @@ function createMissionWindow(): void { } - missionWindow.on('close', (event) => { + missionWindow.on('close', (event): void => { if (!quitting) { event.preventDefault(); // This allows the mission container to update to closed mission window. @@ -401,7 +401,7 @@ function showMissionWindow(): void { const trayMenu: MenuItemConstructorOptions[] = [ { label: 'NGCP Ground Control Station', - click() { showMainWindow(); }, + click(): void { showMainWindow(); }, }, { type: 'separator' }, quitRole, @@ -430,12 +430,12 @@ function createTray(): void { tray.setContextMenu(Menu.buildFromTemplate(trayMenu)); - tray.on('click', () => { showMainWindow(); }); + tray.on('click', (): void => { showMainWindow(); }); } app.on('activate', showMainWindow); -app.on('ready', () => { +app.on('ready', (): void => { /* * Prevents the app from starting if MapBox token is not set up. * This is necessary to run the map container. @@ -451,11 +451,11 @@ app.on('ready', () => { createTray(); }); -app.on('before-quit', () => { +app.on('before-quit', (): void => { quitting = true; }); -ipcMain.on('post', (_: Event, notification: string, data: object) => { +ipcMain.on('post', (_: Event, notification: string, data: object): void => { if (notification === 'showMissionWindow') { showMissionWindow(); } else if (notification === 'hideMissionWindow') { diff --git a/src/renderer/fixtures/completeJobFixture.ts b/src/renderer/fixtures/completeJobFixture.ts index 0dbccd3c..77e7918d 100644 --- a/src/renderer/fixtures/completeJobFixture.ts +++ b/src/renderer/fixtures/completeJobFixture.ts @@ -5,8 +5,8 @@ import { ipcRenderer } from 'electron'; // eslint-disable-line import/no-extrane */ export default function completeJob(): void { // Sends completeJob notification after receiving startJob notification. - ipcRenderer.on('startJob', () => { - setTimeout(() => { + ipcRenderer.on('startJob', (): void => { + setTimeout((): void => { ipcRenderer.send('post', 'completeJob'); }, 5000); }); diff --git a/src/renderer/fixtures/index.ts b/src/renderer/fixtures/index.ts index 9faa186b..4a92e8df 100644 --- a/src/renderer/fixtures/index.ts +++ b/src/renderer/fixtures/index.ts @@ -12,7 +12,7 @@ import updateVehicles from './updateVehiclesFixtures'; completeJob(); -setInterval(() => { +setInterval((): void => { updateMessages(); updateVehicles(); }, 1000); diff --git a/src/renderer/fixtures/updateVehiclesFixtures.ts b/src/renderer/fixtures/updateVehiclesFixtures.ts index ed04ec31..f6bb4007 100644 --- a/src/renderer/fixtures/updateVehiclesFixtures.ts +++ b/src/renderer/fixtures/updateVehiclesFixtures.ts @@ -32,7 +32,7 @@ const status = Object.keys(vehicleStatuses); * Sends an updateVehicle notification with random vehicle fixtures. */ export default function updateVehicles(): void { - const newFixtures = fixtures.map(fixture => ({ + const newFixtures = fixtures.map((fixture): Vehicle => ({ ...fixture, lat: fixture.lat + (Math.random() / 5000) - 0.0001, lng: fixture.lng + (Math.random() / 5000) - 0.0001, diff --git a/src/renderer/mainWindow/log/LogContainer.tsx b/src/renderer/mainWindow/log/LogContainer.tsx index ba5d312f..9ab11022 100644 --- a/src/renderer/mainWindow/log/LogContainer.tsx +++ b/src/renderer/mainWindow/log/LogContainer.tsx @@ -84,7 +84,7 @@ export default class LogContainer extends Component { } public componentDidMount(): void { - ipcRenderer.on('updateMessages', (_: Event, messages: Message[]) => this.updateMessages(messages)); + ipcRenderer.on('updateMessages', (_: Event, messages: Message[]): void => this.updateMessages(messages)); } /** @@ -145,7 +145,7 @@ export default class LogContainer extends Component { this.setState({ filter: newFilter as MessageType, - filteredMessages: newFilter === '' ? messages.slice(0) : messages.filter(message => message.type === newFilter), + filteredMessages: newFilter === '' ? messages.slice(0) : messages.filter((message): boolean => message.type === newFilter), }); } @@ -157,7 +157,7 @@ export default class LogContainer extends Component { const currentMessages = thisMessages; const currentFilteredMessages = filteredMessages; - messages.forEach((message) => { + messages.forEach((message): void => { const msg: Message = { type: message.type || '', ...message, @@ -184,7 +184,7 @@ export default class LogContainer extends Component {
- {({ height, width }) => ( + {({ height, width }): ReactNode => ( { } public componentDidMount(): void { - ipcRenderer.on('loadConfig', (_: Event, data: FileLoadOptions) => this.loadConfig(data)); - ipcRenderer.on('saveConfig', (_: Event, data: FileSaveOptions) => this.saveConfig(data)); + ipcRenderer.on('loadConfig', (_: Event, data: FileLoadOptions): void => this.loadConfig(data)); + ipcRenderer.on('saveConfig', (_: Event, data: FileSaveOptions): void => this.saveConfig(data)); - ipcRenderer.on('centerMapToVehicle', (_: Event, vehicle: VehicleUI) => this.centerMapToVehicle(vehicle)); + ipcRenderer.on('centerMapToVehicle', (_: Event, vehicle: VehicleUI): void => this.centerMapToVehicle(vehicle)); ipcRenderer.on('setMapToUserLocation', this.setMapToUserLocation); - ipcRenderer.on('updateMapLocation', (_: Event, location: LatLngZoom) => this.updateMapLocation(location)); - ipcRenderer.on('updateVehicles', (_: Event, vehicles: Vehicle[]) => updateVehicles(this, vehicles)); + ipcRenderer.on('updateMapLocation', (_: Event, location: LatLngZoom): void => this.updateMapLocation(location)); + ipcRenderer.on('updateVehicles', (_: Event, vehicles: Vehicle[]): void => updateVehicles(this, vehicles)); } /** @@ -185,7 +185,7 @@ export default class MapContainer extends Component { const { theme } = this.props; const { viewport, vehicles } = this.state; - const markers = Object.keys(vehicles).map(sid => ( + const markers = Object.keys(vehicles).map((sid): ReactNode => ( { } public componentDidMount(): void { - ipcRenderer.on('setSelectedMission', (_: Event, index: number) => this.setSelectedMission(index)); + ipcRenderer.on('setSelectedMission', (_: Event, index: number): void => this.setSelectedMission(index)); - ipcRenderer.on('startMission', () => this.updateMission('started')); - ipcRenderer.on('stopMission', () => this.updateMission('notStarted')); - ipcRenderer.on('completeMission', () => this.updateMission('completed')); + ipcRenderer.on('startMission', (): void => this.updateMission('started')); + ipcRenderer.on('stopMission', (): void => this.updateMission('notStarted')); + ipcRenderer.on('completeMission', (): void => this.updateMission('completed')); - ipcRenderer.on('pauseJob', () => this.updateMission('paused')); - ipcRenderer.on('resumeJob', () => this.updateMission('started')); + ipcRenderer.on('pauseJob', (): void => this.updateMission('paused')); + ipcRenderer.on('resumeJob', (): void => this.updateMission('started')); } /** diff --git a/src/renderer/mainWindow/mission/MissionTable.tsx b/src/renderer/mainWindow/mission/MissionTable.tsx index 12fc670b..42c57c2d 100644 --- a/src/renderer/mainWindow/mission/MissionTable.tsx +++ b/src/renderer/mainWindow/mission/MissionTable.tsx @@ -43,7 +43,7 @@ export default class MissionTable extends Component { const { dataKey, rowData } = props; const { type, message } = rowData.status; - return {message}; + return {message}; } /** @@ -132,7 +132,7 @@ export default class MissionTable extends Component { return ( - {({ height, width: tableWidth }) => ( + {({ height, width: tableWidth }): ReactNode => ( { } public componentDidMount(): void { - ipcRenderer.on('updateVehicles', (_: Event, data: Vehicle[]) => updateVehicles(this, data)); + ipcRenderer.on('updateVehicles', (_: Event, data: Vehicle[]): void => updateVehicles(this, data)); } public render(): ReactNode { diff --git a/src/renderer/mainWindow/vehicle/VehicleTable.tsx b/src/renderer/mainWindow/vehicle/VehicleTable.tsx index d8a63926..a44b28e0 100644 --- a/src/renderer/mainWindow/vehicle/VehicleTable.tsx +++ b/src/renderer/mainWindow/vehicle/VehicleTable.tsx @@ -77,7 +77,7 @@ export default class VehicleTable extends Component { private rowGetter({ index }: Index): VehicleUI { const { vehicles } = this.props; - const v = Object.keys(vehicles).sort((a, b) => parseInt(a, 10) - parseInt(b, 10)); + const v = Object.keys(vehicles).sort((a, b): number => parseInt(a, 10) - parseInt(b, 10)); return vehicles[v[index]]; } @@ -100,7 +100,7 @@ export default class VehicleTable extends Component { return ( - {({ height, width: tableWidth }) => ( + {({ height, width: tableWidth }): ReactNode => (
{ /** * Object of string keys to generic type arrays for the class to use. */ - private dictionary: { [key: string]: List } = {}; + private dictionary: { [key: string]: List | undefined } = {}; /** * Total number of items between all lists in the dictionary. @@ -52,21 +52,40 @@ export default class DictionaryList { private numItems = 0; /** - * Custom callback function to obtain values for list. + * Removes the oldest element from dictionary's list with the given key + * and returns it. Will return undefined if there is no list at the given key + * or if the list at that key is empty. + * + * @param key The list in the dictionary's key. */ - private predicate: Predicate | undefined = undefined; + public shift(key: string): T | undefined { + const list = this.dictionary[key]; + if (!list) return undefined; + + return list.shift(); + } - public constructor(options?: DictionaryListOptions) { - if (!options) return; + /** + * Removes all elements from oldest to the element (inclusive) where predicate is true. + * If no element makes predicate true, then no elements are removed and undefined is returned. + * + * @param key The list in the dictionary's key. + * @param predicate Calls predicate once for each element of the array, in ascending + * order, until it finds one where predicate returns true. If such an element is found, + * array will remove all elements from oldest up to that element. Otherwise, will not + * remove any elements and returns undefined. + */ + public shiftToElement(key: string, predicate: Predicate): T[] | undefined { + const list = this.dictionary[key]; + if (!list) return undefined; - this.predicate = options.predicate; + return list.shiftToElement(predicate); } /** - * Removes the oldest element from dictionary's list with the given key - * and returns it. + * Gets the total number of items in the lists in the dictionary. */ - public dequeue(key: string): T | undefined { - return this.dictionary[key].shift(); + public size(): number { + return this.numItems; } } diff --git a/src/util/util.ts b/src/util/util.ts index 6d4060ed..1b3e2492 100644 --- a/src/util/util.ts +++ b/src/util/util.ts @@ -26,7 +26,7 @@ export function updateVehicles( const { vehicles: thisVehicles } = component.state; const currentVehicles = thisVehicles; - vehicles.forEach((vehicle) => { + vehicles.forEach((vehicle): void => { currentVehicles[vehicle.sid] = { ...vehicle, ...vehicleInfos[vehicle.sid], @@ -96,11 +96,11 @@ export const job = { * Converts a float number to a hex string. */ function toHexString(float: number): string { - const getHex = (i: number): string => `00${i.toString(16)}`.slice(-2); + const getHex = (index: number): string => `00${index.toString(16)}`.slice(-2); const view = new DataView(new ArrayBuffer(4)); view.setFloat32(0, float); - return [0, 0, 0, 0].map((_, i) => getHex(view.getUint8(i))).join(''); + return [0, 0, 0, 0].map((_, index): string => getHex(view.getUint8(index))).join(''); } /** From 290a3becbafe366acbc26770464413025ca1c785 Mon Sep 17 00:00:00 2001 From: Paul Matthew Barrameda Date: Wed, 3 Apr 2019 19:26:47 -0700 Subject: [PATCH 042/152] Version bump --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index bc492fee..870ec577 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "GCS", - "version": "0.6.1", + "version": "0.6.2", "license": "MIT", "author": "Northrop Grumman Collaboration Project", "description": "Ground Control Station for autonomous vehicle platforms in NGCP", From 74019489bf4268d5feee208b33a4499d96d2d174 Mon Sep 17 00:00:00 2001 From: Paul Matthew Barrameda Date: Wed, 3 Apr 2019 21:25:54 -0700 Subject: [PATCH 043/152] Moved folders to make more sense --- src/{ => common}/struct/DictionaryList.ts | 0 src/main/index.ts | 2 +- src/renderer/fixtures/updateVehiclesFixtures.ts | 2 +- src/renderer/index.js | 2 +- src/renderer/mainWindow/map/MapContainer.tsx | 2 +- src/renderer/mainWindow/map/VehicleMarker.tsx | 2 +- src/renderer/mainWindow/map/map.css | 12 ++++++------ src/{config => static}/config.json | 0 src/{config => static}/images/arrow.png | Bin src/{config => static}/images/icon.png | Bin src/{config => static}/images/logo/ngcp_calpoly.png | Bin src/{config => static}/images/logo/ngcp_pomona.png | Bin src/{config => static}/images/markers/poi_fp.png | Bin src/{config => static}/images/markers/poi_unkwn.png | Bin src/{config => static}/images/markers/poi_vld.png | Bin .../images/markers/vehicles/plane.png | Bin .../images/markers/vehicles/plane_red.png | Bin .../images/markers/vehicles/rover.png | Bin .../images/markers/vehicles/rover_red.png | Bin src/{config => static}/images/other/fullmoon.png | Bin .../images/other/fullmoon_hover.png | Bin src/{config => static}/images/other/geolocation.png | Bin .../images/other/geolocation_hover.png | Bin src/{config => static}/images/other/moon.png | Bin src/{config => static}/images/other/moon_hover.png | Bin src/{config => static}/images/pin.png | Bin src/{config => static}/index.ts | 0 src/{config => static}/locations.json | 0 src/{config => static}/vehicle.json | 0 src/util/util.ts | 2 +- 30 files changed, 12 insertions(+), 12 deletions(-) rename src/{ => common}/struct/DictionaryList.ts (100%) rename src/{config => static}/config.json (100%) rename src/{config => static}/images/arrow.png (100%) rename src/{config => static}/images/icon.png (100%) rename src/{config => static}/images/logo/ngcp_calpoly.png (100%) rename src/{config => static}/images/logo/ngcp_pomona.png (100%) rename src/{config => static}/images/markers/poi_fp.png (100%) rename src/{config => static}/images/markers/poi_unkwn.png (100%) rename src/{config => static}/images/markers/poi_vld.png (100%) rename src/{config => static}/images/markers/vehicles/plane.png (100%) rename src/{config => static}/images/markers/vehicles/plane_red.png (100%) rename src/{config => static}/images/markers/vehicles/rover.png (100%) rename src/{config => static}/images/markers/vehicles/rover_red.png (100%) rename src/{config => static}/images/other/fullmoon.png (100%) rename src/{config => static}/images/other/fullmoon_hover.png (100%) rename src/{config => static}/images/other/geolocation.png (100%) rename src/{config => static}/images/other/geolocation_hover.png (100%) rename src/{config => static}/images/other/moon.png (100%) rename src/{config => static}/images/other/moon_hover.png (100%) rename src/{config => static}/images/pin.png (100%) rename src/{config => static}/index.ts (100%) rename src/{config => static}/locations.json (100%) rename src/{config => static}/vehicle.json (100%) diff --git a/src/struct/DictionaryList.ts b/src/common/struct/DictionaryList.ts similarity index 100% rename from src/struct/DictionaryList.ts rename to src/common/struct/DictionaryList.ts diff --git a/src/main/index.ts b/src/main/index.ts index b4d7e9a2..7fef6724 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -13,7 +13,7 @@ import { // eslint-disable-line import/no-extraneous-dependencies import fs from 'fs'; import moment from 'moment'; -import { images, locations as locationsConfig } from '../config/index'; +import { images, locations as locationsConfig } from '../static/index'; import { LocationSignature } from '../util/types'; diff --git a/src/renderer/fixtures/updateVehiclesFixtures.ts b/src/renderer/fixtures/updateVehiclesFixtures.ts index f6bb4007..d83f035d 100644 --- a/src/renderer/fixtures/updateVehiclesFixtures.ts +++ b/src/renderer/fixtures/updateVehiclesFixtures.ts @@ -1,6 +1,6 @@ import { ipcRenderer } from 'electron'; // eslint-disable-line import/no-extraneous-dependencies -import { vehicleStatuses } from '../../config/index'; +import { vehicleStatuses } from '../../static/index'; // TODO: Remove disable line comment when issue gets fixed (https://github.com/benmosher/eslint-plugin-import/pull/1304) import { Vehicle, VehicleStatus } from '../../util/types'; // eslint-disable-line import/named diff --git a/src/renderer/index.js b/src/renderer/index.js index 61ecc147..9184dc79 100644 --- a/src/renderer/index.js +++ b/src/renderer/index.js @@ -12,7 +12,7 @@ import 'react-virtualized/styles.css'; import './index.css'; -import { fixtures, geolocation } from '../config/index'; +import { fixtures, geolocation } from '../static/index'; const isDevelopment = process.env.NODE_ENV !== 'production'; diff --git a/src/renderer/mainWindow/map/MapContainer.tsx b/src/renderer/mainWindow/map/MapContainer.tsx index c62cc9a6..2a22dc9b 100644 --- a/src/renderer/mainWindow/map/MapContainer.tsx +++ b/src/renderer/mainWindow/map/MapContainer.tsx @@ -12,7 +12,7 @@ import { Map, TileLayer, Viewport } from 'react-leaflet'; import { locations as locationsConfig, startLocation, -} from '../../../config/index'; +} from '../../../static/index'; import { FileLoadOptions, diff --git a/src/renderer/mainWindow/map/VehicleMarker.tsx b/src/renderer/mainWindow/map/VehicleMarker.tsx index 48665ef2..1161ff25 100644 --- a/src/renderer/mainWindow/map/VehicleMarker.tsx +++ b/src/renderer/mainWindow/map/VehicleMarker.tsx @@ -2,7 +2,7 @@ import L from 'leaflet'; import React, { PureComponent, ReactNode } from 'react'; import { Marker, Tooltip } from 'react-leaflet'; -import { images } from '../../../config/index'; +import { images } from '../../../static/index'; // TODO: Remove disable line comment when issue gets fixed (https://github.com/benmosher/eslint-plugin-import/pull/1304) import { VehicleUI } from '../../../util/types'; // eslint-disable-line import/named diff --git a/src/renderer/mainWindow/map/map.css b/src/renderer/mainWindow/map/map.css index 80c44a71..885ffefb 100644 --- a/src/renderer/mainWindow/map/map.css +++ b/src/renderer/mainWindow/map/map.css @@ -10,11 +10,11 @@ } .geolocation-control { - background-image: url('../../../config/images/other/geolocation.png'); + background-image: url('../../../static/images/other/geolocation.png'); } .geolocation-control:hover { - background-image: url('../../../config/images/other/geolocation_hover.png'); + background-image: url('../../../static/images/other/geolocation_hover.png'); } /* Light theme */ @@ -22,11 +22,11 @@ .theme-control { cursor: pointer; background-size: 75%; - background-image: url('../../../config/images/other/moon.png'); + background-image: url('../../../static/images/other/moon.png'); } .theme-control:hover { - background-image: url('../../../config/images/other/moon_hover.png'); + background-image: url('../../../static/images/other/moon_hover.png'); } /* Dark theme */ @@ -34,9 +34,9 @@ .theme-control_dark { cursor: pointer; background-size: 75%; - background-image: url('../../../config/images/other/fullmoon.png'); + background-image: url('../../../static/images/other/fullmoon.png'); } .theme-control_dark:hover { - background-image: url('../../../config/images/other/fullmoon_hover.png'); + background-image: url('../../../static/images/other/fullmoon_hover.png'); } diff --git a/src/config/config.json b/src/static/config.json similarity index 100% rename from src/config/config.json rename to src/static/config.json diff --git a/src/config/images/arrow.png b/src/static/images/arrow.png similarity index 100% rename from src/config/images/arrow.png rename to src/static/images/arrow.png diff --git a/src/config/images/icon.png b/src/static/images/icon.png similarity index 100% rename from src/config/images/icon.png rename to src/static/images/icon.png diff --git a/src/config/images/logo/ngcp_calpoly.png b/src/static/images/logo/ngcp_calpoly.png similarity index 100% rename from src/config/images/logo/ngcp_calpoly.png rename to src/static/images/logo/ngcp_calpoly.png diff --git a/src/config/images/logo/ngcp_pomona.png b/src/static/images/logo/ngcp_pomona.png similarity index 100% rename from src/config/images/logo/ngcp_pomona.png rename to src/static/images/logo/ngcp_pomona.png diff --git a/src/config/images/markers/poi_fp.png b/src/static/images/markers/poi_fp.png similarity index 100% rename from src/config/images/markers/poi_fp.png rename to src/static/images/markers/poi_fp.png diff --git a/src/config/images/markers/poi_unkwn.png b/src/static/images/markers/poi_unkwn.png similarity index 100% rename from src/config/images/markers/poi_unkwn.png rename to src/static/images/markers/poi_unkwn.png diff --git a/src/config/images/markers/poi_vld.png b/src/static/images/markers/poi_vld.png similarity index 100% rename from src/config/images/markers/poi_vld.png rename to src/static/images/markers/poi_vld.png diff --git a/src/config/images/markers/vehicles/plane.png b/src/static/images/markers/vehicles/plane.png similarity index 100% rename from src/config/images/markers/vehicles/plane.png rename to src/static/images/markers/vehicles/plane.png diff --git a/src/config/images/markers/vehicles/plane_red.png b/src/static/images/markers/vehicles/plane_red.png similarity index 100% rename from src/config/images/markers/vehicles/plane_red.png rename to src/static/images/markers/vehicles/plane_red.png diff --git a/src/config/images/markers/vehicles/rover.png b/src/static/images/markers/vehicles/rover.png similarity index 100% rename from src/config/images/markers/vehicles/rover.png rename to src/static/images/markers/vehicles/rover.png diff --git a/src/config/images/markers/vehicles/rover_red.png b/src/static/images/markers/vehicles/rover_red.png similarity index 100% rename from src/config/images/markers/vehicles/rover_red.png rename to src/static/images/markers/vehicles/rover_red.png diff --git a/src/config/images/other/fullmoon.png b/src/static/images/other/fullmoon.png similarity index 100% rename from src/config/images/other/fullmoon.png rename to src/static/images/other/fullmoon.png diff --git a/src/config/images/other/fullmoon_hover.png b/src/static/images/other/fullmoon_hover.png similarity index 100% rename from src/config/images/other/fullmoon_hover.png rename to src/static/images/other/fullmoon_hover.png diff --git a/src/config/images/other/geolocation.png b/src/static/images/other/geolocation.png similarity index 100% rename from src/config/images/other/geolocation.png rename to src/static/images/other/geolocation.png diff --git a/src/config/images/other/geolocation_hover.png b/src/static/images/other/geolocation_hover.png similarity index 100% rename from src/config/images/other/geolocation_hover.png rename to src/static/images/other/geolocation_hover.png diff --git a/src/config/images/other/moon.png b/src/static/images/other/moon.png similarity index 100% rename from src/config/images/other/moon.png rename to src/static/images/other/moon.png diff --git a/src/config/images/other/moon_hover.png b/src/static/images/other/moon_hover.png similarity index 100% rename from src/config/images/other/moon_hover.png rename to src/static/images/other/moon_hover.png diff --git a/src/config/images/pin.png b/src/static/images/pin.png similarity index 100% rename from src/config/images/pin.png rename to src/static/images/pin.png diff --git a/src/config/index.ts b/src/static/index.ts similarity index 100% rename from src/config/index.ts rename to src/static/index.ts diff --git a/src/config/locations.json b/src/static/locations.json similarity index 100% rename from src/config/locations.json rename to src/static/locations.json diff --git a/src/config/vehicle.json b/src/static/vehicle.json similarity index 100% rename from src/config/vehicle.json rename to src/static/vehicle.json diff --git a/src/util/util.ts b/src/util/util.ts index 1b3e2492..5dcb1548 100644 --- a/src/util/util.ts +++ b/src/util/util.ts @@ -3,7 +3,7 @@ import { Component } from 'react'; import { vehicleInfos as vehicleInfosConfig, vehicleStatuses as vehicleStatusesConfig, -} from '../config/index'; +} from '../static/index'; // TODO: Remove disable line comment when issue gets fixed (https://github.com/benmosher/eslint-plugin-import/pull/1304) import { From cba58f6bee4bb7648a75a5e67824898ad0af778c Mon Sep 17 00:00:00 2001 From: Paul Matthew Barrameda Date: Wed, 3 Apr 2019 23:08:58 -0700 Subject: [PATCH 044/152] Implemented DictionaryList Needs unit tests --- src/common/struct/DictionaryList.ts | 128 +++++++++++++++++----------- 1 file changed, 79 insertions(+), 49 deletions(-) diff --git a/src/common/struct/DictionaryList.ts b/src/common/struct/DictionaryList.ts index 6f473daf..a790f60a 100644 --- a/src/common/struct/DictionaryList.ts +++ b/src/common/struct/DictionaryList.ts @@ -1,85 +1,115 @@ -/** - * "Predicate" name and description is taken from official TypeScript definitions. - */ -type Predicate = (value: T, index: number, obj: T[]) => boolean; - -/** - * Modified array class with a modified shift function. - * - * Note: shift is JavaScript's convention of naming the Queue's dequeue function. - */ -export class List extends Array { - /** - * Modified version of Array's shift function. Removes all elements from oldest to the element - * (inclusive) where predicate is true. If no element makes predicate true, then - * no elements are removed and undefined is returned. - * - * @param predicate Calls predicate once for each element of the array, in ascending - * order, until it finds one where predicate returns true. If such an element is found, - * array will remove all elements from oldest up to that element. Otherwise, will not - * remove any elements and returns undefined. - */ - public shiftToElement(predicate: Predicate): T[] | undefined { - if (this.length <= 0) { - return undefined; - } - - const elementIndex = this.findIndex(predicate); - return elementIndex !== -1 ? this.splice(elementIndex) : undefined; - } -} - -/** - * Options for the DictionaryList class. - */ -export interface DictionaryListOptions { - predicate: Predicate; -} +type Callback = (value: T, index: number, array: T[]) => boolean; /** * Structure that can store lists in a dictionary. Lists can be obtained with a key string, * and can be modified with custom functions. + * + * The functions in this structure can be called from the list with a few lines of code. However + * it is possible that the value of a certain key can be undefined, which this class will do + * checks for. */ export default class DictionaryList { /** * Object of string keys to generic type arrays for the class to use. */ - private dictionary: { [key: string]: List | undefined } = {}; + private dictionary: { [key: string]: T[] | undefined } = {}; /** * Total number of items between all lists in the dictionary. */ private numItems = 0; + /** + * Appends new elements to an array, and returns the new size of the dictionary. + * + * @param key The key to access the list in the dictionary. + */ + public push(key: string, ...values: T[]): number { + if (!this.dictionary[key]) { + this.dictionary[key] = []; + } + + // Cast to T[] since this value cannot be undefined. + const list = this.dictionary[key] as T[]; + + this.numItems += values.length; + list.push(...values); + + return this.numItems; + } + + /** + * Gets the oldest element from dictionary's list with the given key + * where the callback is true. + * + * @param key The key to access the list in the dictionary. + * @param callback Test each element, from oldest to newest. First element to pass + * will be returned. + */ + public get(key: string, callback: Callback): T | undefined { + const list = this.dictionary[key]; + if (!list) return undefined; + + return list.find(callback); + } + /** * Removes the oldest element from dictionary's list with the given key * and returns it. Will return undefined if there is no list at the given key * or if the list at that key is empty. * - * @param key The list in the dictionary's key. + * @param key The key to access the list in the dictionary. */ public shift(key: string): T | undefined { const list = this.dictionary[key]; - if (!list) return undefined; + if (!list || list.length === 0) return undefined; + this.numItems -= 1; return list.shift(); } /** - * Removes all elements from oldest to the element (inclusive) where predicate is true. - * If no element makes predicate true, then no elements are removed and undefined is returned. + * Removes the oldest element from dictionary's list with the given key + * where the callback is true. * - * @param key The list in the dictionary's key. - * @param predicate Calls predicate once for each element of the array, in ascending - * order, until it finds one where predicate returns true. If such an element is found, - * array will remove all elements from oldest up to that element. Otherwise, will not - * remove any elements and returns undefined. + * @param key The key to access the list in the dictionary. + * @param callback Test each element, from oldest to newest. First element to pass + * will be removed and returned. */ - public shiftToElement(key: string, predicate: Predicate): T[] | undefined { + public remove(key: string, callback: Callback): T | undefined { const list = this.dictionary[key]; if (!list) return undefined; - return list.shiftToElement(predicate); + const index = list.findIndex(callback); + if (index === -1) return undefined; + + // Removes the element at that index and returns it. + this.numItems -= 1; + return list.splice(index, 1)[0]; + } + + /** + * Returns the elements of an array that meet the condition specified in a callback function. + * @param callback Test each element, from oldest to newest. All elements to pass + * will be returned in an array. + */ + public filter(key: string, callback: Callback): T[] | undefined { + const list = this.dictionary[key]; + if (!list) return undefined; + + return list.filter(callback); + } + + /** + * Gets all keys of the object whose list value has an element or more. + */ + public keys(): string[] { + return Object.keys(this.dictionary).filter((key): boolean => { + const list = this.dictionary[key]; + if (!list) return false; + + return list.length > 0; + }); } /** From e3a4e06c8ac2be9eea1bdb8c6cc367bd8225b35a Mon Sep 17 00:00:00 2001 From: Paul Matthew Barrameda Date: Thu, 4 Apr 2019 01:12:34 -0700 Subject: [PATCH 045/152] Added UpdateHandler Needs unit tests --- src/common/struct/DictionaryList.ts | 49 +++++++++++- src/common/struct/UpdateHandler.ts | 120 ++++++++++++++++++++++++++++ 2 files changed, 168 insertions(+), 1 deletion(-) create mode 100644 src/common/struct/UpdateHandler.ts diff --git a/src/common/struct/DictionaryList.ts b/src/common/struct/DictionaryList.ts index a790f60a..4bc89e46 100644 --- a/src/common/struct/DictionaryList.ts +++ b/src/common/struct/DictionaryList.ts @@ -38,6 +38,25 @@ export default class DictionaryList { return this.numItems; } + /** + * Gets the list stored at the dictionary with the given key. + * + * @param key The key to access the list in the dictionary. + */ + public get(key: string): T[] | undefined { + return this.dictionary[key]; + } + + /** + * Sets the list stored at the dictionary with the given key to the provided list. + * + * @param key The key to access the list in the dictionary. + * @param list The list to change the dictionary's list to. + */ + public set(key: string, list: T[]): void { + this.dictionary[key] = list; + } + /** * Gets the oldest element from dictionary's list with the given key * where the callback is true. @@ -46,7 +65,7 @@ export default class DictionaryList { * @param callback Test each element, from oldest to newest. First element to pass * will be returned. */ - public get(key: string, callback: Callback): T | undefined { + public find(key: string, callback: Callback): T | undefined { const list = this.dictionary[key]; if (!list) return undefined; @@ -88,8 +107,36 @@ export default class DictionaryList { return list.splice(index, 1)[0]; } + /** + * Removes all elements of an array that meets the condition specified in a callback function. + * + * @param key The key to access the list in the dictionary. + * @param callback Test each element, from oldest to newest. All elements to pass + * will be removed and returned in an array. + */ + public removeAll(key: string, callback: Callback): T[] | undefined { + const list = this.dictionary[key]; + if (!list) return undefined; + + const removed: T[] = []; + const kept: T[] = []; + for (let i = 0; i < list.length; i += 1) { + if (callback(list[i], i, list)) { + removed.push(list[i]); + } else { + kept.push(list[i]); + } + } + + this.numItems -= removed.length; + this.dictionary[key] = kept; + return removed; + } + /** * Returns the elements of an array that meet the condition specified in a callback function. + * + * @param key The key to access the list in the dictionary. * @param callback Test each element, from oldest to newest. All elements to pass * will be returned in an array. */ diff --git a/src/common/struct/UpdateHandler.ts b/src/common/struct/UpdateHandler.ts new file mode 100644 index 00000000..0f60ff8c --- /dev/null +++ b/src/common/struct/UpdateHandler.ts @@ -0,0 +1,120 @@ +import DictionaryList from './DictionaryList'; + +type Handler = (value: T, allEvents?: { [key: string]: T }) => boolean; + +/** + * Interface to handle expiry for an event when it has a timeout. + */ +interface EventTimeout { + /** + * Callback that is executed when timeout occurs. + */ + callback(): void; + + /** + * Time is in milliseconds + */ + time: number; +} + +interface Event { + /** + * Handler function for the event. + */ + handler: Handler; + + /** + * Expiry for a timeout, allows us to call clearTimeout on it. + */ + expiry?: NodeJS.Timeout; +} + + +export default class UpdateHandler { + private eventDictionary = new DictionaryList>(); + + /** + * Adds a new event to the handlers. + * + * @param identifier The name of the event handler string. + * @param handler The function to handle the event. + * @param timeout The timeout function and time for the event to expire. + */ + public addHandler( + identifier: string, + handler: Handler, + timeout?: EventTimeout, + ): Event { + // Create an event with the handler function. + const event: Event = { handler }; + + /* + * If a timeout is provided, the event will remove itself and run its callback function + * when the timeout runs out. If we need to remove the event manually and it has a timeout, + * we will need to call the clearTimeout function on the event's expiry. + */ + if (timeout) { + event.expiry = setTimeout((): void => { + this.eventDictionary.remove(identifier, (value): boolean => value !== event); + timeout.callback(); + }, timeout.time); + } + + // Adds the event to the list with the proper event identifier. + this.eventDictionary.push(identifier, event); + + return event; + } + + /** + * Processes the event based on the handlers stored. + * + * @param identifier The name of the event handler string. + * @param value The value being updated. + * @param allEvents Key/Value pair of all events. + */ + public event(identifier: string, value: T, allEvents?: { [key: string]: T }): void { + const newEvents = this.eventDictionary.filter(identifier, (v): boolean => { + const handled = v.handler(value, allEvents); + if (handled) { + // Remove handler. + if (v.expiry) { + clearTimeout(v.expiry); + } + + return false; + } + + // Keep handler. + return true; + }); + + if (newEvents) { + this.eventDictionary.set(identifier, newEvents); + } + } + + /** + * Process all the given events. The value must be a key/value pair. All items + * will attempt to be processed. + */ + public events(allEvents: { [key: string]: T }): void { + Object.keys(allEvents).forEach((key): void => { + this.event(key, allEvents[key], allEvents); + }); + } + + /** + * Remove the given handler, the name of the event must be the same as the one used when + * creating the handler, and the object must be the same as the one returned by addHandler + * + * @param identifier The name of the event handler string. + * @param event The event itself. + */ + public removeHandler(identifier: string, event: Event): void { + const ev = this.eventDictionary.remove(identifier, (value): boolean => value === event); + if (ev && ev.expiry) { + clearTimeout(ev.expiry); + } + } +} From be9a4d9ae8a623da9bbea8512aed47c30883bceb Mon Sep 17 00:00:00 2001 From: Paul Matthew Barrameda Date: Thu, 4 Apr 2019 04:22:32 -0700 Subject: [PATCH 046/152] Added unit tests You ready to merge yet @maxwey :^) --- .eslintrc.yml | 6 + package.json | 10 +- src/common/struct/DictionaryList.ts | 16 +- src/common/struct/UpdateHandler.ts | 132 +++++---- test/common/struct/DictionaryList.test.ts | 131 +++++++++ test/common/struct/UpdateHandler.test.ts | 310 ++++++++++++++++++++++ tsconfig.json | 3 + 7 files changed, 546 insertions(+), 62 deletions(-) create mode 100644 test/common/struct/DictionaryList.test.ts create mode 100644 test/common/struct/UpdateHandler.test.ts diff --git a/.eslintrc.yml b/.eslintrc.yml index 114f1e5c..538626bc 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -8,6 +8,7 @@ extends: plugins: - import + - chai-friendly env: browser: true @@ -24,8 +25,13 @@ settings: # Additional rules rules: + # Turn off linting about having wrong line ending, handled in .gitattributes linebreak-style: off + # Have chai friendly no-unused-expressions rules, otherwise code can look nasty + no-unused-expressions: off + chai-friendly/no-unused-expressions: error + # Add strictness to type declarations in TypeScript "@typescript-eslint/explicit-function-return-type": error "@typescript-eslint/no-explicit-any": error diff --git a/package.json b/package.json index 870ec577..82b7f7c0 100644 --- a/package.json +++ b/package.json @@ -7,9 +7,10 @@ "repository": "https://github.com/NGCP/GCS", "scripts": { "postinstall": "electron-builder install-app-deps", - "test": "npm run test:lint & npm run test:types", + "test": "npm run test:lint & npm run test:types & npm run test:unit", "test:lint": "eslint --ext .js,.jsx,.ts,.tsx \".\" & stylelint \"**/*.css\"", "test:types": "tsc --project . --outDir dist --pretty", + "test:unit": "mocha --require ts-node/register test/**/*.test.ts", "lint": "eslint --fix --ext .js,.jsx,.ts,.tsx \".\" & stylelint \"**/*.css\" --fix", "start": "electron-webpack dev", "build": "electron-webpack && electron-builder" @@ -31,6 +32,8 @@ }, "devDependencies": { "@babel/preset-react": "^7.0.0", + "@types/chai": "^4.1.7", + "@types/mocha": "^5.2.6", "@types/pouchdb": "^6.3.3", "@types/react": "^16.8.12", "@types/react-dom": "^16.8.3", @@ -39,6 +42,7 @@ "@typescript-eslint/eslint-plugin": "^1.6.0", "@typescript-eslint/parser": "^1.6.0", "babel-eslint": "^10.0.1", + "chai": "^4.2.0", "dotenv-webpack": "^1.7.0", "electron": "^3.1.8", "electron-builder": "^20.39.0", @@ -47,12 +51,16 @@ "eslint": "^5.16.0", "eslint-config-airbnb": "^17.1.0", "eslint-import-resolver-typescript": "^1.1.1", + "eslint-plugin-chai-friendly": "^0.4.1", "eslint-plugin-import": "^2.16.0", "eslint-plugin-jsx-a11y": "^6.2.1", "eslint-plugin-react": "^7.12.4", + "mocha": "^6.0.2", "stylelint": "^9.10.1", "stylelint-config-standard": "^18.2.0", "stylelint-order": "^2.1.0", + "ts-mocha": "^6.0.0", + "ts-node": "^8.0.3", "typescript": "^3.3.4000", "webpack": "^4.29.6" }, diff --git a/src/common/struct/DictionaryList.ts b/src/common/struct/DictionaryList.ts index 4bc89e46..6a2d0f40 100644 --- a/src/common/struct/DictionaryList.ts +++ b/src/common/struct/DictionaryList.ts @@ -1,4 +1,4 @@ -type Callback = (value: T, index: number, array: T[]) => boolean; +export type Callback = (value: T, index: number, array: T[]) => boolean; /** * Structure that can store lists in a dictionary. Lists can be obtained with a key string, @@ -160,9 +160,19 @@ export default class DictionaryList { } /** - * Gets the total number of items in the lists in the dictionary. + * Gets the total number of items in the lists in the dictionary, or a certain list if + * a key is provided. + * + * @param key The key to access the list in the dictionary. */ - public size(): number { + public size(key?: string): number { + if (key) { + const list = this.dictionary[key]; + if (!list) return 0; + + return list.length; + } + return this.numItems; } } diff --git a/src/common/struct/UpdateHandler.ts b/src/common/struct/UpdateHandler.ts index 0f60ff8c..c39780e5 100644 --- a/src/common/struct/UpdateHandler.ts +++ b/src/common/struct/UpdateHandler.ts @@ -1,52 +1,60 @@ -import DictionaryList from './DictionaryList'; +/* eslint-disable @typescript-eslint/no-explicit-any */ -type Handler = (value: T, allEvents?: { [key: string]: T }) => boolean; +type Handler = (value: T, events?: { [key: string]: any }) => boolean; /** - * Interface to handle expiry for an event when it has a timeout. + * Options for the timeout for a listener. */ -interface EventTimeout { +interface ListenerTimeoutOptions { /** - * Callback that is executed when timeout occurs. + * Function that will execute once the function times out. */ callback(): void; /** - * Time is in milliseconds + * Amount of time (in milliseconds) for the function to time out. */ time: number; } -interface Event { +interface Listener { /** - * Handler function for the event. + * Function that checks whether or not a given event value passes this listener's standards. */ handler: Handler; /** - * Expiry for a timeout, allows us to call clearTimeout on it. + * Reference to the setTimeout that is created. This allows us to call clearTimeout when + * deleting the handler while the timeout has not executed. */ expiry?: NodeJS.Timeout; } - -export default class UpdateHandler { - private eventDictionary = new DictionaryList>(); +/** + * Handler that can create listeners for certain events which will perform callbacks + * when the listener receives such event. Basically it is something that can handle processing + * different types of events with different values. + * + * For example, a handler can have a listener for an event which will close once it receives + * an event with a certain response (in our case it can be an event with a "complete" message). + */ +export default class UpdateHandler { + private eventDictionary: { [key: string]: Listener[] | undefined } = {}; /** - * Adds a new event to the handlers. + * Adds a new listener to the handlers. * - * @param identifier The name of the event handler string. + * @param name The name of the event. * @param handler The function to handle the event. * @param timeout The timeout function and time for the event to expire. */ - public addHandler( - identifier: string, + public addHandler( + name: string, handler: Handler, - timeout?: EventTimeout, - ): Event { + timeout?: ListenerTimeoutOptions, + ): Listener { // Create an event with the handler function. - const event: Event = { handler }; + const listener: Listener = { handler }; /* * If a timeout is provided, the event will remove itself and run its callback function @@ -54,67 +62,75 @@ export default class UpdateHandler { * we will need to call the clearTimeout function on the event's expiry. */ if (timeout) { - event.expiry = setTimeout((): void => { - this.eventDictionary.remove(identifier, (value): boolean => value !== event); + listener.expiry = setTimeout((): void => { + // Deletes the listener. + if (this.eventDictionary[name]) { + this.eventDictionary[name] = (this.eventDictionary[name] as Listener[]) + .filter((lis): boolean => lis !== listener); + } + timeout.callback(); }, timeout.time); } // Adds the event to the list with the proper event identifier. - this.eventDictionary.push(identifier, event); + if (!this.eventDictionary[name]) { + this.eventDictionary[name] = []; + } + (this.eventDictionary[name] as Listener[]).push(listener); - return event; + return listener; } /** * Processes the event based on the handlers stored. * - * @param identifier The name of the event handler string. - * @param value The value being updated. - * @param allEvents Key/Value pair of all events. + * @param name The name of the event that occured. + * @param value The value being passed to the listeners of that event. + * @param events All other events that happened at that time. */ - public event(identifier: string, value: T, allEvents?: { [key: string]: T }): void { - const newEvents = this.eventDictionary.filter(identifier, (v): boolean => { - const handled = v.handler(value, allEvents); - if (handled) { - // Remove handler. - if (v.expiry) { - clearTimeout(v.expiry); + public event(name: string, value: T, events?: { [key: string]: any }): void { + if (!this.eventDictionary[name]) return; + + this.eventDictionary[name] = (this.eventDictionary[name] as Listener[]) + .filter((lis): boolean => { + const handled = lis.handler(value, events); + /* + * Removes listener if it has not been triggered yet. Will also clearTimeout + * the expiry if the listener has one so that the timeout will not trigger afterwards. + */ + if (handled) { + if (lis.expiry) clearTimeout(lis.expiry); } - - return false; - } - - // Keep handler. - return true; - }); - - if (newEvents) { - this.eventDictionary.set(identifier, newEvents); - } + return !handled; + }); } /** - * Process all the given events. The value must be a key/value pair. All items - * will attempt to be processed. + * Process all the given events. + * + * @param events Key/value pair of the events that happened and their corresponding values. */ - public events(allEvents: { [key: string]: T }): void { - Object.keys(allEvents).forEach((key): void => { - this.event(key, allEvents[key], allEvents); + public events(events: { [key: string]: any }): void { + Object.keys(events).forEach((key): void => { + this.event(key, events[key], events); }); } /** - * Remove the given handler, the name of the event must be the same as the one used when - * creating the handler, and the object must be the same as the one returned by addHandler + * Remove the listener from the handler. The provided name and listener must be the same + * as the ones used when creating the handler. * - * @param identifier The name of the event handler string. - * @param event The event itself. + * @param name The name of the event handler string. + * @param listener The listener itself. */ - public removeHandler(identifier: string, event: Event): void { - const ev = this.eventDictionary.remove(identifier, (value): boolean => value === event); - if (ev && ev.expiry) { - clearTimeout(ev.expiry); - } + public removeHandler(name: string, listener: Listener): void { + // Will clearTimeout the listener if it has an expiry. + if (listener.expiry) clearTimeout(listener.expiry); + + // Removes the listener from the handler. + if (!this.eventDictionary[name]) return; + this.eventDictionary[name] = (this.eventDictionary[name] as Listener[]) + .filter((lis): boolean => lis !== listener); } } diff --git a/test/common/struct/DictionaryList.test.ts b/test/common/struct/DictionaryList.test.ts new file mode 100644 index 00000000..1d2381cb --- /dev/null +++ b/test/common/struct/DictionaryList.test.ts @@ -0,0 +1,131 @@ +import { expect } from 'chai'; +import { beforeEach, describe, it } from 'mocha'; + +// TODO: remove when https://github.com/benmosher/eslint-plugin-import/issues/1282 resolves +import DictionaryList, { Callback } from '../../../src/common/struct/DictionaryList'; // eslint-disable-line import/named + +let dict: DictionaryList; + +describe('DictionaryList', (): void => { + describe('+ shift()', (): void => { + beforeEach((): void => { dict = new DictionaryList(); }); + + it('should shift values in order that they were inserted', (): void => { + dict.push('P1', 100); + dict.push('P1', 101); + dict.push('P1', 102); + + expect(dict.size()).to.equal(3); + expect(dict.shift('P1')).to.equal(100); + expect(dict.shift('P1')).to.equal(101); + expect(dict.shift('P1')).to.equal(102); + expect(dict.size()).to.equal(0); + }); + + it('should return undefined when attempting to shift an empty or invalid key', (): void => { + dict.push('P1', 100); + + expect(dict.shift('P1')).to.equal(100); + expect(dict.shift('P1')).to.be.undefined; + expect(dict.shift('ETC')).to.be.undefined; + }); + }); + + describe('+ remove()', (): void => { + beforeEach((): void => { dict = new DictionaryList(); }); + + it('should use custom callback method to remove when set', (): void => { + const callback: Callback = (_, index, array): boolean => index === array.length - 1; + + dict.push('P1', 100); + dict.push('P1', 101); + dict.push('P1', 102); + dict.push('P1', 103); + + expect(dict.remove('P1', callback)).to.equal(103); + expect(dict.remove('P1', callback)).to.equal(102); + expect(dict.remove('P1', callback)).to.equal(101); + expect(dict.remove('P1', callback)).to.equal(100); + expect(dict.remove('P1', callback)).to.be.undefined; + }); + }); + + describe('+ push()', (): void => { + beforeEach((): void => { dict = new DictionaryList(); }); + + it('should add an item to the list with the specified key', (): void => { + expect(dict.get('P1')).to.be.undefined; + expect(dict.get('P2')).to.be.undefined; + expect(dict.get('P3')).to.be.undefined; + + dict.push('P1', 100); + dict.push('P1', 100); + dict.push('P1', 100); + dict.push('P2', 100); + + expect(dict.size('P1')).to.equal(3); + expect(dict.size('P2')).to.equal(1); + expect(dict.size('P3')).to.equal(0); + expect(dict.size()).to.equal(4); + }); + + it('should be able to add to an old key that has previously been exhaused', (): void => { + dict.push('P1', 100); + dict.push('P1', 100); + dict.push('P1', 100); + dict.push('P2', 100); + + dict.shift('P1'); + dict.shift('P1'); + dict.shift('P1'); + + expect(dict.size('P1')).to.equal(0); + expect(dict.size('P2')).to.equal(1); + expect(dict.size()).to.equal(1); + + dict.push('P1', 101); + + expect(dict.size('P1')).to.equal(1); + expect(dict.shift('P1')).to.equal(101); + }); + }); + + describe('+ removeAll()', (): void => { + beforeEach((): void => { dict = new DictionaryList(); }); + + it('should remove the values equal to that specified for the given key', (): void => { + dict.push('P1', 100); + dict.push('P1', 101); + dict.push('P2', 102); + dict.push('P3', 103); + + expect(dict.size('P1')).to.equal(2); + expect(dict.size()).to.equal(4); + + dict.removeAll('P1', (value): boolean => value === 101); + expect(dict.size('P1')).to.equal(1); + + dict.removeAll('P1', (value): boolean => value === 100); + expect(dict.size('P1')).to.equal(0); + expect(dict.size()).to.equal(2); + }); + + it('should remove all the values equal to that specified for the given key, other keys are untouched', (): void => { + dict.push('P1', 101); + dict.push('P1', 101); + dict.push('P1', 101); + dict.push('P1', 102); + dict.push('P1', 101); + + dict.push('P2', 101); + + expect(dict.size('P1')).to.equal(5); + expect(dict.size()).to.equal(6); + + dict.removeAll('P1', (value): boolean => value === 101); + expect(dict.size('P1')).to.equal(1); + + expect(dict.size()).to.equal(2); + }); + }); +}); diff --git a/test/common/struct/UpdateHandler.test.ts b/test/common/struct/UpdateHandler.test.ts new file mode 100644 index 00000000..ef7a168f --- /dev/null +++ b/test/common/struct/UpdateHandler.test.ts @@ -0,0 +1,310 @@ +import { expect } from 'chai'; +import { beforeEach, describe, it } from 'mocha'; + +import UpdateHandler from '../../../src/common/struct/UpdateHandler'; + +let handler: UpdateHandler; + +describe('UpdateHandler', (): void => { + describe('+ addHandler() & + event()', (): void => { + beforeEach((): void => { handler = new UpdateHandler(); }); + + it('should allow adding a simple one time event', (): void => { + let counter = 0; + + handler.addHandler('status', (v): boolean => { + if (v === 10) counter += 1; + return v === 10; + }); + + expect(counter).to.equal(0); + + // Event should run and remove itself (only runs once). + handler.event('status', 10); + expect(counter).to.equal(1); + + // Since event not present, should do nothing. + handler.event('status', 10); + expect(counter).to.equal(1); + }); + + it('event does not get removed if it does not get run', (): void => { + let counter = 0; + + handler.addHandler('status', (v): boolean => { + if (v === 10) counter += 1; + return v === 10; + }); + + expect(counter).to.equal(0); + + handler.event('nope', 10); + expect(counter).to.equal(0); + + handler.event('status', 10); + expect(counter).to.equal(1); + }); + + it('should allow adding events that can be run, but not get removed immediately', (): void => { + let counter = 0; + + handler.addHandler('status', (v): boolean => { + counter += 1; + return v === 10; + }); + + expect(counter).to.equal(0); + + handler.event('status', 0); + expect(counter).to.equal(1); + + handler.event('status', -100); + expect(counter).to.equal(2); + + handler.event('status', 1); + expect(counter).to.equal(3); + + handler.event('status', 10); + expect(counter).to.equal(4); + + handler.event('status', 10); + expect(counter).to.equal(4); + }); + + it('should allow adding several events with different keys', (): void => { + let counter = 0; + + handler.addHandler('status', (v): boolean => { + counter += 1; + return v === 10; + }); + + handler.addHandler('job', (v): boolean => { + counter += 100; + return v === 'error'; + }); + + expect(counter).to.equal(0); + + handler.event('status', 0); + expect(counter).to.equal(1); + + handler.event('job', -100); + expect(counter).to.equal(101); + + handler.event('status', 1); + expect(counter).to.equal(102); + + handler.event('job', 'error'); + expect(counter).to.equal(202); + + handler.event('status', 10); + expect(counter).to.equal(203); + + handler.event('job', 'error'); + expect(counter).to.equal(203); + + handler.event('status', 10); + expect(counter).to.equal(203); + }); + + it('should allow adding several events with the same key', (): void => { + let counter1 = 0; + let counter2 = 0; + + handler.addHandler('status', (v): boolean => { + counter1 += 1; + return v === 10; + }); + + handler.addHandler('status', (v): boolean => { + counter2 += 100; + return v === 'error'; + }); + + expect(counter1).to.equal(0); + expect(counter2).to.equal(0); + + handler.event('status', 0); + expect(counter1).to.equal(1); + expect(counter2).to.equal(100); + + handler.event('status', 1); + expect(counter1).to.equal(2); + expect(counter2).to.equal(200); + + handler.event('status', 'error'); + expect(counter1).to.equal(3); + expect(counter2).to.equal(300); + + handler.event('status', 'error'); + expect(counter1).to.equal(4); + expect(counter2).to.equal(300); + + handler.event('status', 10); + expect(counter1).to.equal(5); + expect(counter2).to.equal(300); + + handler.event('status', 10); + expect(counter1).to.equal(5); + expect(counter2).to.equal(300); + }); + + it('should allow for events to expire on their own after a certain amount of time', (done): void => { + /* + * This test tests that the timeout runs after a certain amount of time. + * If this test is timing out, this is likely because the timeout handler is + * not getting executed. + * Failure can also happen if the expiry runs before the events (unlikely). + * Try increasing the expiry to a longer time (will slow tests) to check if this is your + * issue. + */ + let counter = 0; + + handler.addHandler('status', (v): boolean => { + counter += 1; + return v === 10; + }, { + callback(): void { + counter += 100; + expect(counter).to.equal(102); + + handler.event('status', 0); + expect(counter).to.equal(102); + + done(); + }, + time: 25, + }); + + expect(counter).to.equal(0); + + handler.event('status', 0); + expect(counter).to.equal(1); + + handler.event('status', 0); + expect(counter).to.equal(2); + }); + + it('should allow for events to expire on their own after a certain amount of time, with more events for the same key', (done): void => { + /* + * This test tests that the timeout runs after a certain amount of time. + * If this test is timing out, this is likely because the timeout handler is + * not getting executed. + * Failure can also happen if the expiry runs before the events (unlikely). + * Try increasing the expiry to a longer time (will slow tests) to check if this is your + * issue. + */ + let counter1 = 0; + let counter2 = 0; + + handler.addHandler('status', (v): boolean => { + counter1 += 1; + return v === 10; + }, { + callback(): void { + counter1 -= 100; + expect(counter1).to.equal(-98); + expect(counter2).to.equal(2); + + handler.event('status', 0); + expect(counter1).to.equal(-98); + expect(counter2).to.equal(3); + + done(); + }, + time: 25, + }); + + handler.addHandler('status', (v): boolean => { + counter2 += 1; + return v === 'error'; + }); + + expect(counter1).to.equal(0); + expect(counter2).to.equal(0); + + handler.event('status', 0); + expect(counter1).to.equal(1); + expect(counter2).to.equal(1); + + handler.event('status', 0); + expect(counter1).to.equal(2); + expect(counter2).to.equal(2); + }); + }); + + describe('+ events()', (): void => { + beforeEach((): void => { handler = new UpdateHandler(); }); + + it('should processing many events at once', (): void => { + handler = new UpdateHandler(); + let statusCounter = 0; + let locationCounter = 0; + + handler.addHandler('status', (v): boolean => { + statusCounter += v; + return false; + }); + + handler.addHandler('location', (v): boolean => { + locationCounter += v; + return false; + }); + + expect(statusCounter).to.equal(0); + expect(locationCounter).to.equal(0); + + handler.events({ status: 1, location: 1 }); + + expect(statusCounter).to.equal(1); + expect(locationCounter).to.equal(1); + + handler.events({ status: 5 }); + + expect(statusCounter).to.equal(6); + expect(locationCounter).to.equal(1); + }); + }); + + + describe('+ removeHandler()', (): void => { + beforeEach((): void => { handler = new UpdateHandler(); }); + + it('should allow the removal of the given handler', (): void => { + handler = new UpdateHandler(); + let statusCounter = 0; + let locationCounter = 0; + + const statusHandler = handler.addHandler('status', (v): boolean => { + statusCounter += v; + return false; + }); + + const locationHandler = handler.addHandler('location', (v): boolean => { + locationCounter += v; + return false; + }); + + expect(statusCounter).to.equal(0); + expect(locationCounter).to.equal(0); + + handler.events({ status: 1, location: 1 }); + + expect(statusCounter).to.equal(1); + expect(locationCounter).to.equal(1); + + handler.removeHandler('status', statusHandler); + handler.events({ status: 1, location: 1 }); + + expect(statusCounter).to.equal(1); + expect(locationCounter).to.equal(2); + + handler.removeHandler('location', locationHandler); + handler.events({ status: 1, location: 1 }); + + expect(statusCounter).to.equal(1); + expect(locationCounter).to.equal(2); + }); + }); +}); diff --git a/tsconfig.json b/tsconfig.json index 699fd7d2..7d14a3aa 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,9 @@ { "extends": "./node_modules/electron-webpack/tsconfig-base.json", "compilerOptions": { + "target": "es6", + "module": "commonjs", + "jsx": "react", "resolveJsonModule": true, From 4b10a5a5b79b12782c5128e134914307ca205012 Mon Sep 17 00:00:00 2001 From: Paul Matthew Barrameda Date: Thu, 4 Apr 2019 14:51:42 -0700 Subject: [PATCH 047/152] Import node in hopes to fix Timeout issue Also updated .travis.yml script to run tests one at a time. `npm test` can be left so that the developer can use it whenever necessary. --- .travis.yml | 6 +++++- package.json | 1 + 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index c32abd19..facbc8ea 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,7 +15,11 @@ cache: npm # Scripts to run to test application script: - - npm test + # Run each test manually instead of npm test so that it runs sequentially (and not in parallel) + - npm run test:lint + - npm run test:types + - npm run test:unit + - fossa init - fossa analyze - fossa test diff --git a/package.json b/package.json index 82b7f7c0..7f76c325 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "@babel/preset-react": "^7.0.0", "@types/chai": "^4.1.7", "@types/mocha": "^5.2.6", + "@types/node": "^11.13.0", "@types/pouchdb": "^6.3.3", "@types/react": "^16.8.12", "@types/react-dom": "^16.8.3", From 0973dceb535f775442721311c7fb2b7c895daa75 Mon Sep 17 00:00:00 2001 From: Paul Matthew Barrameda Date: Thu, 4 Apr 2019 16:34:11 -0700 Subject: [PATCH 048/152] Minor changes Added signatures to json imports in src/static. Added electron to import's core-modules so we don't need to put "eslint-disable-line import/no-extraneous-dependencies" on every electron import. Mapped vehicles in order before passing it from vehicle container to vehicle table to increase performance from O(n^2 logn) to O(nlogn). --- .eslintrc.yml | 8 +++- src/main/index.ts | 14 ++---- src/renderer/fixtures/completeJobFixture.ts | 2 +- .../fixtures/updateMessagesFixtures.ts | 2 +- .../fixtures/updateVehiclesFixtures.ts | 2 +- src/renderer/index.js | 2 +- src/renderer/mainWindow/MainWindow.tsx | 4 +- src/renderer/mainWindow/log/LogContainer.tsx | 2 +- src/renderer/mainWindow/log/log.css | 2 +- src/renderer/mainWindow/map/MapContainer.tsx | 9 +--- src/renderer/mainWindow/map/VehicleMarker.tsx | 22 ++++----- .../map/control/GeolocationControl.tsx | 2 +- .../mainWindow/map/control/ThemeControl.tsx | 2 +- .../mainWindow/mission/MissionContainer.tsx | 2 +- .../mainWindow/mission/MissionTable.tsx | 2 +- .../mainWindow/vehicle/VehicleContainer.tsx | 12 +++-- .../mainWindow/vehicle/VehicleTable.tsx | 12 ++--- src/renderer/missionWindow/MissionWindow.jsx | 2 +- .../missionWindow/missions/Mission.jsx | 2 +- .../missionWindow/missions/jobs/Job.jsx | 2 +- src/static/index.ts | 47 +++++++++++++------ src/util/types.ts | 28 ----------- src/util/util.ts | 17 ++----- test/common/struct/DictionaryList.test.ts | 2 +- 24 files changed, 85 insertions(+), 116 deletions(-) diff --git a/.eslintrc.yml b/.eslintrc.yml index 538626bc..a431ec05 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -14,15 +14,21 @@ env: browser: true node: true -# TypeScript compatibility settings: + # TypeScript compatibility import/parsers: "@typescript-eslint/parser": - .ts - .tsx + + # TypeScript compatibility import/resolver: typescript: {} + # Modules that should be a devDependency and will be used in the application + import/core-modules: + - electron + # Additional rules rules: # Turn off linting about having wrong line ending, handled in .gitattributes diff --git a/src/main/index.ts b/src/main/index.ts index 7fef6724..12230c7c 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -1,4 +1,4 @@ -import { // eslint-disable-line import/no-extraneous-dependencies +import { app, BrowserWindow, dialog, @@ -13,15 +13,7 @@ import { // eslint-disable-line import/no-extraneous-dependencies import fs from 'fs'; import moment from 'moment'; -import { images, locations as locationsConfig } from '../static/index'; - -import { LocationSignature } from '../util/types'; - -/** - * List of locations that are in the locations configuration file. - * Will be loaded under the locations column in the menu. - */ -const locations: LocationSignature = locationsConfig; +import { images, locations } from '../static/index'; /** * This key is required to enable geolocation in the application. @@ -52,7 +44,7 @@ const HEIGHT = 576; const isDevelopment = process.env.NODE_ENV !== 'production'; // TODO: Put icon tray back to macOS but resize it so that its not huge on macOS's menu. -const icon = nativeImage.createFromDataURL(images.icon); +const icon = nativeImage.createFromDataURL(images.icon as string); /** *Variable to keep track when the app will quit, which is different from hiding the app. diff --git a/src/renderer/fixtures/completeJobFixture.ts b/src/renderer/fixtures/completeJobFixture.ts index 77e7918d..3a710920 100644 --- a/src/renderer/fixtures/completeJobFixture.ts +++ b/src/renderer/fixtures/completeJobFixture.ts @@ -1,4 +1,4 @@ -import { ipcRenderer } from 'electron'; // eslint-disable-line import/no-extraneous-dependencies +import { ipcRenderer } from 'electron'; /** * Sends a completeJob notification 5 seconds after the startJob notification is received. diff --git a/src/renderer/fixtures/updateMessagesFixtures.ts b/src/renderer/fixtures/updateMessagesFixtures.ts index de14a18a..ccf5caf8 100644 --- a/src/renderer/fixtures/updateMessagesFixtures.ts +++ b/src/renderer/fixtures/updateMessagesFixtures.ts @@ -1,4 +1,4 @@ -import { ipcRenderer } from 'electron'; // eslint-disable-line import/no-extraneous-dependencies +import { ipcRenderer } from 'electron'; // TODO: Remove disable line comment when issue gets fixed (https://github.com/benmosher/eslint-plugin-import/pull/1304) import { MessageType } from '../../util/types'; // eslint-disable-line import/named diff --git a/src/renderer/fixtures/updateVehiclesFixtures.ts b/src/renderer/fixtures/updateVehiclesFixtures.ts index d83f035d..a0a25f13 100644 --- a/src/renderer/fixtures/updateVehiclesFixtures.ts +++ b/src/renderer/fixtures/updateVehiclesFixtures.ts @@ -1,4 +1,4 @@ -import { ipcRenderer } from 'electron'; // eslint-disable-line import/no-extraneous-dependencies +import { ipcRenderer } from 'electron'; import { vehicleStatuses } from '../../static/index'; diff --git a/src/renderer/index.js b/src/renderer/index.js index 9184dc79..96200d8f 100644 --- a/src/renderer/index.js +++ b/src/renderer/index.js @@ -1,6 +1,6 @@ /* eslint-disable react/jsx-filename-extension */ -import { ipcRenderer } from 'electron'; // eslint-disable-line import/no-extraneous-dependencies +import { ipcRenderer } from 'electron'; import React, { Component } from 'react'; import ReactDOM from 'react-dom'; diff --git a/src/renderer/mainWindow/MainWindow.tsx b/src/renderer/mainWindow/MainWindow.tsx index 8e3751bb..8647588a 100644 --- a/src/renderer/mainWindow/MainWindow.tsx +++ b/src/renderer/mainWindow/MainWindow.tsx @@ -1,12 +1,12 @@ import React, { ReactNode } from 'react'; +import { ThemeProps } from '../../util/types'; + import LogContainer from './log/LogContainer'; import MapContainer from './map/MapContainer'; import MissionContainer from './mission/MissionContainer'; import VehicleContainer from './vehicle/VehicleContainer'; -import { ThemeProps } from '../../util/types'; - import './main.css'; /** diff --git a/src/renderer/mainWindow/log/LogContainer.tsx b/src/renderer/mainWindow/log/LogContainer.tsx index 9ab11022..28de4266 100644 --- a/src/renderer/mainWindow/log/LogContainer.tsx +++ b/src/renderer/mainWindow/log/LogContainer.tsx @@ -1,4 +1,4 @@ -import { Event, ipcRenderer } from 'electron'; // eslint-disable-line import/no-extraneous-dependencies +import { Event, ipcRenderer } from 'electron'; import moment, { Moment } from 'moment'; import React, { Component, FormEvent, ReactNode } from 'react'; import { diff --git a/src/renderer/mainWindow/log/log.css b/src/renderer/mainWindow/log/log.css index dacc75de..d8e79f96 100644 --- a/src/renderer/mainWindow/log/log.css +++ b/src/renderer/mainWindow/log/log.css @@ -32,7 +32,7 @@ .logContainer .messages_dark { background-color: #141414; - color: #fff; + color: #bdbdbd; font-family: Consolas, Menlo, diff --git a/src/renderer/mainWindow/map/MapContainer.tsx b/src/renderer/mainWindow/map/MapContainer.tsx index 2a22dc9b..34d70be7 100644 --- a/src/renderer/mainWindow/map/MapContainer.tsx +++ b/src/renderer/mainWindow/map/MapContainer.tsx @@ -1,4 +1,4 @@ -import { Event, ipcRenderer } from 'electron'; // eslint-disable-line import/no-extraneous-dependencies +import { Event, ipcRenderer } from 'electron'; import fs from 'fs'; import { LocationEvent } from 'leaflet'; import React, { @@ -10,15 +10,12 @@ import React, { } from 'react'; import { Map, TileLayer, Viewport } from 'react-leaflet'; -import { - locations as locationsConfig, startLocation, -} from '../../../static/index'; +import { locations, startLocation } from '../../../static/index'; import { FileLoadOptions, FileSaveOptions, LatLngZoom, - LocationSignature, ThemeProps, Vehicle, VehicleUI, @@ -33,8 +30,6 @@ import VehicleMarker from './VehicleMarker'; import './map.css'; -const locations: LocationSignature = locationsConfig; - const mapOptions = { attribution: 'Map data © OpenStreetMap contributors, CC-BY-SA, Imagery © Mapbox', url: 'https://api.tiles.mapbox.com/v4/{id}/{z}/{x}/{y}.png?access_token={accessToken}', diff --git a/src/renderer/mainWindow/map/VehicleMarker.tsx b/src/renderer/mainWindow/map/VehicleMarker.tsx index 1161ff25..6dc8b7da 100644 --- a/src/renderer/mainWindow/map/VehicleMarker.tsx +++ b/src/renderer/mainWindow/map/VehicleMarker.tsx @@ -2,36 +2,34 @@ import L from 'leaflet'; import React, { PureComponent, ReactNode } from 'react'; import { Marker, Tooltip } from 'react-leaflet'; -import { images } from '../../../static/index'; +import { images, RecursiveImageSignature } from '../../../static/index'; // TODO: Remove disable line comment when issue gets fixed (https://github.com/benmosher/eslint-plugin-import/pull/1304) import { VehicleUI } from '../../../util/types'; // eslint-disable-line import/named -interface VehicleSignature { - /** - * Get the vehicle by its vehicle type and status. - */ - [typeStatus: string]: string; -} - -const { vehicles }: { vehicles: VehicleSignature } = images.markers; - type VehicleMarkerProps = VehicleUI; +const vehicles = (images.markers as RecursiveImageSignature).vehicles as RecursiveImageSignature; + /** * Marker that will represent a vehicle. Used by the map container. */ export default class VehicleMarker extends PureComponent { public render(): ReactNode { const { - sid, name, lat, lng, type, status, + sid, + name, + lat, + lng, + type, + status, } = this.props; return ( { const { vehicles } = this.state; /* - * TODO: Map vehicles to an array by sid to increase performance by preventing VehicleTable - * from remapping the table N times. - * Will improve the time complexity from O(N^2logN) to O(NlogN) + * Put the sids of the vehicles in order (least to greatest) and maps those sids to their + * respective values in the vehicles object. Time complexity is O(nlogn). */ + const vehicleArray = Object.keys(vehicles) + .sort((a, b): number => parseInt(a, 10) - parseInt(b, 10)) + .map((sid): VehicleUI => vehicles[sid]); return (
- +
); } diff --git a/src/renderer/mainWindow/vehicle/VehicleTable.tsx b/src/renderer/mainWindow/vehicle/VehicleTable.tsx index a44b28e0..052cf897 100644 --- a/src/renderer/mainWindow/vehicle/VehicleTable.tsx +++ b/src/renderer/mainWindow/vehicle/VehicleTable.tsx @@ -1,4 +1,4 @@ -import { ipcRenderer } from 'electron'; // eslint-disable-line import/no-extraneous-dependencies +import { ipcRenderer } from 'electron'; import React, { Component, ReactNode } from 'react'; import { AutoSizer, @@ -31,7 +31,7 @@ export interface VehicleTableProps extends ThemeProps { /** * Object of vehicles. */ - vehicles: { [sid: string]: VehicleUI }; + vehicles: VehicleUI[]; } /** @@ -60,10 +60,8 @@ export default class VehicleTable extends Component { * Callback when a row is clicked in the table. */ private onRowClick(info: RowMouseEventHandlerParams): void { - const { rowData } = info; const { vehicles } = this.props; - - ipcRenderer.send('post', 'centerMapToVehicle', vehicles[rowData.sid]); + ipcRenderer.send('post', 'centerMapToVehicle', vehicles[info.index]); } /** @@ -76,9 +74,7 @@ export default class VehicleTable extends Component { */ private rowGetter({ index }: Index): VehicleUI { const { vehicles } = this.props; - - const v = Object.keys(vehicles).sort((a, b): number => parseInt(a, 10) - parseInt(b, 10)); - return vehicles[v[index]]; + return vehicles[index]; } /** diff --git a/src/renderer/missionWindow/MissionWindow.jsx b/src/renderer/missionWindow/MissionWindow.jsx index 487a6b2a..8fde70d0 100644 --- a/src/renderer/missionWindow/MissionWindow.jsx +++ b/src/renderer/missionWindow/MissionWindow.jsx @@ -1,4 +1,4 @@ -import { ipcRenderer } from 'electron'; // eslint-disable-line import/no-extraneous-dependencies +import { ipcRenderer } from 'electron'; import PropTypes from 'prop-types'; import React, { Component } from 'react'; diff --git a/src/renderer/missionWindow/missions/Mission.jsx b/src/renderer/missionWindow/missions/Mission.jsx index bf4c9bd3..e58720e1 100644 --- a/src/renderer/missionWindow/missions/Mission.jsx +++ b/src/renderer/missionWindow/missions/Mission.jsx @@ -1,4 +1,4 @@ -import { ipcRenderer } from 'electron'; // eslint-disable-line import/no-extraneous-dependencies +import { ipcRenderer } from 'electron'; import PropTypes from 'prop-types'; import React, { Component } from 'react'; diff --git a/src/renderer/missionWindow/missions/jobs/Job.jsx b/src/renderer/missionWindow/missions/jobs/Job.jsx index bf9054fc..a210e5a4 100644 --- a/src/renderer/missionWindow/missions/jobs/Job.jsx +++ b/src/renderer/missionWindow/missions/jobs/Job.jsx @@ -1,4 +1,4 @@ -import { ipcRenderer } from 'electron'; // eslint-disable-line import/no-extraneous-dependencies +import { ipcRenderer } from 'electron'; import PropTypes from 'prop-types'; import React, { Component } from 'react'; diff --git a/src/static/index.ts b/src/static/index.ts index 69ced48d..e43c0d8e 100644 --- a/src/static/index.ts +++ b/src/static/index.ts @@ -2,6 +2,14 @@ /* eslint-disable @typescript-eslint/camelcase */ +import { fixtures, geolocation } from './config.json'; +import { startLocation, locations as locationsObject } from './locations.json'; +import { + vehicleIds as vehicleIdsObject, + vehicleInfos as vehicleInfosObject, + vehicleStatuses as vehicleStatusesObject, +} from './vehicle.json'; + import arrow from './images/arrow.png'; import icon from './images/icon.png'; import pin from './images/pin.png'; @@ -25,26 +33,37 @@ import geolocation_img from './images/other/geolocation.png'; import moon_hover from './images/other/moon_hover.png'; import moon from './images/other/moon.png'; -import { fixtures, geolocation } from './config.json'; -import { startLocation, locations } from './locations.json'; -import { vehicleIds, vehicleInfos, vehicleStatuses } from './vehicle.json'; +// Export json objects with signature to allow us to access it with TypeScript. +export const locations: { + [name: string]: { lat: number; lng: number; zoom?: number }; +} = locationsObject; -/* - * TODO: add typings to all variables before exporting (and remove the corresponding typings) - * in types.ts. - */ +export const vehicleIds: { + [name: string]: number; +} = vehicleIdsObject; + +export const vehicleInfos: { + [id: string]: { macAddress: string; name: string; 'type': string }; +} = vehicleInfosObject; + +export const vehicleStatuses: { + [status: string]: { 'type': string; message: string }; +} = vehicleStatusesObject; export { fixtures, geolocation, startLocation, - locations, - vehicleIds, - vehicleInfos, - vehicleStatuses, }; -export const images = { +/** + * A given key for an image will give either a string or an object with similar structure. + */ +export interface RecursiveImageSignature { + [key: string]: string | RecursiveImageSignature; +} + +export const images: RecursiveImageSignature = { arrow, icon, pin, @@ -72,10 +91,10 @@ export const images = { export default { fixtures, - images, geolocation, - startLocation, + images, locations, + startLocation, vehicleIds, vehicleInfos, vehicleStatuses, diff --git a/src/util/types.ts b/src/util/types.ts index 5999040a..d96335ba 100644 --- a/src/util/types.ts +++ b/src/util/types.ts @@ -239,31 +239,3 @@ export interface FileSaveOptions { */ data: FileLoadOptions; } - -/** - * Signature to the locations variable in locations.json. - */ -export interface LocationSignature { - [key: string]: LatLngZoom; -} - -/** - * Signature to the vehicleInfos variable in vehicle.json. - */ -export interface VehicleInfoSignature { - [key: string]: { - macAddress: string; - name: string; - type: string; - }; -} - -/** - * Signature to the vehicleStatuses variable in vehicle.json. - */ -export interface VehicleStatusSignature { - [key: string]: { - type: string; - message: string; - }; -} diff --git a/src/util/util.ts b/src/util/util.ts index 5dcb1548..d776d64e 100644 --- a/src/util/util.ts +++ b/src/util/util.ts @@ -1,21 +1,10 @@ -import { ipcRenderer } from 'electron'; // eslint-disable-line import/no-extraneous-dependencies +import { ipcRenderer } from 'electron'; import { Component } from 'react'; -import { - vehicleInfos as vehicleInfosConfig, vehicleStatuses as vehicleStatusesConfig, -} from '../static/index'; +import { vehicleInfos, vehicleStatuses } from '../static/index'; // TODO: Remove disable line comment when issue gets fixed (https://github.com/benmosher/eslint-plugin-import/pull/1304) -import { - MessageType, // eslint-disable-line import/named - Vehicle, - VehicleInfoSignature, - VehicleStatusSignature, - VehicleUI, -} from './types'; - -const vehicleInfos: VehicleInfoSignature = vehicleInfosConfig; -const vehicleStatuses: VehicleStatusSignature = vehicleStatusesConfig; +import { MessageType, Vehicle, VehicleUI } from './types'; // eslint-disable-line import/named /** * Updates vehicles being shown. diff --git a/test/common/struct/DictionaryList.test.ts b/test/common/struct/DictionaryList.test.ts index 1d2381cb..5f0f88cb 100644 --- a/test/common/struct/DictionaryList.test.ts +++ b/test/common/struct/DictionaryList.test.ts @@ -1,7 +1,7 @@ import { expect } from 'chai'; import { beforeEach, describe, it } from 'mocha'; -// TODO: remove when https://github.com/benmosher/eslint-plugin-import/issues/1282 resolves +// TODO: Remove disable line comment when issue gets fixed (https://github.com/benmosher/eslint-plugin-import/pull/1304) import DictionaryList, { Callback } from '../../../src/common/struct/DictionaryList'; // eslint-disable-line import/named let dict: DictionaryList; From eed6bf540317eea35f2c377a8efcabc4aac3032c Mon Sep 17 00:00:00 2001 From: Paul Matthew Barrameda Date: Fri, 5 Apr 2019 02:16:23 -0700 Subject: [PATCH 049/152] Modified some files to prepare for Vehicle class Changed "listener" terminology in UpdateHandler to "handler". Changed updateVehicles and updateMessages notification to take multiple parameters instead of an array of vehicles/messages through parameter destrucutring. Added logic to static/index.ts to set startLocation and export that as a LatLngZoom instead of having other classes figure out that location. Also changed order of importing files in the same file, will add documentation on the order local files should be imported. Added disconnection time constant to static/vehicle.json that is also exported throughs static/index.ts. Changed Vehicle interface in util/types.ts to VehicleUpdate. Will eventually change this to UpdateMessage (see the JSON protocol) and will add typings for all other messages too (when Vehicle is done being implemented, the user interface will take in the Vehicle objects instead of UpdateMessages). Added failsafe measures to updateVehicles function. --- src/common/struct/UpdateHandler.ts | 84 +++++++++---------- src/main/index.ts | 6 +- .../fixtures/updateMessagesFixtures.ts | 2 +- .../fixtures/updateVehiclesFixtures.ts | 10 ++- src/renderer/mainWindow/log/LogContainer.tsx | 4 +- src/renderer/mainWindow/map/MapContainer.tsx | 21 ++--- .../mainWindow/vehicle/VehicleContainer.tsx | 4 +- src/static/index.ts | 58 +++++++------ src/static/vehicle.json | 1 + src/util/types.ts | 9 +- src/util/util.ts | 29 +++++-- 11 files changed, 124 insertions(+), 104 deletions(-) diff --git a/src/common/struct/UpdateHandler.ts b/src/common/struct/UpdateHandler.ts index c39780e5..dcf78329 100644 --- a/src/common/struct/UpdateHandler.ts +++ b/src/common/struct/UpdateHandler.ts @@ -1,11 +1,11 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -type Handler = (value: T, events?: { [key: string]: any }) => boolean; +type HandlerCheck = (value: T, events?: { [key: string]: any }) => boolean; /** - * Options for the timeout for a listener. + * Options for the timeout for a handler. */ -interface ListenerTimeoutOptions { +interface HandlerTimeoutOptions { /** * Function that will execute once the function times out. */ @@ -17,11 +17,11 @@ interface ListenerTimeoutOptions { time: number; } -interface Listener { +interface Handler { /** - * Function that checks whether or not a given event value passes this listener's standards. + * Function that checks whether or not a given event value passes this handler's standards. */ - handler: Handler; + handlerCheck: HandlerCheck; /** * Reference to the setTimeout that is created. This allows us to call clearTimeout when @@ -31,73 +31,73 @@ interface Listener { } /** - * Handler that can create listeners for certain events which will perform callbacks - * when the listener receives such event. Basically it is something that can handle processing + * Class that can create handlers for certain events which will perform callbacks + * when the handler receives such event. Basically it is something that can handle processing * different types of events with different values. * - * For example, a handler can have a listener for an event which will close once it receives - * an event with a certain response (in our case it can be an event with a "complete" message). + * For example, a handler can be created for an event which will close once it receives + * an event with a certain response. */ export default class UpdateHandler { - private eventDictionary: { [key: string]: Listener[] | undefined } = {}; + private eventDictionary: { [key: string]: Handler[] | undefined } = {}; /** - * Adds a new listener to the handlers. + * Adds a new handler. * * @param name The name of the event. - * @param handler The function to handle the event. + * @param handlerCheck The function to check whether or not the handler has been handled. * @param timeout The timeout function and time for the event to expire. */ public addHandler( name: string, - handler: Handler, - timeout?: ListenerTimeoutOptions, - ): Listener { - // Create an event with the handler function. - const listener: Listener = { handler }; + handlerCheck: HandlerCheck, + timeout?: HandlerTimeoutOptions, + ): Handler { + const handler: Handler = { handlerCheck }; /* - * If a timeout is provided, the event will remove itself and run its callback function - * when the timeout runs out. If we need to remove the event manually and it has a timeout, - * we will need to call the clearTimeout function on the event's expiry. + * If a timeout is provided, the handler will remove itself and run its callback function + * when the timeout runs out. If we need to remove the handler manually and it has a timeout, + * we will need to call the clearTimeout function on the handler's expiry. */ if (timeout) { - listener.expiry = setTimeout((): void => { - // Deletes the listener. + handler.expiry = setTimeout((): void => { + // Deletes the handler. if (this.eventDictionary[name]) { - this.eventDictionary[name] = (this.eventDictionary[name] as Listener[]) - .filter((lis): boolean => lis !== listener); + this.eventDictionary[name] = (this.eventDictionary[name] as Handler[]) + .filter((lis): boolean => lis !== handler); } timeout.callback(); }, timeout.time); } - // Adds the event to the list with the proper event identifier. + // Adds the handler to the list with the proper event name. if (!this.eventDictionary[name]) { this.eventDictionary[name] = []; } - (this.eventDictionary[name] as Listener[]).push(listener); + (this.eventDictionary[name] as Handler[]).push(handler); - return listener; + return handler; } /** * Processes the event based on the handlers stored. * * @param name The name of the event that occured. - * @param value The value being passed to the listeners of that event. + * @param value The value being passed to the handlers of that event. * @param events All other events that happened at that time. */ public event(name: string, value: T, events?: { [key: string]: any }): void { if (!this.eventDictionary[name]) return; - this.eventDictionary[name] = (this.eventDictionary[name] as Listener[]) + this.eventDictionary[name] = (this.eventDictionary[name] as Handler[]) .filter((lis): boolean => { - const handled = lis.handler(value, events); + const handled = lis.handlerCheck(value, events); + /* - * Removes listener if it has not been triggered yet. Will also clearTimeout - * the expiry if the listener has one so that the timeout will not trigger afterwards. + * Removes handler if it has not been triggered yet. Will also clearTimeout + * the expiry if the handler has one so that the timeout will not trigger afterwards. */ if (handled) { if (lis.expiry) clearTimeout(lis.expiry); @@ -118,19 +118,19 @@ export default class UpdateHandler { } /** - * Remove the listener from the handler. The provided name and listener must be the same + * Remove the handler. The provided name and handler must be the same * as the ones used when creating the handler. * - * @param name The name of the event handler string. - * @param listener The listener itself. + * @param name The name of the event the handler is in. + * @param handler The handler itself. */ - public removeHandler(name: string, listener: Listener): void { - // Will clearTimeout the listener if it has an expiry. - if (listener.expiry) clearTimeout(listener.expiry); + public removeHandler(name: string, handler: Handler): void { + // Will clearTimeout the handler if it has an expiry. + if (handler.expiry) clearTimeout(handler.expiry); - // Removes the listener from the handler. + // Removes the handler from the event's list of handlers. if (!this.eventDictionary[name]) return; - this.eventDictionary[name] = (this.eventDictionary[name] as Listener[]) - .filter((lis): boolean => lis !== listener); + this.eventDictionary[name] = (this.eventDictionary[name] as Handler[]) + .filter((lis): boolean => lis !== handler); } } diff --git a/src/main/index.ts b/src/main/index.ts index 12230c7c..1a583ce9 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -447,7 +447,7 @@ app.on('before-quit', (): void => { quitting = true; }); -ipcMain.on('post', (_: Event, notification: string, data: object): void => { +ipcMain.on('post', (_: Event, notification: string, ...data: any[]): void => { // eslint-disable-line @typescript-eslint/no-explicit-any if (notification === 'showMissionWindow') { showMissionWindow(); } else if (notification === 'hideMissionWindow') { @@ -460,9 +460,9 @@ ipcMain.on('post', (_: Event, notification: string, data: object): void => { * notifications go for which window, but its simpler to have it forwarded to both. */ if (mainWindow) { - mainWindow.webContents.send(notification, data); + mainWindow.webContents.send(notification, ...data); } if (missionWindow) { - missionWindow.webContents.send(notification, data); + missionWindow.webContents.send(notification, ...data); } }); diff --git a/src/renderer/fixtures/updateMessagesFixtures.ts b/src/renderer/fixtures/updateMessagesFixtures.ts index ccf5caf8..6de0ca0c 100644 --- a/src/renderer/fixtures/updateMessagesFixtures.ts +++ b/src/renderer/fixtures/updateMessagesFixtures.ts @@ -26,5 +26,5 @@ const fixtures: { type?: MessageType; message: string }[] = [ */ export default function updateMessages(): void { const fixture = fixtures[Math.floor(Math.random() * fixtures.length)]; - ipcRenderer.send('post', 'updateMessages', [fixture]); + ipcRenderer.send('post', 'updateMessages', fixture); } diff --git a/src/renderer/fixtures/updateVehiclesFixtures.ts b/src/renderer/fixtures/updateVehiclesFixtures.ts index a0a25f13..1b36b531 100644 --- a/src/renderer/fixtures/updateVehiclesFixtures.ts +++ b/src/renderer/fixtures/updateVehiclesFixtures.ts @@ -3,9 +3,11 @@ import { ipcRenderer } from 'electron'; import { vehicleStatuses } from '../../static/index'; // TODO: Remove disable line comment when issue gets fixed (https://github.com/benmosher/eslint-plugin-import/pull/1304) -import { Vehicle, VehicleStatus } from '../../util/types'; // eslint-disable-line import/named +import { VehicleUpdate, VehicleStatus } from '../../util/types'; // eslint-disable-line import/named -let fixtures: Vehicle[] = [ +// TODO: Use Vehicle class to create these. + +let fixtures: VehicleUpdate[] = [ { sid: 100, lat: 34.056482, @@ -32,7 +34,7 @@ const status = Object.keys(vehicleStatuses); * Sends an updateVehicle notification with random vehicle fixtures. */ export default function updateVehicles(): void { - const newFixtures = fixtures.map((fixture): Vehicle => ({ + const newFixtures = fixtures.map((fixture): VehicleUpdate => ({ ...fixture, lat: fixture.lat + (Math.random() / 5000) - 0.0001, lng: fixture.lng + (Math.random() / 5000) - 0.0001, @@ -40,5 +42,5 @@ export default function updateVehicles(): void { })); fixtures = newFixtures; - ipcRenderer.send('post', 'updateVehicles', fixtures); + ipcRenderer.send('post', 'updateVehicles', ...fixtures); } diff --git a/src/renderer/mainWindow/log/LogContainer.tsx b/src/renderer/mainWindow/log/LogContainer.tsx index 28de4266..b8e8e645 100644 --- a/src/renderer/mainWindow/log/LogContainer.tsx +++ b/src/renderer/mainWindow/log/LogContainer.tsx @@ -84,7 +84,7 @@ export default class LogContainer extends Component { } public componentDidMount(): void { - ipcRenderer.on('updateMessages', (_: Event, messages: Message[]): void => this.updateMessages(messages)); + ipcRenderer.on('updateMessages', (_: Event, ...messages: Message[]): void => this.updateMessages(...messages)); } /** @@ -152,7 +152,7 @@ export default class LogContainer extends Component { /** * Updates the messages in the log. Will update filtered messages accordingly. */ - private updateMessages(messages: Message[]): void { + private updateMessages(...messages: Message[]): void { const { filteredMessages, messages: thisMessages, filter } = this.state; const currentMessages = thisMessages; const currentFilteredMessages = filteredMessages; diff --git a/src/renderer/mainWindow/map/MapContainer.tsx b/src/renderer/mainWindow/map/MapContainer.tsx index 34d70be7..6df81c5e 100644 --- a/src/renderer/mainWindow/map/MapContainer.tsx +++ b/src/renderer/mainWindow/map/MapContainer.tsx @@ -10,14 +10,14 @@ import React, { } from 'react'; import { Map, TileLayer, Viewport } from 'react-leaflet'; -import { locations, startLocation } from '../../../static/index'; +import { startLocation } from '../../../static/index'; import { FileLoadOptions, FileSaveOptions, LatLngZoom, ThemeProps, - Vehicle, + VehicleUpdate, VehicleUI, } from '../../../util/types'; import { updateVehicles } from '../../../util/util'; @@ -37,15 +37,6 @@ const mapOptions = { accessToken: process.env.MAPBOX_TOKEN, }; -/* - * Logic to set up the start location for the map. - * Default location is (0, 0) unless there is a locations file defined already. - */ -let start: LatLngZoom = { lat: 0, lng: 0, zoom: 18 }; -if (startLocation && locations[startLocation]) { - start = { ...start, ...locations[startLocation] }; -} - /** * State of the map container. */ @@ -71,10 +62,10 @@ export default class MapContainer extends Component { this.state = { vehicles: {}, - ...start, + ...startLocation, viewport: { - center: [start.lat, start.lng], - zoom: start.zoom, + center: [startLocation.lat, startLocation.lng], + zoom: startLocation.zoom, }, }; @@ -96,7 +87,7 @@ export default class MapContainer extends Component { ipcRenderer.on('centerMapToVehicle', (_: Event, vehicle: VehicleUI): void => this.centerMapToVehicle(vehicle)); ipcRenderer.on('setMapToUserLocation', this.setMapToUserLocation); ipcRenderer.on('updateMapLocation', (_: Event, location: LatLngZoom): void => this.updateMapLocation(location)); - ipcRenderer.on('updateVehicles', (_: Event, vehicles: Vehicle[]): void => updateVehicles(this, vehicles)); + ipcRenderer.on('updateVehicles', (_: Event, ...vehicles: VehicleUpdate[]): void => updateVehicles(this, ...vehicles)); } /** diff --git a/src/renderer/mainWindow/vehicle/VehicleContainer.tsx b/src/renderer/mainWindow/vehicle/VehicleContainer.tsx index 3e922430..e710ae36 100644 --- a/src/renderer/mainWindow/vehicle/VehicleContainer.tsx +++ b/src/renderer/mainWindow/vehicle/VehicleContainer.tsx @@ -4,7 +4,7 @@ import React, { Component, ReactNode } from 'react'; import VehicleTable from './VehicleTable'; import { updateVehicles } from '../../../util/util'; -import { ThemeProps, Vehicle, VehicleUI } from '../../../util/types'; +import { ThemeProps, VehicleUpdate, VehicleUI } from '../../../util/types'; import './vehicle.css'; @@ -31,7 +31,7 @@ export default class VehicleContainer extends Component { } public componentDidMount(): void { - ipcRenderer.on('updateVehicles', (_: Event, data: Vehicle[]): void => updateVehicles(this, data)); + ipcRenderer.on('updateVehicles', (_: Event, ...vehicles: VehicleUpdate[]): void => updateVehicles(this, ...vehicles)); } public render(): ReactNode { diff --git a/src/static/index.ts b/src/static/index.ts index e43c0d8e..cbb48ee7 100644 --- a/src/static/index.ts +++ b/src/static/index.ts @@ -1,31 +1,19 @@ -/* When importing anything inside resources, please import through this file */ - /* eslint-disable @typescript-eslint/camelcase */ -import { fixtures, geolocation } from './config.json'; -import { startLocation, locations as locationsObject } from './locations.json'; -import { - vehicleIds as vehicleIdsObject, - vehicleInfos as vehicleInfosObject, - vehicleStatuses as vehicleStatusesObject, -} from './vehicle.json'; - -import arrow from './images/arrow.png'; -import icon from './images/icon.png'; -import pin from './images/pin.png'; +import { LatLngZoom } from '../util/types'; import ngcp_calpoly from './images/logo/ngcp_calpoly.png'; import ngcp_pomona from './images/logo/ngcp_pomona.png'; -import poi_fp from './images/markers/poi_fp.png'; -import poi_unknwn from './images/markers/poi_unkwn.png'; -import poi_vld from './images/markers/poi_vld.png'; - import plane_red from './images/markers/vehicles/plane_red.png'; import plane from './images/markers/vehicles/plane.png'; import rover_red from './images/markers/vehicles/rover_red.png'; import rover from './images/markers/vehicles/rover.png'; +import poi_fp from './images/markers/poi_fp.png'; +import poi_unknwn from './images/markers/poi_unkwn.png'; +import poi_vld from './images/markers/poi_vld.png'; + import fullmoon_hover from './images/other/fullmoon_hover.png'; import fullmoon from './images/other/fullmoon.png'; import geolocation_img_hover from './images/other/geolocation_hover.png'; @@ -33,28 +21,45 @@ import geolocation_img from './images/other/geolocation.png'; import moon_hover from './images/other/moon_hover.png'; import moon from './images/other/moon.png'; +import arrow from './images/arrow.png'; +import icon from './images/icon.png'; +import pin from './images/pin.png'; + +import { fixtures, geolocation } from './config.json'; +import { startLocation as startLocationString, locations as locationsObject } from './locations.json'; +import { + vehicleDisconnectionTime, + vehicleIds as vehicleIdsObject, + vehicleInfos as vehicleInfosObject, + vehicleStatuses as vehicleStatusesObject, +} from './vehicle.json'; + // Export json objects with signature to allow us to access it with TypeScript. export const locations: { - [name: string]: { lat: number; lng: number; zoom?: number }; + [name: string]: { lat: number; lng: number; zoom?: number } | undefined; } = locationsObject; export const vehicleIds: { - [name: string]: number; + [name: string]: number | undefined; } = vehicleIdsObject; export const vehicleInfos: { - [id: string]: { macAddress: string; name: string; 'type': string }; + [id: string]: { macAddress: string; name: string; 'type': string } | undefined; } = vehicleInfosObject; export const vehicleStatuses: { - [status: string]: { 'type': string; message: string }; + [status: string]: { 'type': string; message: string } | undefined; } = vehicleStatusesObject; -export { - fixtures, - geolocation, - startLocation, -}; +// Add logic to set startLocation. +export const startLocation: LatLngZoom = startLocationString && locations[startLocationString] + ? locations[startLocationString] as LatLngZoom : { + lat: 0, + lng: 0, + zoom: 18, + }; + +export { fixtures, geolocation, vehicleDisconnectionTime }; /** * A given key for an image will give either a string or an object with similar structure. @@ -95,6 +100,7 @@ export default { images, locations, startLocation, + vehicleDisconnectionTime, vehicleIds, vehicleInfos, vehicleStatuses, diff --git a/src/static/vehicle.json b/src/static/vehicle.json index 58496942..7b198906 100644 --- a/src/static/vehicle.json +++ b/src/static/vehicle.json @@ -1,4 +1,5 @@ { + "vehicleDisconnectionTime": 20, "vehicleIds": { "UAV": 100, "UGV": 200, diff --git a/src/util/types.ts b/src/util/types.ts index d96335ba..ed523c80 100644 --- a/src/util/types.ts +++ b/src/util/types.ts @@ -41,9 +41,10 @@ export function isVehicleStatus(status: string): boolean { } /** - * Actual vehicle values. This can change as Orchestrator is re-implemented into TypeScript. + * Values passed on when vehicles send update messaages. + * TODO: Update name to UpdateMessage. Probably add types to other types of messages. */ -export interface Vehicle { +export interface VehicleUpdate { /** * The ID of the vehicle. */ @@ -78,6 +79,10 @@ export interface Vehicle { * Status of the vehicle. Generated by the vehicles themselves or by the Orchestrator. */ status: VehicleStatus; + + /** + * Message generated if vehicle is in an error state. + */ errorMessage?: string; } diff --git a/src/util/util.ts b/src/util/util.ts index d776d64e..4e626821 100644 --- a/src/util/util.ts +++ b/src/util/util.ts @@ -4,23 +4,38 @@ import { Component } from 'react'; import { vehicleInfos, vehicleStatuses } from '../static/index'; // TODO: Remove disable line comment when issue gets fixed (https://github.com/benmosher/eslint-plugin-import/pull/1304) -import { MessageType, Vehicle, VehicleUI } from './types'; // eslint-disable-line import/named +import { MessageType, VehicleUpdate, VehicleUI } from './types'; // eslint-disable-line import/named /** * Updates vehicles being shown. */ export function updateVehicles( - component: Component<{}, { vehicles: { [key: string]: VehicleUI } }>, vehicles: Vehicle[], + component: Component<{}, { vehicles: { [key: string]: VehicleUI } }>, + ...vehicles: VehicleUpdate[] ): void { const { vehicles: thisVehicles } = component.state; const currentVehicles = thisVehicles; vehicles.forEach((vehicle): void => { - currentVehicles[vehicle.sid] = { - ...vehicle, - ...vehicleInfos[vehicle.sid], - status: vehicleStatuses[vehicle.status] as { type: MessageType; message: string }, - }; + /* + * Checks if we have the corresponding information on that vehicle on vehicleInfos. + * If not we will delete the vehicle from currentVehicles (will not throw an error if that + * vehicle was not in the object of vehicles in the first place) and will log to the log + * container. + */ + if (vehicleInfos[vehicle.sid]) { + currentVehicles[vehicle.sid] = { + ...vehicle, + ...vehicleInfos[vehicle.sid] as { name: string; type: string }, + status: vehicleStatuses[vehicle.status] as { type: MessageType; message: string }, + }; + } else { + delete currentVehicles[vehicle.sid]; + ipcRenderer.send('post', 'updateMessage', { + type: 'failure', + message: `Received vehicle ID (${vehicle.sid}) in which no vehicle corresponds to`, + }); + } }); component.setState({ vehicles: currentVehicles }); From ad7f2a8aae08e07ad79b1e33da4cbe5ce2664c83 Mon Sep 17 00:00:00 2001 From: Paul Matthew Barrameda Date: Fri, 5 Apr 2019 02:38:10 -0700 Subject: [PATCH 050/152] Change functions in objects to have arrow notation Functions without arrow notation will struggle to access private/instance variables in classes. Also added a default export to util/util.ts --- src/common/struct/UpdateHandler.ts | 2 +- src/main/index.ts | 14 +++++++------- src/util/util.ts | 7 +++++++ 3 files changed, 15 insertions(+), 8 deletions(-) diff --git a/src/common/struct/UpdateHandler.ts b/src/common/struct/UpdateHandler.ts index dcf78329..7500f5f5 100644 --- a/src/common/struct/UpdateHandler.ts +++ b/src/common/struct/UpdateHandler.ts @@ -9,7 +9,7 @@ interface HandlerTimeoutOptions { /** * Function that will execute once the function times out. */ - callback(): void; + callback: () => void; /** * Amount of time (in milliseconds) for the function to time out. diff --git a/src/main/index.ts b/src/main/index.ts index 1a583ce9..f26c493f 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -72,7 +72,7 @@ let tray: Tray; const quitRole: MenuItemConstructorOptions = { label: 'Quit', accelerator: 'CommandOrControl+Q', - click(): void { + click: (): void => { quitting = true; app.quit(); }, @@ -172,7 +172,7 @@ const menu: MenuItemConstructorOptions[] = [ { label: 'Open File...', accelerator: 'CommandOrControl+O', - click(): void { loadConfig(); }, + click: (): void => { loadConfig(); }, }, { type: 'separator' }, { role: 'close' }, @@ -180,7 +180,7 @@ const menu: MenuItemConstructorOptions[] = [ { label: 'Save As...', accelerator: 'CommandOrControl+S', - click(): void { saveConfig(); }, + click: (): void => { saveConfig(); }, }, ], }, @@ -212,7 +212,7 @@ const menu: MenuItemConstructorOptions[] = [ submenu: [ { label: 'My Location', - click(): void { + click: (): void => { if (mainWindow) { mainWindow.webContents.send('setMapToUserLocation'); } @@ -232,7 +232,7 @@ const menu: MenuItemConstructorOptions[] = [ submenu: [ { label: 'Help', - click(): void { shell.openExternal('https://github.com/NGCP/missioncontrol'); }, + click: (): void => { shell.openExternal('https://github.com/NGCP/missioncontrol'); }, }, ], }, @@ -263,7 +263,7 @@ function setLocationMenu(): void { Object.keys(locations).forEach((label): void => { locationMenu.push({ label, - click(menuItem): void { + click: (menuItem): void => { if (mainWindow) { mainWindow.webContents.send('updateMapLocation', locations[menuItem.label]); } @@ -393,7 +393,7 @@ function showMissionWindow(): void { const trayMenu: MenuItemConstructorOptions[] = [ { label: 'NGCP Ground Control Station', - click(): void { showMainWindow(); }, + click: (): void => { showMainWindow(); }, }, { type: 'separator' }, quitRole, diff --git a/src/util/util.ts b/src/util/util.ts index 4e626821..0c0287ac 100644 --- a/src/util/util.ts +++ b/src/util/util.ts @@ -133,3 +133,10 @@ function toFloat(hexString: string): number { export const floatHex = { toHexString, toFloat }; /* eslint-enable no-bitwise */ + +export default { + floatHex, + job, + mission, + updateVehicles, +}; From 2a18d4c851c5b006d56dd94f80abf36238272b18 Mon Sep 17 00:00:00 2001 From: Paul Matthew Barrameda Date: Fri, 5 Apr 2019 17:18:54 -0700 Subject: [PATCH 051/152] Added Vehicle class Integrated class to the rest of the class. The Vehicle class is created using a message (which has an sid field), which will output a VehicleObject that can be sent to the user interface (through ipcRenderer) and can be used then. Added definitions for Messages and Jobs too. --- src/common/struct/Jobs.ts | 144 ++++++++ src/common/struct/Messages.ts | 149 ++++++++ src/common/struct/Vehicle.ts | 334 ++++++++++++++++++ .../fixtures/updateVehiclesFixtures.ts | 68 ++-- src/renderer/mainWindow/map/MapContainer.tsx | 26 +- src/renderer/mainWindow/map/VehicleMarker.tsx | 43 ++- .../mainWindow/vehicle/VehicleContainer.tsx | 12 +- .../mainWindow/vehicle/VehicleTable.tsx | 36 +- src/static/index.ts | 21 +- src/util/types.ts | 138 +------- src/util/util.ts | 31 +- 11 files changed, 791 insertions(+), 211 deletions(-) create mode 100644 src/common/struct/Jobs.ts create mode 100644 src/common/struct/Messages.ts create mode 100644 src/common/struct/Vehicle.ts diff --git a/src/common/struct/Jobs.ts b/src/common/struct/Jobs.ts new file mode 100644 index 00000000..28aea832 --- /dev/null +++ b/src/common/struct/Jobs.ts @@ -0,0 +1,144 @@ +/* + * https://github.com/NGCP/GCS/wiki/List-of-Job-Types + * + * These definitions will not have the time, sid, tid, and id fields. + */ + +export interface JobBase { + /** + * Name of the job. + */ + jobType: string; + + /** + * Information related to complete the job. + */ + missionInfo: object; +} + +export interface Takeoff extends JobBase { + jobType: 'takeoff'; + missionInfo: { + lat: number; + lng: number; + alt: number; + loiter: { + lat: number; + lng: number; + alt: number; + radius: number; + direction: number; + }; + }; +} + +export interface Loiter extends JobBase { + jobType: 'loiter'; + missionInfo: { + lat: number; + lng: number; + alt: number; + radius: number; + direction: number; + }; +} + +// eslint-disable-next-line @typescript-eslint/interface-name-prefix +export interface ISRSearch extends JobBase { + jobType: 'isrSearch'; + missionInfo: { + alt: number; + waypoints: [ + { + lat: number; + lng: number; + }, + { + lat: number; + lng: number; + }, + { + lat: number; + lng: number; + } + ]; + }; +} + +export interface PayloadDrop extends JobBase { + jobType: 'payloadDrop'; + missionInfo: { + waypoints: [ + { + lat: number; + lng: number; + alt: number; + }, + { + lat: number; + lng: number; + alt: number; + } + ]; + }; +} + +export interface Land extends JobBase { + jobType: 'land'; + missionInfo: { + waypoints: [ + { + lat: number; + lng: number; + alt: number; + }, + { + lat: number; + lng: number; + alt: number; + } + ]; + }; +} + +export interface RetrieveTargetUGV extends JobBase { + jobType: 'retrieveTarget'; + missionInfo: { + lat: number; + lng: number; + }; +} + +export interface DeliverTarget extends JobBase { + jobType: 'deliverTarget'; + missionInfo: { + lat: number; + lng: number; + }; +} + +export interface RetrieveTargetUUV extends JobBase { + jobType: 'retrieveTarget'; + missionINfo: {}; +} + +export interface QuickScan extends JobBase { + jobType: 'quickScan'; + missionInfo: { + lat: number; + lng: number; + innerRadius: number; + outerRadius: number; + }; +} + +export interface DetailedSearch extends JobBase { + jobType: 'detailedSearch'; + missionInfo: { + lat: number; + lng: number; + }; +} + +export type Job = Takeoff | Loiter | ISRSearch | PayloadDrop | Land | RetrieveTargetUGV +| DeliverTarget | RetrieveTargetUUV | QuickScan | DetailedSearch; diff --git a/src/common/struct/Messages.ts b/src/common/struct/Messages.ts new file mode 100644 index 00000000..2767b6e3 --- /dev/null +++ b/src/common/struct/Messages.ts @@ -0,0 +1,149 @@ +/* + * Definitions for all messages that are sent between GCS and vehicles. See the wiki for more + * information: https://github.com/NGCP/GCS/wiki/GCS-JSON-Messages + * + * These definitions will not have the time, sid, tid, and id fields. + */ + +// TODO: Remove disable line comment when issue gets fixed (https://github.com/benmosher/eslint-plugin-import/pull/1304) +import { VehicleStatus } from '../../util/types'; // eslint-disable-line import/named + +// From GCS to vehicles. + +interface MessageBase { + /** + * Type of message. + */ + type: string; +} + +export interface StartMessage extends MessageBase { + type: 'start'; + + /** + * Name of job to perform. + */ + jobType: string; + + /** + * Any other information the vehicle should have before performing the job. + */ + options?: object; +} + +export interface AddMissionMessage extends MessageBase { + type: 'addMission'; + + /** + * Information related to accomplishing specific job. + */ + missionInfo: object; +} + +export interface PauseMessage extends MessageBase { + type: 'pause'; +} + +export interface ResumeMessage extends MessageBase { + type: 'resume'; +} + +export interface StopMessage extends MessageBase { + type: 'stop'; +} + +export interface ConnectionAckMessage extends MessageBase { + type: 'connectionAck'; +} + +// From vehicles to GCS. + +export interface UpdateMessage extends MessageBase { + type: 'update'; + + /** + * Latitude of the vehicle. + */ + lat: number; + + /** + * Longitude of the vehicle. + */ + lng: number; + + /** + * Altitude of the vehicle. + */ + alt?: number; + + /** + * Current vehicle heading. Value is in degrees. + */ + heading?: number; + + /** + * Current battery of the vehicle, expressed as a decimal. Will vary from 0 to 1. + */ + battery?: number; + + /** + * Status of the vehicle. + */ + status: VehicleStatus; + + /** + * Message generated if vehicle is in an error state. + */ + errorMessage?: string; +} + +export interface POIMessage extends MessageBase { + type: 'poi'; + + /** + * Latitude of the point of interest. + */ + lat: number; + + /** + * Longitude of the point of interest. + */ + lng: number; +} + +export interface CompleteMessage extends MessageBase { + type: 'complete'; +} + +export interface ConnectionMessage extends MessageBase { + type: 'connect'; + + /** + * List of all the different jobs the vehicle is capable of doing. + */ + jobsAvailable: string[]; +} + +// Other message types. + +export interface AckMessage extends MessageBase { + type: 'ack'; + + /** + * ID of the message that is being acknowledged. + */ + ackid: number; +} + +export interface BadMessageMessage extends MessageBase { + type: 'badMessage'; + + /** + * Description of why message was bad. + */ + error: string; +} + +export type Message = StartMessage | AddMissionMessage | PauseMessage | ResumeMessage | StopMessage +| ConnectionAckMessage | UpdateMessage | POIMessage | CompleteMessage | ConnectionMessage +| AckMessage | BadMessageMessage; diff --git a/src/common/struct/Vehicle.ts b/src/common/struct/Vehicle.ts new file mode 100644 index 00000000..e2436845 --- /dev/null +++ b/src/common/struct/Vehicle.ts @@ -0,0 +1,334 @@ +import { ipcRenderer } from 'electron'; + +import { vehicleInfos } from '../../static/index'; + +// TODO: Remove disable line comment when issue gets fixed (https://github.com/benmosher/eslint-plugin-import/pull/1304) +import { VehicleStatus } from '../../util/types'; // eslint-disable-line import/named + +import { Job } from './Jobs'; // eslint-disable-line import/named +import { Message, StartMessage, UpdateMessage } from './Messages'; // eslint-disable-line import/named +import UpdateHandler from './UpdateHandler'; + +type ErrorCallback = (message?: string) => void; + +/** + * Options to initialize a vehicle. + */ +export interface VehicleOptions { + sid: number; + jobs: string[]; + status?: VehicleStatus; +} + +/** + * Publicly accessible object that contains information about the vehicle. Modifying + * information of this object will not modify the actual vehicle class. + */ +export interface VehicleObject { + /** + * ID of the vehicle. + */ + vehicleId: number; + + /** + * Current status of the vehicle. + */ + status: VehicleStatus; + + /** + * Jobs that the vehicle can perform. + */ + jobs: string[]; + + /** + * Current latitude of the vehicle. Starts at 0. + */ + lat: number; + + /** + * Current longitude of the vehicle. Starts at 0. + */ + lng: number; + + /** + * Current altitude of the vehicle. + */ + alt?: number; + /** + * + * Current battery of the vehicle, expressed as a decimal. Will vary from 0 to 1. + */ + battery?: number; + + /** + * Current vehicle heading. Value is in degrees. + */ + heading?: number; +} + +/** + * Contains data held by a vehicle as well as functions used by the vehicle. + */ +export default class Vehicle { + /** + * ID of the vehicle. + */ + private vehicleId: number; + + /** + * Currently assigned job. + */ + private assignedJob: string = ''; + + /** + * Whether or not the vehicle is currently in the state of being assigned a job. + * When the vehicle is assigned a job, it will be waiting for job info until it gets that. + * During that time, this variable will be true. False otherwise. + */ + private waitingForJobInfo: boolean = false; + + /** + * Current status of the vehicle. + */ + private status: VehicleStatus = 'disconnected'; + + /** + * Jobs that the vehicle can perform. + */ + private jobs: string[]; + + /** + * Current latitude of the vehicle. Starts at 0. + */ + private lat = 0; + + /** + * Current longitude of the vehicle. Starts at 0. + */ + private lng = 0; + + /** + * Current altitude of the vehicle. + */ + private alt?: number; + + /** + * Current battery of the vehicle, expressed as a decimal. Will vary from 0 to 1. + */ + private battery?: number; + + /** + * Current vehicle heading. Value is in degrees. + */ + private heading?: number; + + /** + * Callback to when the vehicle enters the error state. + */ + private errorCallback: ErrorCallback = (): void => {}; + + /** + * Handler that listens for different events from the vehicle connected. + */ + private updateEventHandler = new UpdateHandler(); + + /** + * Last time that GCS received any message from the vehicle. + * Time is in number of milliseconds since Epoch. + */ + private lastConnectionTime = Date.now(); + + public constructor(options: VehicleOptions) { + this.vehicleId = options.sid; + this.jobs = options.jobs; + + if (options.status) this.status = options.status; + + // We never want these handlers to disappear so always return false on their callback functions. + + this.updateEventHandler.addHandler('status', (status, message): boolean => { + this.status = status; + if (status === 'error') { + // Calls the errorCallback function and provides errorMessage to it. + this.errorCallback(message && message.errorMessage); + } + return false; + }); + + this.updateEventHandler.addHandler('lat', (lat): boolean => { + this.lat = lat; + return false; + }); + + this.updateEventHandler.addHandler('lng', (lng): boolean => { + this.lng = lng; + return false; + }); + + this.updateEventHandler.addHandler('alt', (alt): boolean => { + this.alt = alt; + return false; + }); + + this.updateEventHandler.addHandler('battery', (battery, message): boolean => { + if (battery > 1) { + const vehicleInfo = message && message.sid && vehicleInfos[message.sid]; + + ipcRenderer.send('post', 'updateMessages', { + type: 'failure', + message: `Received a battery status of more than 100% (${battery * 100}%) from ${(vehicleInfo && vehicleInfo.name) || 'an unknown vehicle'}`, + }); + } else { + this.battery = battery; + } + return false; + }); + + this.updateEventHandler.addHandler('heading', (heading): boolean => { + this.heading = heading; + return false; + }); + } + + public getVehicleId(): number { return this.vehicleId; } + + public getStatus(): VehicleStatus { return this.status; } + + public getJobs(): string[] { return this.jobs; } + + public getLat(): number { return this.lat; } + + public getLng(): number { return this.lng; } + + public getAlt(): number | undefined { return this.alt; } + + public getBattery(): number | undefined { return this.battery; } + + public getHeading(): number | undefined { return this.heading; } + + public getLastConnectionTime(): number { return this.lastConnectionTime; } + + public getUpdateEventHandler(): UpdateHandler { return this.updateEventHandler; } + + /** + * Converts vehicle to a plain object so that its private variables can be read + * when it is sent through ipcRenderer. + */ + public toPlainObject(): VehicleObject { + return { + vehicleId: this.vehicleId, + status: this.status, + jobs: this.jobs, + lat: this.lat, + lng: this.lng, + alt: this.alt, + battery: this.battery, + heading: this.heading, + }; + } + + /** + * Updates all variables in this vehicle to the variables in the message. Called + * by the Orchestrator when the GCS receives an update message from the vehicle. + * + * @param message The message from the vehicle itself. + */ + public update(message: UpdateMessage): void { + this.updateEventHandler.events(message); + } + + /** + * Sets lastConnectionTime to current time when function is called. Called + * by the Orchestrator whenever GCS receives a message from the vehicle. + */ + public updateLastConnectionTime(): void { + this.lastConnectionTime = Date.now(); + } + + /** + * Assigns a job to the vehicle. + */ + public assignJob( + job: string, + options?: object, + completionCallback?: () => void, + disconnectionCallback?: () => void, + errorCallback?: ErrorCallback, + ): void { + if (this.waitingForJobInfo) { + throw new Error('Vehicle has already been assigned a job and is waiting for info.'); + } + + this.assignedJob = job; + this.waitingForJobInfo = true; + if (errorCallback) this.errorCallback = errorCallback; + + const startMessage: StartMessage = { + type: 'start', + jobType: this.assignedJob, + }; + + if (options) startMessage.options = options; + + // Sends the start message to the vehicle with corresponding job name. + this.sendMessage({ + type: 'start', + jobType: this.assignedJob, + }); + + /* + * Create handler that will call completion callback and when vehicle goes back to + * "ready" status. + * + * Vehicle goes back to "ready" status once it either finishes a job or gets its job done and + * is ready for another job to be assigned. + * + * There will be a callback that can be provided in the case when the vehicle is in a state + * other than "ready" and it gets disconnected. We do not need to worry about a disconnection + * callback when the vehicle is "ready" since it is in a stable state. + */ + this.updateEventHandler.addHandler('status', (value): boolean => { + if (value === 'ready') { + if (completionCallback) completionCallback(); + this.waitingForJobInfo = false; + } else if (value === 'disconnected') { + if (disconnectionCallback) disconnectionCallback(); + } + return value === 'ready'; + }); + } + + /** + * Starts the vehicle's job by giving the vehicle information. This will be called by the + * Orchestrator when the user interface provides it with information for the job. + */ + public startJob(job: Job): void { + if (!this.waitingForJobInfo) { + throw new Error('Vehicle has not been assigned a job yet. Assign the job before telling the vehicle to start.'); + } + + this.sendMessage({ + type: 'addMission', + missionInfo: job.missionInfo, + }); + } + + /** + * Sends stop message to vehicle. + */ + public stop(): void { + this.sendMessage({ + type: 'stop', + }); + + this.assignedJob = ''; + } + + // TODO: Make this function and get it to send message using Xbee. + private sendMessage(message: Message): void { // eslint-disable-line + /* + * Xbee.send(this.vehicleId, message); or use MAC address, probably better to get this from + * Xbee class though + */ + } +} diff --git a/src/renderer/fixtures/updateVehiclesFixtures.ts b/src/renderer/fixtures/updateVehiclesFixtures.ts index 1b36b531..2a145555 100644 --- a/src/renderer/fixtures/updateVehiclesFixtures.ts +++ b/src/renderer/fixtures/updateVehiclesFixtures.ts @@ -1,46 +1,68 @@ import { ipcRenderer } from 'electron'; -import { vehicleStatuses } from '../../static/index'; +import Vehicle, { VehicleObject, VehicleOptions } from '../../common/struct/Vehicle'; -// TODO: Remove disable line comment when issue gets fixed (https://github.com/benmosher/eslint-plugin-import/pull/1304) -import { VehicleUpdate, VehicleStatus } from '../../util/types'; // eslint-disable-line import/named +import { startLocation, vehicleStatuses } from '../../static/index'; -// TODO: Use Vehicle class to create these. +// TODO: Remove disable line comment when issue gets fixed (https://github.com/benmosher/eslint-plugin-import/pull/1304) +import { VehicleStatus } from '../../util/types'; // eslint-disable-line import/named -let fixtures: VehicleUpdate[] = [ +const fixtureOptions: VehicleOptions[] = [ { sid: 100, - lat: 34.056482, - lng: -117.823912, - status: 'disconnected', + jobs: ['takeoff', 'loiter', 'isrSearch', 'payloadDrop', 'land'], + }, + { + sid: 200, + jobs: ['retrieveTarget', 'deliverTarget'], + }, + { + sid: 300, + jobs: ['retrieveTarget'], }, { sid: 400, - lat: 34.053095, - lng: -117.821970, - status: 'disconnected', + jobs: ['quickScan'], }, { - sid: 200, - lat: 34.053509, - lng: -117.818452, - status: 'disconnected', + sid: 401, + jobs: ['detailedSearch'], + }, + { + sid: 500, + jobs: [], + }, + { + sid: 600, + jobs: [], }, ]; const status = Object.keys(vehicleStatuses); +const vehicles = fixtureOptions.map((options): Vehicle => new Vehicle(options)); +vehicles.forEach((vehicle): void => { + vehicle.getUpdateEventHandler().events({ + lat: startLocation.lat + (Math.random() / 1000) - 0.0005, + lng: startLocation.lng + (Math.random() / 1000) - 0.0005, + }); +}); + +const fixtures: VehicleObject[] = []; + /** * Sends an updateVehicle notification with random vehicle fixtures. */ export default function updateVehicles(): void { - const newFixtures = fixtures.map((fixture): VehicleUpdate => ({ - ...fixture, - lat: fixture.lat + (Math.random() / 5000) - 0.0001, - lng: fixture.lng + (Math.random() / 5000) - 0.0001, - status: status[Math.floor(Math.random() * status.length)] as VehicleStatus, - })); - - fixtures = newFixtures; + for (let i = 0; i < vehicles.length; i += 1) { + vehicles[i].getUpdateEventHandler().events({ + lat: vehicles[i].getLat() + (Math.random() / 5000) - 0.0001, + lng: vehicles[i].getLng() + (Math.random() / 5000) - 0.0001, + status: status[Math.floor(Math.random() * status.length)] as VehicleStatus, + }); + + fixtures[i] = vehicles[i].toPlainObject(); + } + ipcRenderer.send('post', 'updateVehicles', ...fixtures); } diff --git a/src/renderer/mainWindow/map/MapContainer.tsx b/src/renderer/mainWindow/map/MapContainer.tsx index 6df81c5e..dc3f6c1d 100644 --- a/src/renderer/mainWindow/map/MapContainer.tsx +++ b/src/renderer/mainWindow/map/MapContainer.tsx @@ -10,6 +10,8 @@ import React, { } from 'react'; import { Map, TileLayer, Viewport } from 'react-leaflet'; +import { VehicleObject } from '../../../common/struct/Vehicle'; + import { startLocation } from '../../../static/index'; import { @@ -17,8 +19,6 @@ import { FileSaveOptions, LatLngZoom, ThemeProps, - VehicleUpdate, - VehicleUI, } from '../../../util/types'; import { updateVehicles } from '../../../util/util'; @@ -44,7 +44,7 @@ interface State extends LatLngZoom { /** * Object of vehicles displayed in the user interface. */ - vehicles: { [sid: string]: VehicleUI }; + vehicles: { [vehicleId: string]: VehicleObject }; /** * Viewport storing locations on location of map. This is more precise than @@ -84,10 +84,10 @@ export default class MapContainer extends Component { ipcRenderer.on('loadConfig', (_: Event, data: FileLoadOptions): void => this.loadConfig(data)); ipcRenderer.on('saveConfig', (_: Event, data: FileSaveOptions): void => this.saveConfig(data)); - ipcRenderer.on('centerMapToVehicle', (_: Event, vehicle: VehicleUI): void => this.centerMapToVehicle(vehicle)); + ipcRenderer.on('centerMapToVehicle', (_: Event, vehicle: VehicleObject): void => this.centerMapToVehicle(vehicle)); ipcRenderer.on('setMapToUserLocation', this.setMapToUserLocation); ipcRenderer.on('updateMapLocation', (_: Event, location: LatLngZoom): void => this.updateMapLocation(location)); - ipcRenderer.on('updateVehicles', (_: Event, ...vehicles: VehicleUpdate[]): void => updateVehicles(this, ...vehicles)); + ipcRenderer.on('updateVehicles', (_: Event, ...vehicles: VehicleObject[]): void => updateVehicles(this, ...vehicles)); } /** @@ -143,7 +143,7 @@ export default class MapContainer extends Component { /** * Centers map around a certain vehicle. */ - private centerMapToVehicle(vehicle: VehicleUI): void { + private centerMapToVehicle(vehicle: VehicleObject): void { this.updateMapLocation(vehicle); } @@ -171,12 +171,14 @@ export default class MapContainer extends Component { const { theme } = this.props; const { viewport, vehicles } = this.state; - const markers = Object.keys(vehicles).map((sid): ReactNode => ( - - )); + const markers = Object + .keys(vehicles) + .map((vehicleId): ReactNode => ( + + )); return ( { public render(): ReactNode { + const { vehicle } = this.props; + const { name, type } = vehicleInfos[vehicle.vehicleId] as VehicleInfo; + const { - sid, - name, - lat, - lng, - type, - status, - } = this.props; + type: vehicleStatusTypeString, + message, + } = vehicleStatuses[vehicle.status] as VehicleStatus; + const vehicleStatusType = vehicleStatusTypeString as MessageType; + const vehicleType = `${type}${vehicleStatusType === 'failure' ? '_red' : ''}`; return ( { direction="top" offset={[0, -20]} > -

{`#${sid}: ${name}`}

+

{`#${vehicle.vehicleId}: ${name}`}

{'Status: '} - {status.message} + {message}

diff --git a/src/renderer/mainWindow/vehicle/VehicleContainer.tsx b/src/renderer/mainWindow/vehicle/VehicleContainer.tsx index e710ae36..c34c7a2f 100644 --- a/src/renderer/mainWindow/vehicle/VehicleContainer.tsx +++ b/src/renderer/mainWindow/vehicle/VehicleContainer.tsx @@ -3,8 +3,10 @@ import React, { Component, ReactNode } from 'react'; import VehicleTable from './VehicleTable'; +import { VehicleObject } from '../../../common/struct/Vehicle'; + +import { ThemeProps } from '../../../util/types'; import { updateVehicles } from '../../../util/util'; -import { ThemeProps, VehicleUpdate, VehicleUI } from '../../../util/types'; import './vehicle.css'; @@ -15,7 +17,7 @@ interface State { /** * Object of vehicles. */ - vehicles: { [sid: string]: VehicleUI }; + vehicles: { [vehicleId: string]: VehicleObject }; } /** @@ -31,7 +33,7 @@ export default class VehicleContainer extends Component { } public componentDidMount(): void { - ipcRenderer.on('updateVehicles', (_: Event, ...vehicles: VehicleUpdate[]): void => updateVehicles(this, ...vehicles)); + ipcRenderer.on('updateVehicles', (_: Event, ...vehicles: VehicleObject[]): void => updateVehicles(this, ...vehicles)); } public render(): ReactNode { @@ -39,12 +41,12 @@ export default class VehicleContainer extends Component { const { vehicles } = this.state; /* - * Put the sids of the vehicles in order (least to greatest) and maps those sids to their + * Put the ids of the vehicles in order (least to greatest) and maps those ids to their * respective values in the vehicles object. Time complexity is O(nlogn). */ const vehicleArray = Object.keys(vehicles) .sort((a, b): number => parseInt(a, 10) - parseInt(b, 10)) - .map((sid): VehicleUI => vehicles[sid]); + .map((vehicleId): VehicleObject => vehicles[vehicleId]); return (
diff --git a/src/renderer/mainWindow/vehicle/VehicleTable.tsx b/src/renderer/mainWindow/vehicle/VehicleTable.tsx index 052cf897..043b56e7 100644 --- a/src/renderer/mainWindow/vehicle/VehicleTable.tsx +++ b/src/renderer/mainWindow/vehicle/VehicleTable.tsx @@ -9,7 +9,16 @@ import { TableCellProps, } from 'react-virtualized'; -import { ThemeProps, VehicleUI } from '../../../util/types'; +import { VehicleObject } from '../../../common/struct/Vehicle'; + +import { + VehicleInfo, + vehicleInfos, + VehicleStatus, + vehicleStatuses, +} from '../../../static/index'; + +import { ThemeProps } from '../../../util/types'; interface WidthSignature { [column: string]: number; @@ -19,7 +28,7 @@ interface WidthSignature { * Width for columns in table. */ const width: WidthSignature = { - id: 0.2, + vehicleId: 0.2, name: 0.4, status: 0.4, }; @@ -31,19 +40,29 @@ export interface VehicleTableProps extends ThemeProps { /** * Object of vehicles. */ - vehicles: VehicleUI[]; + vehicles: VehicleObject[]; } /** * Table to render all vehicles in the vehicle container. */ export default class VehicleTable extends Component { + /** + * Renderer for the status column. + */ + private static nameRenderer(props: TableCellProps): string { + const { rowData: vehicle }: { rowData: VehicleObject } = props; + const { name } = vehicleInfos[vehicle.vehicleId] as VehicleInfo; + return name; + } + /** * Renderer for the status column. */ private static statusRenderer(props: TableCellProps): ReactNode { - const { rowData } = props; - return {rowData.status.message}; + const { rowData }: { rowData: VehicleObject } = props; + const vehicleStatus = vehicleStatuses[rowData.status] as VehicleStatus; + return {vehicleStatus.message}; } public constructor(props: VehicleTableProps) { @@ -72,7 +91,7 @@ export default class VehicleTable extends Component { /** * Callback to obtain data for a certain row in the table. */ - private rowGetter({ index }: Index): VehicleUI { + private rowGetter({ index }: Index): VehicleObject { const { vehicles } = this.props; return vehicles[index]; } @@ -110,13 +129,14 @@ export default class VehicleTable extends Component { > , - ...vehicles: VehicleUpdate[] + component: Component<{}, { vehicles: { [key: string]: VehicleObject } }>, + ...vehicles: VehicleObject[] ): void { const { vehicles: thisVehicles } = component.state; const currentVehicles = thisVehicles; vehicles.forEach((vehicle): void => { - /* - * Checks if we have the corresponding information on that vehicle on vehicleInfos. - * If not we will delete the vehicle from currentVehicles (will not throw an error if that - * vehicle was not in the object of vehicles in the first place) and will log to the log - * container. - */ - if (vehicleInfos[vehicle.sid]) { - currentVehicles[vehicle.sid] = { - ...vehicle, - ...vehicleInfos[vehicle.sid] as { name: string; type: string }, - status: vehicleStatuses[vehicle.status] as { type: MessageType; message: string }, - }; - } else { - delete currentVehicles[vehicle.sid]; - ipcRenderer.send('post', 'updateMessage', { - type: 'failure', - message: `Received vehicle ID (${vehicle.sid}) in which no vehicle corresponds to`, - }); - } + currentVehicles[vehicle.vehicleId] = vehicle; }); component.setState({ vehicles: currentVehicles }); From bf07c5a02983d035a613a12cf1e13276205d5e94 Mon Sep 17 00:00:00 2001 From: Paul Matthew Barrameda Date: Fri, 5 Apr 2019 21:49:58 -0700 Subject: [PATCH 052/152] Added typings for XbeeAPI Help my soul --- typings/react-leaflet-control.d.ts | 2 +- typings/xbee-api.d.ts | 726 +++++++++++++++++++++++++++++ 2 files changed, 727 insertions(+), 1 deletion(-) create mode 100644 typings/xbee-api.d.ts diff --git a/typings/react-leaflet-control.d.ts b/typings/react-leaflet-control.d.ts index 53cb2319..f9c4e79b 100644 --- a/typings/react-leaflet-control.d.ts +++ b/typings/react-leaflet-control.d.ts @@ -1,5 +1,5 @@ /* - * Allows us to use react-leaflet-control + * Allows us to use react-leaflet-control. * TODO: Add typings to this (and perhaps contribute it to the original repo). */ declare module 'react-leaflet-control'; diff --git a/typings/xbee-api.d.ts b/typings/xbee-api.d.ts new file mode 100644 index 00000000..4acb2b00 --- /dev/null +++ b/typings/xbee-api.d.ts @@ -0,0 +1,726 @@ +/* eslint-disable import/prefer-default-export, @typescript-eslint/class-name-casing, max-len, camelcase, @typescript-eslint/camelcase, @typescript-eslint/no-explicit-any */ + +/* eslint-enable max-len */ +/* + * Allows us to use xbee-api as the original api has no typings. + */ +declare module 'xbee-api' { + import BufferReader from 'buffer-reader'; + import { Buffer } from 'safe-buffer'; + import { Transform } from 'stream'; + + type START_BYTE = number; + type ESCAPE = number; + type XOFF = number; + type XON = number; + + export interface Frame { + addresses?: number[]; + analogSamples?: object; + broadcastRadius?: number; + clusterId?: string; + command?: string; + commandParameter?: any; + commandStatus?: string; + data?: any; + deliveryStatus?: number; + destination16?: string; + destination64?: string; + deviceType?: number; + digiManufacturerID?: string; + digiProfileID?: string; + digitalSamples?: object; + discoveryStatus?: number; + destinationEndpoint?: string; + hopCount?: number; + id?: number; + modemStatus?: number; + nodeIdentifier?: string; + numSamples?: number; + options?: number; + profileId?: string; + remote16?: string; + remote64?: string; + remoteParent16?: string; + receiveOptions?: number; + remoteCommandOptions?: number; + rssi?: number; + sender16?: string; + sender64?: string; + sensors?: number; + sensorValues?: { + AD0: number; + AD1: number; + AD2: number; + AD3: number; + T: number; + temperature?: number; + relativeHumidity?: number; + trueHumidity?: number; + waterPresent: boolean; + }; + sourceEndpoint?: string; + sourceEvent?: number; + transmitRetryCount?: number; + } + + export interface XbeeAPIOptions { + raw_frames?: boolean; + api_mode?: 1 | 2; + module?: '802.15.4' | 'ZNet' | 'ZigBee' | 'Any'; + convert_adc?: boolean; + vref_adc?: number; + parser_buffer_size?: number; + builder_buffer_size?: number; + } + + export class XbeeAPI { + private options: XbeeAPIOptions; + + private parseState: { + buffer: Buffer; + offset: number; + length: number; + total: number; + checksum: number; + b: number; + escape_next: boolean; + waiting: boolean; + }; + + /** + * Builder transform stream of the Xbee. + */ + public builder: Transform; + + /** + * Parser transform stream of the Xbee. + */ + public parser: Transform; + + public constructor(options?: XbeeAPIOptions); + + private escape(buffer: Buffer): Buffer; + + /** + * Returns an API frame (buffer) created from the passed frame object. See Creating frames + * from objects to write to the XBee for details on how these passed objects are specified. + */ + public buildFrame(frame: Frame): Buffer; + + /** + * Parses and returns a frame object from the buffer passed. Note that the buffer must be + * a complete frame, starting with the start byte and ending with the checksum byte. See + * Objects created from received API Frames for details on how the retured objects are + * specified. + */ + public parseFrame(rawFrame: Buffer): Frame; + + /** + * Returns whether the library implements a parser for the frame contained in the provided + * buffer. The buffer only needs to contain up to the frame type segment to determine if it + * can be parsed, but `parseFrame()` will need a complete frame. + */ + public canParse(buffer: Buffer): boolean; + + private canBuild(type: string): boolean; + + public nextFrameId(): number; + + /** + * Returns a parser function with the profile `function(emitter, buffer) {}`. This can be + * passed to a serial reader such as serialport. Note that XBeeAPI will not use the + * emitter to emit a parsed frame, but it's own emitter + */ + public rawParser(): (emitter: any, buffer: Buffer) => void; + + private newStream(): any; + + /** + * Parses data in the buffer, assumes it is comming directly from the XBee. If a complete + * frame is collected, it is emitted as Event: 'frame_object'. + */ + public parseRaw(buffer: Buffer, enc?: any, cb?: () => void): void; + } + + export interface constants { + START_BYTE: START_BYTE; + ESCAPE: ESCAPE; + XOFF: XOFF; + XON: XON; + ESCAPE_WITH: number; + + UNKNOWN_16: [number, number]; + UNKNOWN_64: [number, number, number, number, number, number, number, number]; + BROADCAST_16_XB: [number, number]; + COORDINATOR_16: [number, number]; + COORDINATOR_64: [number, number, number, number, number, number, number, number]; + + ESCAPE_BYTES: [ + START_BYTE, + ESCAPE, + XOFF, + XON + ]; + + FRAME_TYPE: { + AT_COMMAND: number; + 0x08: string; + AT_COMMAND_QUEUE_PARAMETER_VALUE: number; + 0x09: string; + ZIGBEE_TRANSMIT_REQUEST: number; + 0x10: string; + EXPLICIT_ADDRESSING_ZIGBEE_COMMAND_FRAME: number; + 0x11: string; + REMOTE_AT_COMMAND_REQUEST: number; + 0x17: string; + CREATE_SOURCE_ROUTE: number; + 0x21: string; + REGISTER_JOINING_DEVICE: number; + 0x24: string; + AT_COMMAND_RESPONSE: number; + 0x88: string; + MODEM_STATUS: number; + 0x8A: string; + ZIGBEE_TRANSMIT_STATUS: number; + 0x8B: string; + ZIGBEE_RECEIVE_PACKET: number; + 0x90: string; + ZIGBEE_EXPLICIT_RX: number; + 0x91: string; + ZIGBEE_IO_DATA_SAMPLE_RX: number; + 0x92: string; + XBEE_SENSOR_READ: number; + 0x94: string; + NODE_IDENTIFICATION: number; + 0x95: string; + REMOTE_COMMAND_RESPONSE: number; + 0x97: string; + OTA_FIRMWARE_UPDATE_STATUS: number; + 0xA0: string; + ROUTE_RECORD: number; + 0xA1: string; + DEVICE_AUTHENITCATED_INDICATOR: number; + 0xA2: string; + MTO_ROUTE_REQUEST: number; + 0xA3: string; + REGISTER_JOINING_DEVICE_STATUS: number; + 0xA4: string; + JOIN_NOTIFICATION_STATUS: number; + 0xA5: string; + + // Series 1/802.15.4 Support + TX_REQUEST_64: number; + 0x00: string; + TX_REQUEST_16: number; + 0x01: string; + TX_STATUS: number; + 0x89: string; + RX_PACKET_64: number; + 0x80: string; + RX_PACKET_16: number; + 0x81: string; + RX_PACKET_64_IO: number; + 0x82: string; + RX_PACKET_16_IO: number; + 0x83: string; + }; + + DISCOVERY_STATUS: { + NO_DISCOVERY_OVERHEAD: number; + 0x00: string; + ADDRESS_DISCOVERY: number; + 0x01: string; + ROUTE_DISCOVERY: number; + 0x02: string; + ADDRESS_AND_ROUTE_DISCOVERY: number; + 0x03: string; + EXTENDED_TIMEOUT_DISCOVERY: number; + 0x40: string; + }; + + DELIVERY_STATUS: { + SUCCESS: number; + 0x00: string; + MAC_ACK_FALIURE: number; + 0x01: string; + CA_FAILURE: number; + 0x02: string; + INVALID_DESTINATION_ENDPOINT: number; + 0x15: string; + NETWORK_ACK_FAILURE: number; + 0x21: string; + NOT_JOINED_TO_NETWORK: number; + 0x22: string; + SELF_ADDRESSED: number; + 0x23: string; + ADDRESS_NOT_FOUND: number; + 0x24: string; + ROUTE_NOT_FOUND: number; + 0x25: string; + BROADCAST_SOURCE_FAILED: number; + 0x26: string; + INVALID_BINDING_TABLE_INDEX: number; + 0x2B: string; + RESOURCE_ERROR: number; + 0x2C: string; + ATTEMPTED_BROADCAST_WITH_APS_TRANS: number; + 0x2D: string; + ATTEMPTED_BROADCAST_WITH_APS_TRANS_EE0: number; + 0x2E: string; + RESOURCE_ERROR_B: number; + 0x32: string; + DATA_PAYLOAD_TOO_LARGE: number; + 0x74: string; + INDIRECT_MESSAGE_UNREQUESTED: number; + 0x75: string; + }; + + COMMAND_STATUS: { + OK: number; + 0x00: string; + ERROR: number; + 0x01: string; + INVALID_COMMAND: number; + 0x02: string; + INVALID_PARAMETER: number; + 0x03: string; + REMOTE_CMD_TRANS_FAILURE: number; + 0x04: string; + }; + + MODEM_STATUS: { + HARDWARE_RESET: number; + 0x00: string; + WATCHDOG_RESET: number; + 0x01: string; + JOINED_NETWORK: number; + 0x02: string; + DISASSOCIATED: number; + 0x03: string; + COORDINATOR_STARTED: number; + 0x06: string; + SECURITY_KEY_UPDATED: number; + 0x07: string; + VOLTAGE_SUPPLY_LIMIT_EXCEEDED: number; + 0x0D: string; + CONFIGURATION_CHANGED_DURING_JOIN: number; + 0x11: string; + STACK_ERROR: number; + 0x80: string; + }; + + RECEIVE_OPTIONS: { + PACKET_ACKNOWLEDGED: number; + 0x01: string; + PACKET_WAS_BROADCAST: number; + 0x02: string; + PACKET_ENCRYPTED: number; + 0x20: string; + PACKET_SENT_FROM_END_DEVICE: number; + 0x40: string; + }; + + DEVICE_TYPE: { + COORDINATOR: number; + 0x00: string; + ROUTER: number; + 0x01: string; + END_DEVICE: number; + 0x02: string; + }; + + DIGITAL_CHANNELS: { + MASK: { + 0: [string, string]; + 1: [string, string]; + 2: [string, string]; + 3: [string, string]; + 4: [string]; + 5: [string, string]; + 6: [string, string]; + 7: [string, string]; + 10: [string, string]; + 11: [string, string]; + 12: [string, string]; + }; + + DIO0: number; + DIO1: number; + DIO2: number; + DIO3: number; + DIO4: number; + DIO5: number; + DIO6: number; + DIO7: number; + DIO10: number; + DIO11: number; + DIO12: number; + + AD0: number; + AD1: number; + AD2: number; + AD3: number; + + ASSOCIATE: number; + RTS: number; + CTS: number; + RSSI: number; + PWM: number; + CD: number; + }; + + ANALOG_CHANNELS: { + MASK: { + 0: [string, string]; + 1: [string, string]; + 2: [string, string]; + 3: [string, string]; + 7: [string]; + }; + + PIN: { + 20: number; + 19: number; + 18: number; + 17: number; + 11: number; + 15: number; + 16: number; + 12: number; + 6: number; + 7: number; + 4: number; + }; + + AD0: number; + AD1: number; + AD2: number; + AD3: number; + + DIO0: number; + DIO1: number; + + SUPPLY: number; + }; + + PULLUP_RESISTOR: { + MASK: { + 0: [string]; + 1: [string, string]; + 2: [string, string]; + 3: [string, string]; + 4: [string, string]; + 5: [string, string]; + 6: [string, string, string]; + 7: [string, string]; + 8: [string, string]; + 9: [string, string]; + 10: [string]; + 11: [string, string, string]; + 12: [string, string]; + 13: [string, string]; + }; + + PIN: { + 11: number; + 17: number; + 18: number; + 19: number; + 20: number; + 16: number; + 9: number; + 3: number; + 15: number; + 13: number; + 4: number; + 6: number; + 7: number; + 12: number; + + DIO4: number; + DIO3: number; + DIO2: number; + DIO1: number; + DIO0: number; + DIO6: number; + DIO8: number; + DIO9: number; + DIO12: number; + DIO10: number; + DIO11: number; + DIO7: number; + + AD3: number; + AD2: number; + AD1: number; + AD0: number; + + PWM0: number; + PWM1: number; + + RTS: number; + DTR: number; + SLEEP_REQUEST: number; + DIN: number; + CONFIG: number; + ASSOCIATE: number; + ON: number; + SLEEP: number; + RSSI: number; + CTS: number; + }; + }; + + CHANGE_DETECTION: { + MASK: { + 0: [string]; + 1: [string]; + 2: [string]; + 3: [string]; + 4: [string]; + 5: [string]; + 6: [string]; + 7: [string]; + 8: [string]; + 9: [string]; + 10: [string]; + 11: [string]; + }; + + PIN: { + 20: number; + 19: number; + 18: number; + 17: number; + 11: number; + 15: number; + 16: number; + 12: number; + 9: number; + 13: number; + 6: number; + 7: number; + }; + + DIO0: number; + DIO1: number; + DIO2: number; + DIO3: number; + DIO4: number; + DIO5: number; + DIO6: number; + DIO7: number; + DIO8: number; + DIO9: number; + DIO10: number; + DIO11: number; + }; + + PIN_MODE: { + // pin + P2: { + // key + UNMONITORED_INPUT: number; + DIGITAL_INPUT: number; + DIGITAL_OUTPUT_LOW: number; + DIGITAL_OUTPUT_HIGH: number; + 0x00: string; + 0x03: string; + 0x04: string; + 0x05: string; + }; + + P1: { + UNMONITORED_INPUT: number; + DIGITAL_INPUT: number; + DIGITAL_OUTPUT_LOW: number; + DIGITAL_OUTPUT_HIGH: number; + 0x00: string; + 0x03: string; + 0x04: string; + 0x05: string; + }; + + P0: { + DISABLED: number; + RSSI_PWM: number; + DIGITAL_INPUT: number; + DIGITAL_OUTPUT_LOW: number; + DIGITAL_OUTPUT_HIGH: number; + 0x00: string; + 0x01: string; + 0x03: string; + 0x04: string; + 0x05: string; + }; + + D4: { + DISABLED: number; + DIGITAL_INPUT: number; + DIGITAL_OUTPUT_LOW: number; + DIGITAL_OUTPUT_HIGH: number; + 0x00: string; + 0x03: string; + 0x04: string; + 0x05: string; + }; + + D7: { + DISABLED: number; + CTS_FLOW_CTRL: number; + DIGITAL_INPUT: number; + DIGITAL_OUTPUT_LOW: number; + DIGITAL_OUTPUT_HIGH: number; + RS485_TX_LOW: number; + RS485_TX_HIGH: number; + 0x00: string; + 0x01: string; + 0x03: string; + 0x04: string; + 0x05: string; + 0x06: string; + 0x07: string; + }; + + D5: { + DISABLED: number; + ASSOC_LED: number; + DIGITAL_INPUT: number; + DIGITAL_OUTPUT_LOW: number; + DIGITAL_OUTPUT_HIGH: number; + 0x00: string; + 0x01: string; + 0x03: string; + 0x04: string; + 0x05: string; + }; + + D6: { + DISABLED: number; + RTS_FLOW_CTRL: number; + DIGITAL_INPUT: number; + DIGITAL_OUTPUT_LOW: number; + DIGITAL_OUTPUT_HIGH: number; + 0x00: string; + 0x01: string; + 0x03: string; + 0x04: string; + 0x05: string; + }; + + D0: { + DISABLED: number; + NODE_ID_ENABLED: number; // Only valid for D0! + ANALOG_INPUT: number; + DIGITAL_INPUT: number; + DIGITAL_OUTPUT_LOW: number; + DIGITAL_OUTPUT_HIGH: number; + 0x00: string; + 0x01: string; + 0x02: string; + 0x03: string; + 0x04: string; + 0x05: string; + }; + + D1: { + DISABLED: number; + NODE_ID_ENABLED: number; // Only valid for D0! + ANALOG_INPUT: number; + DIGITAL_INPUT: number; + DIGITAL_OUTPUT_LOW: number; + DIGITAL_OUTPUT_HIGH: number; + 0x00: string; + 0x01: string; + 0x02: string; + 0x03: string; + 0x04: string; + 0x05: string; + }; + + D2: { + DISABLED: number; + NODE_ID_ENABLED: number; // Only valid for D0! + ANALOG_INPUT: number; + DIGITAL_INPUT: number; + DIGITAL_OUTPUT_LOW: number; + DIGITAL_OUTPUT_HIGH: number; + 0x00: string; + 0x01: string; + 0x02: string; + 0x03: string; + 0x04: string; + 0x05: string; + }; + + D3: { + DISABLED: number; + NODE_ID_ENABLED: number; // Only valid for D0! + ANALOG_INPUT: number; + DIGITAL_INPUT: number; + DIGITAL_OUTPUT_LOW: number; + DIGITAL_OUTPUT_HIGH: number; + 0x00: string; + 0x01: string; + 0x02: string; + 0x03: string; + 0x04: string; + 0x05: string; + }; + }; + + PIN_COMMAND: { + PIN: { + '6': string; + '7': string; + '4': string; + '12': string; + '16': string; + '20': string; + '19': string; + '18': string; + '17': string; + '11': string; + '15': string; + }; + + DIO10: string; + DIO11: string; + DIO12: string; + DIO7: string; + DIO6: string; + DIO0: string; + DIO1: string; + DIO2: string; + DIO3: string; + DIO4: string; + DIO5: string; + + PWM0: string; + PWM1: string; + + AD0: string; + AD1: string; + AD2: string; + AD3: string; + + RSSIM: string; + CTS: string; + ASSOC: string; + }; + + FRAME_TYPE_SETS: { + '802.15.4': [number, number, number, number, number, number, number, number, number, + number, number, number, number]; + ZNet: [number, number, number, number, number, number, number, number, number, + number, number, number, number, number]; + ZigBee: [number, number, number, number, number, number, number, number, number, + number, number, number, number, number, number, number, number, number, number, + number, number, number]; + Any: [number, number, number, number, number, number, number, number, number, number, + number, number, number, number, number, number, number, number, number, number, number, + number, number, number, number, number, number, number, number]; + }; + } +} From b39adf6bbb510d7e6345f515a4f70fdc49d868ab Mon Sep 17 00:00:00 2001 From: Paul Matthew Barrameda Date: Sat, 6 Apr 2019 01:21:19 -0700 Subject: [PATCH 053/152] Added Xbee support Tested with XCTU, it works. Everything else that needs to be created is all logic in Orchestrator and MessageHandler (and also recreate Missions). --- .eslintrc.yml | 6 +- package.json | 4 +- src/common/MessageHandler.ts | 7 +++ src/common/Orchestrator.ts | 1 + src/common/Xbee.ts | 117 +++++++++++++++++++++++++++++++++++ src/main/index.ts | 3 +- src/renderer/index.js | 30 +++++---- src/static/config.json | 2 +- src/util/types.ts | 3 +- typings/xbee-api.d.ts | 17 +++-- 10 files changed, 162 insertions(+), 28 deletions(-) create mode 100644 src/common/MessageHandler.ts create mode 100644 src/common/Orchestrator.ts create mode 100644 src/common/Xbee.ts diff --git a/.eslintrc.yml b/.eslintrc.yml index a431ec05..6904f828 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -46,12 +46,16 @@ rules: multiline-comment-style: error # Modifying react/jsx-filename-extension to support TypeScript React files - 'react/jsx-filename-extension': + "react/jsx-filename-extension": - error - extensions: - .jsx - .tsx + # Move no-useless-constructor to @typescript-eslint/no-useless-constructor + no-useless-constructor: off + "@typescript-eslint/no-useless-constructor": error + # @typescript-eslint/no-use-before-define is already defined no-use-before-define: off diff --git a/package.json b/package.json index 7f76c325..e9a66360 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "description": "Ground Control Station for autonomous vehicle platforms in NGCP", "repository": "https://github.com/NGCP/GCS", "scripts": { - "postinstall": "electron-builder install-app-deps", + "postinstall": "electron-rebuild", "test": "npm run test:lint & npm run test:types & npm run test:unit", "test:lint": "eslint --ext .js,.jsx,.ts,.tsx \".\" & stylelint \"**/*.css\"", "test:types": "tsc --project . --outDir dist --pretty", @@ -40,6 +40,7 @@ "@types/react-dom": "^16.8.3", "@types/react-leaflet": "^2.2.1", "@types/react-virtualized": "^9.21.1", + "@types/serialport": "^7.0.2", "@typescript-eslint/eslint-plugin": "^1.6.0", "@typescript-eslint/parser": "^1.6.0", "babel-eslint": "^10.0.1", @@ -47,6 +48,7 @@ "dotenv-webpack": "^1.7.0", "electron": "^3.1.8", "electron-builder": "^20.39.0", + "electron-rebuild": "^1.8.4", "electron-webpack": "^2.6.2", "electron-webpack-ts": "^3.1.1", "eslint": "^5.16.0", diff --git a/src/common/MessageHandler.ts b/src/common/MessageHandler.ts new file mode 100644 index 00000000..c7e5ff82 --- /dev/null +++ b/src/common/MessageHandler.ts @@ -0,0 +1,7 @@ +import { Event, ipcRenderer } from 'electron'; + +import './Xbee'; + +ipcRenderer.on('processMessage', (_: Event, messageString: string): void => { + console.log(messageString); +}); diff --git a/src/common/Orchestrator.ts b/src/common/Orchestrator.ts new file mode 100644 index 00000000..6f6fc7ca --- /dev/null +++ b/src/common/Orchestrator.ts @@ -0,0 +1 @@ +import './MessageHandler'; diff --git a/src/common/Xbee.ts b/src/common/Xbee.ts new file mode 100644 index 00000000..0ad8200d --- /dev/null +++ b/src/common/Xbee.ts @@ -0,0 +1,117 @@ +import { ipcRenderer } from 'electron'; +import SerialPort from 'serialport'; +import { constants as C, Frame, XBeeAPI } from 'xbee-api'; + +import { VehicleInfo, vehicleInfos } from '../static/index'; + +// TODO: Remove disable line comment when issue gets fixed (https://github.com/benmosher/eslint-plugin-import/pull/1304) +import { Message } from './struct/Messages'; // eslint-disable-line import/named + +/* + * TODO: Add a feature with Vehicle container to change this dynamically and reconnect. + * With that feature, no need to have this at all in config. + */ +const port: string | undefined = 'COM5'; + +const serialport = new SerialPort(port, { baudRate: 57600 }, (err): void => { + if (err) { + ipcRenderer.send('post', 'updateMessages', { + type: 'failure', + message: `Failed to initialize Xbee: ${err.message}`, + }); + } +}); + +const xbeeAPI = new XBeeAPI(); + +/** + * Sends a message through the Xbee to a given vehicle. Will not execute if Xbee port + * is not open. + * + * @param id The unique message id. + * @param vehicleId The vehicle id to send this message to. + * @param message The message to send to the vehicle. + */ +export function sendMessage(id: number, vehicleId: number, message: Message): void { + // Cannot send messages if port is not open. + if (!serialport.isOpen) { + ipcRenderer.send('post', 'updateMessages', { + type: 'failure', + message: 'Tried to send a message while connection is closed', + }); + return; + } + + // Check to ensure the vehicle we are sending this to is valid. + const vehicleInfoObject = vehicleInfos[vehicleId]; + if (!vehicleInfoObject) { + ipcRenderer.send('post', 'updateMessages', { + type: 'failure', + message: `Failed to send message to vehicle with id ${vehicleId}`, + }); + return; + } + + const { macAddress } = vehicleInfoObject as VehicleInfo; + + xbeeAPI.builder.write({ + type: C.FRAME_TYPE.ZIGBEE_TRANSMIT_STATUS, + destination64: macAddress, + data: { + id, + sid: 0, + tid: vehicleId, + time: Date.now(), + ...message, + }, + }); +} + +export function openConnection(): boolean { + if (serialport.isOpen) return false; + serialport.open(); + return true; +} + +export function closeConnection(): boolean { + if (!serialport.isOpen) return false; + serialport.close(); + return true; +} + +serialport.pipe(xbeeAPI.parser); +xbeeAPI.builder.pipe(serialport as NodeJS.WritableStream); + +serialport.on('open', (): void => { + ipcRenderer.send('post', 'updateMessages', { + type: 'success', + message: 'Xbee connection has opened', + }); +}); + +serialport.on('error', (error: Error): void => { + ipcRenderer.send('post', 'updateMessages', { + type: 'failure', + message: `An error has occured with the Xbee connection: ${error.message}`, + }); +}); + +serialport.on('close', (): void => { + ipcRenderer.send('post', 'updateMessages', { + type: 'failure', + message: 'Xbee connection has closed', + }); +}); + +xbeeAPI.parser.on('data', (frame: Frame): void => { + if (frame.type !== C.FRAME_TYPE.ZIGBEE_RECEIVE_PACKET || !frame.data) return; + + // Sends notification to process the message (either a valid JSON string or not). + ipcRenderer.send('post', 'processMessage', frame.data.toString()); +}); + +export default { + closeConnection, + openConnection, + sendMessage, +}; diff --git a/src/main/index.ts b/src/main/index.ts index f26c493f..a6e930ea 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -47,7 +47,7 @@ const isDevelopment = process.env.NODE_ENV !== 'production'; const icon = nativeImage.createFromDataURL(images.icon as string); /** - *Variable to keep track when the app will quit, which is different from hiding the app. + * Variable to keep track when the app will quit, which is different from hiding the app. */ let quitting = false; @@ -73,7 +73,6 @@ const quitRole: MenuItemConstructorOptions = { label: 'Quit', accelerator: 'CommandOrControl+Q', click: (): void => { - quitting = true; app.quit(); }, }; diff --git a/src/renderer/index.js b/src/renderer/index.js index 96200d8f..6259a269 100644 --- a/src/renderer/index.js +++ b/src/renderer/index.js @@ -4,6 +4,8 @@ import { ipcRenderer } from 'electron'; import React, { Component } from 'react'; import ReactDOM from 'react-dom'; +import { fixtures, geolocation } from '../static/index'; + import MainWindow from './mainWindow/MainWindow'; import MissionWindow from './missionWindow/MissionWindow'; @@ -12,8 +14,6 @@ import 'react-virtualized/styles.css'; import './index.css'; -import { fixtures, geolocation } from '../static/index'; - const isDevelopment = process.env.NODE_ENV !== 'production'; const windows = { @@ -21,22 +21,30 @@ const windows = { '#mission': MissionWindow, }; -function runGeolocationAndFixtures() { - /* - * Forces this function to only run once, when it is loaded through the main window. - * Running this function more than once (through both main and mission windows) causes - * fixtures to be sent twice a second, as well as the setMapToUserLocation command, - * and we want to prevent this. - */ +/** + * Function runs only once, when it is loaded through the main window. + * Running this function more than once (through both main and mission windows) causes + * the information below to be run twice, and we do not want that to happen. + */ +function runOnce() { + /* eslint-disable global-require */ + if (window.location.hash !== '#main') return; + // Set up Orchestrator. + require('../common/Orchestrator'); + + // Set up geolocation if geolocation is enabled in config. if (geolocation) { ipcRenderer.send('post', 'setMapToUserLocation'); } + // Sets up fixtures if in development and fixtures are enabled in config. if (isDevelopment && fixtures) { - require('./fixtures/index'); // eslint-disable-line global-require + require('./fixtures/index'); } + + /* eslint-enable global-require */ } class Index extends Component { @@ -69,4 +77,4 @@ class Index extends Component { } } -ReactDOM.render(, document.getElementById('app'), runGeolocationAndFixtures); +ReactDOM.render(, document.getElementById('app'), runOnce); diff --git a/src/static/config.json b/src/static/config.json index 0e6c343d..e1316b67 100644 --- a/src/static/config.json +++ b/src/static/config.json @@ -1,4 +1,4 @@ { - "fixtures": true, + "fixtures": false, "geolocation": true } diff --git a/src/util/types.ts b/src/util/types.ts index 5c0d1421..02a039e0 100644 --- a/src/util/types.ts +++ b/src/util/types.ts @@ -1,6 +1,5 @@ /* - * Types for all variables used in GCS. - * + * Types for variables used in various parts in GCS. */ /** diff --git a/typings/xbee-api.d.ts b/typings/xbee-api.d.ts index 4acb2b00..4ddb0a5a 100644 --- a/typings/xbee-api.d.ts +++ b/typings/xbee-api.d.ts @@ -1,12 +1,8 @@ /* eslint-disable import/prefer-default-export, @typescript-eslint/class-name-casing, max-len, camelcase, @typescript-eslint/camelcase, @typescript-eslint/no-explicit-any */ /* eslint-enable max-len */ -/* - * Allows us to use xbee-api as the original api has no typings. - */ + declare module 'xbee-api' { - import BufferReader from 'buffer-reader'; - import { Buffer } from 'safe-buffer'; import { Transform } from 'stream'; type START_BYTE = number; @@ -22,7 +18,7 @@ declare module 'xbee-api' { command?: string; commandParameter?: any; commandStatus?: string; - data?: any; + data?: Buffer; deliveryStatus?: number; destination16?: string; destination64?: string; @@ -62,6 +58,7 @@ declare module 'xbee-api' { sourceEndpoint?: string; sourceEvent?: number; transmitRetryCount?: number; + type?: number; } export interface XbeeAPIOptions { @@ -74,7 +71,7 @@ declare module 'xbee-api' { builder_buffer_size?: number; } - export class XbeeAPI { + export class XBeeAPI { private options: XbeeAPIOptions; private parseState: { @@ -143,7 +140,7 @@ declare module 'xbee-api' { public parseRaw(buffer: Buffer, enc?: any, cb?: () => void): void; } - export interface constants { + interface Constants { START_BYTE: START_BYTE; ESCAPE: ESCAPE; XOFF: XOFF; @@ -518,9 +515,7 @@ declare module 'xbee-api' { }; PIN_MODE: { - // pin P2: { - // key UNMONITORED_INPUT: number; DIGITAL_INPUT: number; DIGITAL_OUTPUT_LOW: number; @@ -723,4 +718,6 @@ declare module 'xbee-api' { number, number, number, number, number, number, number, number]; }; } + + export const constants: Constants; } From 667b000916956e09b2c5c6cf26a0b15e2922dd3d Mon Sep 17 00:00:00 2001 From: Paul Matthew Barrameda Date: Sun, 7 Apr 2019 00:46:46 -0700 Subject: [PATCH 054/152] Fixed jobs to tasks and finished Vehicle.ts --- src/common/Xbee.ts | 8 +- src/common/struct/Jobs.ts | 198 ++++++++++++++++++++++++++++------- src/common/struct/Vehicle.ts | 114 +++++++++++--------- src/main/index.ts | 2 +- 4 files changed, 227 insertions(+), 95 deletions(-) diff --git a/src/common/Xbee.ts b/src/common/Xbee.ts index 0ad8200d..5d0068df 100644 --- a/src/common/Xbee.ts +++ b/src/common/Xbee.ts @@ -34,13 +34,7 @@ const xbeeAPI = new XBeeAPI(); */ export function sendMessage(id: number, vehicleId: number, message: Message): void { // Cannot send messages if port is not open. - if (!serialport.isOpen) { - ipcRenderer.send('post', 'updateMessages', { - type: 'failure', - message: 'Tried to send a message while connection is closed', - }); - return; - } + if (!serialport.isOpen) return; // Check to ensure the vehicle we are sending this to is valid. const vehicleInfoObject = vehicleInfos[vehicleId]; diff --git a/src/common/struct/Jobs.ts b/src/common/struct/Jobs.ts index 28aea832..40f07b4e 100644 --- a/src/common/struct/Jobs.ts +++ b/src/common/struct/Jobs.ts @@ -1,24 +1,22 @@ /* - * https://github.com/NGCP/GCS/wiki/List-of-Job-Types - * - * These definitions will not have the time, sid, tid, and id fields. + * Defines all the available jobs for vehicles, as well as the jobs' corresponding tasks. + * Also has a function to check whether or not a task is valid for a certain job. */ -export interface JobBase { +interface TaskBase { /** - * Name of the job. - */ - jobType: string; - - /** - * Information related to complete the job. + * Information related to complete the task. */ missionInfo: object; } -export interface Takeoff extends JobBase { - jobType: 'takeoff'; +/** + * Takeoff task. + */ +export interface Takeoff extends TaskBase { missionInfo: { + taskType: 'takeoff'; + lat: number; lng: number; alt: number; @@ -32,9 +30,13 @@ export interface Takeoff extends JobBase { }; } -export interface Loiter extends JobBase { - jobType: 'loiter'; +/** + * Update loiter task. + */ +export interface Loiter extends TaskBase { missionInfo: { + taskType: 'loiter'; + lat: number; lng: number; alt: number; @@ -43,10 +45,14 @@ export interface Loiter extends JobBase { }; } +/** + * ISR search task. + */ // eslint-disable-next-line @typescript-eslint/interface-name-prefix -export interface ISRSearch extends JobBase { - jobType: 'isrSearch'; +export interface ISRSearch extends TaskBase { missionInfo: { + taskType: 'isrSearch'; + alt: number; waypoints: [ { @@ -65,9 +71,13 @@ export interface ISRSearch extends JobBase { }; } -export interface PayloadDrop extends JobBase { - jobType: 'payloadDrop'; +/** + * Payload drop task. + */ +export interface PayloadDrop extends TaskBase { missionInfo: { + taskType: 'payloadDrop'; + waypoints: [ { lat: number; @@ -83,9 +93,13 @@ export interface PayloadDrop extends JobBase { }; } -export interface Land extends JobBase { - jobType: 'land'; +/** + * Land task. + */ +export interface Land extends TaskBase { missionInfo: { + taskType: 'land'; + waypoints: [ { lat: number; @@ -101,44 +115,154 @@ export interface Land extends JobBase { }; } -export interface RetrieveTargetUGV extends JobBase { - jobType: 'retrieveTarget'; +/** + * Retrieve target task for UGV. + */ +export interface UGVRetrieveTarget extends TaskBase { missionInfo: { + taskType: 'retrieveTarget'; + lat: number; lng: number; }; } -export interface DeliverTarget extends JobBase { - jobType: 'deliverTarget'; +/** + * Deliver target task. + */ +export interface DeliverTarget extends TaskBase { missionInfo: { + taskType: 'deliverTarget'; + lat: number; lng: number; }; } -export interface RetrieveTargetUUV extends JobBase { - jobType: 'retrieveTarget'; - missionINfo: {}; +/** + * Retrieve target task for UUV. + */ +export interface UUVRetrieveTarget extends TaskBase { + missionInfo: { + taskType: 'retrieveTarget'; + }; } -export interface QuickScan extends JobBase { - jobType: 'quickScan'; +/** + * Quick scan task. + */ +export interface QuickScan extends TaskBase { missionInfo: { - lat: number; - lng: number; - innerRadius: number; - outerRadius: number; + taskType: 'quickScan'; + + searchArea: { + center: [number, number]; + rad1: number; + rad2: number; + }; }; } -export interface DetailedSearch extends JobBase { - jobType: 'detailedSearch'; +/** + * Detailed search task. + */ +export interface DetailedSearch extends TaskBase { missionInfo: { + taskType: 'detailedSearch'; + lat: number; lng: number; }; } -export type Job = Takeoff | Loiter | ISRSearch | PayloadDrop | Land | RetrieveTargetUGV -| DeliverTarget | RetrieveTargetUUV | QuickScan | DetailedSearch; +/** + * An operation that the vehicle can perform given its job. + */ +export type Task = Takeoff | Loiter | ISRSearch | PayloadDrop | Land | UGVRetrieveTarget +| DeliverTarget | UUVRetrieveTarget | QuickScan | DetailedSearch; + +/** + * All vehicle jobs with their corresponding tasks. + */ +export interface Jobs { + /** + * Vehicle is capable of performing ISR search. + */ + isrSearch: { + takeoff: Takeoff; + loiter: Loiter; + isrSearch: ISRSearch; + land: Land; + }; + + /** + * Vehicle is capable of performing payload drop. + */ + payloadDrop: { + takeoff: Takeoff; + loiter: Loiter; + payloadDrop: PayloadDrop; + land: Land; + }; + + /** + * Vehicle is capable of performing retrieving target on land. + */ + ugvRetrieve: { + retrieveTarget: UGVRetrieveTarget; + deliverTarget: DeliverTarget; + }; + + /** + * Vehicle is capable of performing retrieving target underwater. + */ + uuvRetrieve: { + retrieveTarget: UUVRetrieveTarget; + }; + + /** + * Vehicle is capable of performing quick scan. + */ + quickScan: { + quickScan: QuickScan; + }; + + /** + * Vehicle is capable of performing detailed search. + */ + detailedSearch: { + detailedSearch: DetailedSearch; + }; +} + +function contains(array: string[], value: string): boolean { + return array.indexOf(value) >= 0; +} + +/** + * Type guard to see if a task is valid for a certain job. + */ +export function isValidTaskForJob(task: Task, jobType: string): boolean { + const { taskType } = task.missionInfo; + + switch (jobType) { + case 'ISRSearch': + return contains(['takeoff', 'loiter', 'isrSearch', 'land'], taskType); + case 'PayloadDrop': + return contains(['takeoff', 'loiter', 'payloadDrop', 'land'], taskType); + case 'UGVRetrieve': + return contains(['retrieveTarget', 'deliverTarget'], taskType); + case 'UUVRetrieve': + return taskType === 'retrieveTarget'; + case 'QuickScan': + return taskType === 'quickScan'; + case 'DetailedSearch': + return taskType === 'detailedSearch'; + default: + return false; + } +} + +export default { + isValidTaskForJob, +}; diff --git a/src/common/struct/Vehicle.ts b/src/common/struct/Vehicle.ts index e2436845..6a942891 100644 --- a/src/common/struct/Vehicle.ts +++ b/src/common/struct/Vehicle.ts @@ -5,7 +5,7 @@ import { vehicleInfos } from '../../static/index'; // TODO: Remove disable line comment when issue gets fixed (https://github.com/benmosher/eslint-plugin-import/pull/1304) import { VehicleStatus } from '../../util/types'; // eslint-disable-line import/named -import { Job } from './Jobs'; // eslint-disable-line import/named +import { isValidTaskForJob, Task } from './Jobs'; // eslint-disable-line import/named import { Message, StartMessage, UpdateMessage } from './Messages'; // eslint-disable-line import/named import UpdateHandler from './UpdateHandler'; @@ -36,7 +36,7 @@ export interface VehicleObject { status: VehicleStatus; /** - * Jobs that the vehicle can perform. + * Jobs of the vehicle. */ jobs: string[]; @@ -67,25 +67,34 @@ export interface VehicleObject { } /** - * Contains data held by a vehicle as well as functions used by the vehicle. + * Contains data about a specific physical vehicle that the GCS will need to keep track + * of it (missions, information, etc). + * + * Also has functions that allows the GCS to command the physical vehicle by sending it + * tasks. */ export default class Vehicle { + /** + * Forwards message to MessageHandler to send through Xbee. + */ + private static sendMessage(message: Message): void { + ipcRenderer.send('post', 'sendMessage', message); + } + /** * ID of the vehicle. */ private vehicleId: number; /** - * Currently assigned job. + * Current assigned job. */ private assignedJob: string = ''; /** - * Whether or not the vehicle is currently in the state of being assigned a job. - * When the vehicle is assigned a job, it will be waiting for job info until it gets that. - * During that time, this variable will be true. False otherwise. + * Whether or not the vehicle is ready to be assigned a mission. */ - private waitingForJobInfo: boolean = false; + private readyForMission: boolean = true; /** * Current status of the vehicle. @@ -93,7 +102,7 @@ export default class Vehicle { private status: VehicleStatus = 'disconnected'; /** - * Jobs that the vehicle can perform. + * Jobs the vehicle has. These define the tasks the vehicle is capable of performing. */ private jobs: string[]; @@ -171,12 +180,12 @@ export default class Vehicle { }); this.updateEventHandler.addHandler('battery', (battery, message): boolean => { - if (battery > 1) { + if (battery > 1 || battery < 0) { const vehicleInfo = message && message.sid && vehicleInfos[message.sid]; ipcRenderer.send('post', 'updateMessages', { type: 'failure', - message: `Received a battery status of more than 100% (${battery * 100}%) from ${(vehicleInfo && vehicleInfo.name) || 'an unknown vehicle'}`, + message: `Received an invalid battery status (${battery * 100}%) from ${(vehicleInfo && vehicleInfo.name) || 'an unknown vehicle'}`, }); } else { this.battery = battery; @@ -246,89 +255,94 @@ export default class Vehicle { } /** - * Assigns a job to the vehicle. + * Notifies the vehicle that it will be performing a certain mission. We let the vehicle know of + * the job type too, so that it knows which tasks to expect from us and discard any other + * tasks that do not support their job. + * + * Will return true if the mission was assigned successfully. + * + * @param jobType The job that will be used to accomplish the mission. + * @param options Optional information vehicle will need before performing any tasks. + * @param completionCallback Optional callback when vehicle finishes/terminates the mission. + * @param disconnectionCallback Optional callback when vehicle disconnects. + * @param errorCallback Optional callback when vehicle goes in an error state. */ - public assignJob( - job: string, + public assignMission( + jobType: string, options?: object, completionCallback?: () => void, disconnectionCallback?: () => void, errorCallback?: ErrorCallback, - ): void { - if (this.waitingForJobInfo) { - throw new Error('Vehicle has already been assigned a job and is waiting for info.'); + ): boolean { + // Returns false if a vehicle has already been assigned a mission. + if (!this.readyForMission) { + return false; } - this.assignedJob = job; - this.waitingForJobInfo = true; + this.assignedJob = jobType; + this.readyForMission = false; if (errorCallback) this.errorCallback = errorCallback; const startMessage: StartMessage = { type: 'start', - jobType: this.assignedJob, + jobType, }; if (options) startMessage.options = options; // Sends the start message to the vehicle with corresponding job name. - this.sendMessage({ - type: 'start', - jobType: this.assignedJob, - }); + Vehicle.sendMessage(startMessage); /* - * Create handler that will call completion callback and when vehicle goes back to - * "ready" status. - * - * Vehicle goes back to "ready" status once it either finishes a job or gets its job done and - * is ready for another job to be assigned. - * - * There will be a callback that can be provided in the case when the vehicle is in a state - * other than "ready" and it gets disconnected. We do not need to worry about a disconnection - * callback when the vehicle is "ready" since it is in a stable state. + * Create handler that will sets the readyForMission back to true once the vehicle + * goes back to a ready status. Vehicle goes back to "ready" status once it either + * finishes a mission or goes back to a neutral position after its mission is stopped. */ this.updateEventHandler.addHandler('status', (value): boolean => { if (value === 'ready') { if (completionCallback) completionCallback(); - this.waitingForJobInfo = false; + this.readyForMission = true; } else if (value === 'disconnected') { if (disconnectionCallback) disconnectionCallback(); } return value === 'ready'; }); + + return true; } /** - * Starts the vehicle's job by giving the vehicle information. This will be called by the - * Orchestrator when the user interface provides it with information for the job. + * Gives the vehicle a task to perform (the task must be able to be done by the vehicle's job). + * Will return true if the task was assigned successfully. The only way the task would not + * be assigned successfully is if the task is not supported by the vehicle's job. + * + * @param task The task for the vehicle to perform. Must support the vehicle's job. */ - public startJob(job: Job): void { - if (!this.waitingForJobInfo) { - throw new Error('Vehicle has not been assigned a job yet. Assign the job before telling the vehicle to start.'); + public addMission(task: Task): boolean { + /* + * Returns false if the vehicle has not been assigned a mission or if the provided task + * is not compatible with its job. + */ + if (!this.readyForMission || !isValidTaskForJob(task, this.assignedJob)) { + return false; } - this.sendMessage({ + Vehicle.sendMessage({ type: 'addMission', - missionInfo: job.missionInfo, + missionInfo: task, }); + + return true; } /** * Sends stop message to vehicle. */ public stop(): void { - this.sendMessage({ + Vehicle.sendMessage({ type: 'stop', }); this.assignedJob = ''; } - - // TODO: Make this function and get it to send message using Xbee. - private sendMessage(message: Message): void { // eslint-disable-line - /* - * Xbee.send(this.vehicleId, message); or use MAC address, probably better to get this from - * Xbee class though - */ - } } diff --git a/src/main/index.ts b/src/main/index.ts index a6e930ea..263deabd 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -432,7 +432,7 @@ app.on('ready', (): void => { * This is necessary to run the map container. */ if (!process.env.MAPBOX_TOKEN) { - throw new Error('Set the MapBox token in .env before launching the application.'); + throw new Error('Set the MapBox token in .env before launching the application'); } createMainWindow(); From fd2cd0cc1173859f8b3242435e282aa94f6883ce Mon Sep 17 00:00:00 2001 From: Paul Matthew Barrameda Date: Sun, 7 Apr 2019 01:44:30 -0700 Subject: [PATCH 055/152] Fixed job names on Jobs.ts --- src/common/struct/Jobs.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/common/struct/Jobs.ts b/src/common/struct/Jobs.ts index 40f07b4e..647fd13e 100644 --- a/src/common/struct/Jobs.ts +++ b/src/common/struct/Jobs.ts @@ -246,17 +246,17 @@ export function isValidTaskForJob(task: Task, jobType: string): boolean { const { taskType } = task.missionInfo; switch (jobType) { - case 'ISRSearch': + case 'isrSearch': return contains(['takeoff', 'loiter', 'isrSearch', 'land'], taskType); - case 'PayloadDrop': + case 'payloadDrop': return contains(['takeoff', 'loiter', 'payloadDrop', 'land'], taskType); - case 'UGVRetrieve': + case 'ugvRetrieve': return contains(['retrieveTarget', 'deliverTarget'], taskType); - case 'UUVRetrieve': + case 'uuvRetrieve': return taskType === 'retrieveTarget'; - case 'QuickScan': + case 'quickScan': return taskType === 'quickScan'; - case 'DetailedSearch': + case 'detailedSearch': return taskType === 'detailedSearch'; default: return false; From 01555a852231c93f4b661b5a1d76fa7685c9e6e1 Mon Sep 17 00:00:00 2001 From: Paul Matthew Barrameda Date: Sun, 7 Apr 2019 13:16:04 -0700 Subject: [PATCH 056/152] Reformatted configuration files and types Changed exports in static/index so that the variables can be seen as configuration files when imported to other files. Will cause less confusion to the developer. Moved all source types to a `src/types` folder. Note this is different from external module typings, which will contain types for external modules. Commented out things that do not work right now because of these changes, and will fix them back once I have the time. Added typings to all tasks and messages. TODO: Fix code that I have commented with code that works for it now. Finish the type guard functions in src/types/messages. @tnleang can you help me with this? Fix documentation for all of this. Perhaps add a `docs` folder in root? --- src/common/MessageHandler.ts | 135 ++++++- src/common/Xbee.ts | 47 ++- src/common/struct/Jobs.ts | 268 ------------- src/common/struct/Messages.ts | 149 -------- src/common/struct/Vehicle.ts | 85 ++--- src/main/index.ts | 10 +- .../fixtures/updateMessagesFixtures.ts | 2 +- .../fixtures/updateVehiclesFixtures.ts | 12 +- src/renderer/index.js | 6 +- src/renderer/mainWindow/MainWindow.tsx | 2 +- src/renderer/mainWindow/log/LogContainer.tsx | 2 +- src/renderer/mainWindow/map/MapContainer.tsx | 9 +- src/renderer/mainWindow/map/VehicleMarker.tsx | 17 +- .../mainWindow/map/control/ThemeControl.tsx | 2 +- .../mainWindow/mission/MissionContainer.tsx | 2 +- .../mainWindow/mission/MissionTable.tsx | 2 +- .../mainWindow/vehicle/VehicleContainer.tsx | 4 +- .../mainWindow/vehicle/VehicleTable.tsx | 11 +- .../missionWindow/missions/Mission.jsx | 4 +- .../missionWindow/missions/jobs/Job.jsx | 22 +- src/static/config.json | 5 +- src/static/index.ts | 65 +++- src/static/vehicle.json | 9 +- src/types/messages.ts | 352 ++++++++++++++++++ src/{util => types}/types.ts | 64 +++- src/util/util.ts | 70 +--- 26 files changed, 705 insertions(+), 651 deletions(-) delete mode 100644 src/common/struct/Jobs.ts delete mode 100644 src/common/struct/Messages.ts create mode 100644 src/types/messages.ts rename src/{util => types}/types.ts (73%) diff --git a/src/common/MessageHandler.ts b/src/common/MessageHandler.ts index c7e5ff82..3c83220d 100644 --- a/src/common/MessageHandler.ts +++ b/src/common/MessageHandler.ts @@ -1,7 +1,134 @@ import { Event, ipcRenderer } from 'electron'; -import './Xbee'; +import { config } from '../static/index'; -ipcRenderer.on('processMessage', (_: Event, messageString: string): void => { - console.log(messageString); -}); +// TODO: Remove disable line comment when issue gets fixed (https://github.com/benmosher/eslint-plugin-import/pull/1304) +import DictionaryList from './struct/DictionaryList'; +import { Message } from '../types/messages'; // eslint-disable-line import/named +import UpdateHandler from './struct/UpdateHandler'; + +import xbee, { JSONMessage } from './Xbee'; // eslint-disable-line import/named + +export default class MessageHandler { + /** + * Dictionary for all messages that are being sent, sent and acknowledged, and received. + * + * Description of keys: + * "outbox": will have a list of messages that are being sent, but are awaiting + * acknowledgement. + * "sent": will have a list of messages that have been sent. + * "received": will have a list of messages that have been received. + */ + private messageDictionary = new DictionaryList(); + + /** + * Handler that listens for different update events for the message handler. + */ + private updateHandler = new UpdateHandler(); + + /** + * Message id being sent so far. + */ + private id = 0; + + public constructor() { + ipcRenderer.on('sendMessage', (_: Event, vehicleId: number, message: Message): void => this.sendMessage(vehicleId, message)); + + ipcRenderer.on('receiveMessage', (_: Event, string: string): void => this.receiveMessage(string)); + } + + /** + * Removes a message from the outbox. + * + * @param id Message id + */ + private removeMessage(key: 'outbox' | 'sent' | 'received', id: number): JSONMessage | undefined { + return this.messageDictionary.remove(key, (message: JSONMessage): boolean => message.id === id); + } + + /** + * Sends message to vehicle through Xbee. + * + * @param vehicleId Vehicle id to send the message to. + * @param message Message format (note this does not have sid, tid, id, and time). + */ + private sendMessage(vehicleId: number, message: Message): void { + const jsonMessage: JSONMessage = { + id: this.id, + sid: 0, + tid: vehicleId, + time: Date.now(), + ...message, + }; + + this.id += 1; + + /* + * Adds the json message to "sent" and "outbox" on messageDictionary. If it's an ack + * or badMessage message, it is only added to "sent", since that message does not need to be + * acknowledged. + */ + this.messageDictionary.push('sent', jsonMessage); + + if (jsonMessage.type === 'ack' || jsonMessage.type === 'badMessage') { + /* + * Will only send these types of messages once, but everything else will be sent + * continuously until acknowleged. + */ + xbee.sendMessage(jsonMessage); + return; + } + + this.messageDictionary.push('outbox', jsonMessage); + + /* + * Keep sending the message in a certain rate (see messageSendRate). + * This will end when the message is acknowledged (the updateHandler will clear + * the interval). + */ + const expiry = setInterval((): void => { + xbee.sendMessage(jsonMessage); + }, config.messageSendRate * 1000); + + /* + * Removes json message from "outbox" when an ack message is received from the vehicle. + * If no ack message is received within a certain amount of time (vehicleDisconnectionTime), + * then the GCS will disconnect itself from the vehicle. + */ + this.updateHandler.addHandler(`${jsonMessage.tid}#${jsonMessage.id}`, (): boolean => { + clearInterval(expiry); + this.removeMessage('outbox', jsonMessage.id); + return true; + }, { + time: config.vehicleDisconnectionTime * 1000, + callback: (): void => { + this.removeMessage('outbox', jsonMessage.id); + ipcRenderer.send('post', 'deactivateVehicle', vehicleId); + }, + }); + } + + /** + * Acknowledge a message received. + * + * @param message The message to acknowledge. + * TODO: remove ? from message + */ + private acknowledge(message?: JSONMessage): void { // eslint-disable-line + if (!message) return; // TODO: remove this line + this.sendMessage(message.sid, { + type: 'ack', + ackid: message.id, + }); + } + + /** + * Processes a message that was received. + * + * @param string The raw string. Either can contain the message (a valid JSON) or some + * random stuff. + */ + private receiveMessage(string: string): void { + if (string === 'asdffdasfdas') this.acknowledge(); // TODO: remove this line + } +} diff --git a/src/common/Xbee.ts b/src/common/Xbee.ts index 5d0068df..c512a0d0 100644 --- a/src/common/Xbee.ts +++ b/src/common/Xbee.ts @@ -1,11 +1,28 @@ +// Do not interact with this file outside the MissionHandler class. + import { ipcRenderer } from 'electron'; import SerialPort from 'serialport'; import { constants as C, Frame, XBeeAPI } from 'xbee-api'; -import { VehicleInfo, vehicleInfos } from '../static/index'; +import { VehicleInfo, vehicleConfig } from '../static/index'; // TODO: Remove disable line comment when issue gets fixed (https://github.com/benmosher/eslint-plugin-import/pull/1304) -import { Message } from './struct/Messages'; // eslint-disable-line import/named +import { Message } from '../types/messages'; // eslint-disable-line import/named + +/** + * Requirements for all messages that are sent/received. + */ +interface MessageRequirements { + id: number; + sid: 0; + tid: number; + time: number; +} + +/** + * Message with id, sid, tid, and time. + */ +export type JSONMessage = Message & MessageRequirements; /* * TODO: Add a feature with Vehicle container to change this dynamically and reconnect. @@ -28,20 +45,22 @@ const xbeeAPI = new XBeeAPI(); * Sends a message through the Xbee to a given vehicle. Will not execute if Xbee port * is not open. * - * @param id The unique message id. * @param vehicleId The vehicle id to send this message to. * @param message The message to send to the vehicle. */ -export function sendMessage(id: number, vehicleId: number, message: Message): void { +function sendMessage(message: JSONMessage): void { // Cannot send messages if port is not open. if (!serialport.isOpen) return; - // Check to ensure the vehicle we are sending this to is valid. - const vehicleInfoObject = vehicleInfos[vehicleId]; + /* + * Check to ensure the vehicle we are sending this to is valid. This is just in case + * MessageHandler somehow provides an invalid tid (id of the target vehicle). + */ + const vehicleInfoObject = vehicleConfig.vehicleInfos[message.tid]; if (!vehicleInfoObject) { ipcRenderer.send('post', 'updateMessages', { type: 'failure', - message: `Failed to send message to vehicle with id ${vehicleId}`, + message: `Failed to send message to vehicle with id ${message.tid}`, }); return; } @@ -51,23 +70,17 @@ export function sendMessage(id: number, vehicleId: number, message: Message): vo xbeeAPI.builder.write({ type: C.FRAME_TYPE.ZIGBEE_TRANSMIT_STATUS, destination64: macAddress, - data: { - id, - sid: 0, - tid: vehicleId, - time: Date.now(), - ...message, - }, + data: message, }); } -export function openConnection(): boolean { +function openConnection(): boolean { if (serialport.isOpen) return false; serialport.open(); return true; } -export function closeConnection(): boolean { +function closeConnection(): boolean { if (!serialport.isOpen) return false; serialport.close(); return true; @@ -101,7 +114,7 @@ xbeeAPI.parser.on('data', (frame: Frame): void => { if (frame.type !== C.FRAME_TYPE.ZIGBEE_RECEIVE_PACKET || !frame.data) return; // Sends notification to process the message (either a valid JSON string or not). - ipcRenderer.send('post', 'processMessage', frame.data.toString()); + ipcRenderer.send('post', 'receiveMessage', frame.data.toString()); }); export default { diff --git a/src/common/struct/Jobs.ts b/src/common/struct/Jobs.ts deleted file mode 100644 index 647fd13e..00000000 --- a/src/common/struct/Jobs.ts +++ /dev/null @@ -1,268 +0,0 @@ -/* - * Defines all the available jobs for vehicles, as well as the jobs' corresponding tasks. - * Also has a function to check whether or not a task is valid for a certain job. - */ - -interface TaskBase { - /** - * Information related to complete the task. - */ - missionInfo: object; -} - -/** - * Takeoff task. - */ -export interface Takeoff extends TaskBase { - missionInfo: { - taskType: 'takeoff'; - - lat: number; - lng: number; - alt: number; - loiter: { - lat: number; - lng: number; - alt: number; - radius: number; - direction: number; - }; - }; -} - -/** - * Update loiter task. - */ -export interface Loiter extends TaskBase { - missionInfo: { - taskType: 'loiter'; - - lat: number; - lng: number; - alt: number; - radius: number; - direction: number; - }; -} - -/** - * ISR search task. - */ -// eslint-disable-next-line @typescript-eslint/interface-name-prefix -export interface ISRSearch extends TaskBase { - missionInfo: { - taskType: 'isrSearch'; - - alt: number; - waypoints: [ - { - lat: number; - lng: number; - }, - { - lat: number; - lng: number; - }, - { - lat: number; - lng: number; - } - ]; - }; -} - -/** - * Payload drop task. - */ -export interface PayloadDrop extends TaskBase { - missionInfo: { - taskType: 'payloadDrop'; - - waypoints: [ - { - lat: number; - lng: number; - alt: number; - }, - { - lat: number; - lng: number; - alt: number; - } - ]; - }; -} - -/** - * Land task. - */ -export interface Land extends TaskBase { - missionInfo: { - taskType: 'land'; - - waypoints: [ - { - lat: number; - lng: number; - alt: number; - }, - { - lat: number; - lng: number; - alt: number; - } - ]; - }; -} - -/** - * Retrieve target task for UGV. - */ -export interface UGVRetrieveTarget extends TaskBase { - missionInfo: { - taskType: 'retrieveTarget'; - - lat: number; - lng: number; - }; -} - -/** - * Deliver target task. - */ -export interface DeliverTarget extends TaskBase { - missionInfo: { - taskType: 'deliverTarget'; - - lat: number; - lng: number; - }; -} - -/** - * Retrieve target task for UUV. - */ -export interface UUVRetrieveTarget extends TaskBase { - missionInfo: { - taskType: 'retrieveTarget'; - }; -} - -/** - * Quick scan task. - */ -export interface QuickScan extends TaskBase { - missionInfo: { - taskType: 'quickScan'; - - searchArea: { - center: [number, number]; - rad1: number; - rad2: number; - }; - }; -} - -/** - * Detailed search task. - */ -export interface DetailedSearch extends TaskBase { - missionInfo: { - taskType: 'detailedSearch'; - - lat: number; - lng: number; - }; -} - -/** - * An operation that the vehicle can perform given its job. - */ -export type Task = Takeoff | Loiter | ISRSearch | PayloadDrop | Land | UGVRetrieveTarget -| DeliverTarget | UUVRetrieveTarget | QuickScan | DetailedSearch; - -/** - * All vehicle jobs with their corresponding tasks. - */ -export interface Jobs { - /** - * Vehicle is capable of performing ISR search. - */ - isrSearch: { - takeoff: Takeoff; - loiter: Loiter; - isrSearch: ISRSearch; - land: Land; - }; - - /** - * Vehicle is capable of performing payload drop. - */ - payloadDrop: { - takeoff: Takeoff; - loiter: Loiter; - payloadDrop: PayloadDrop; - land: Land; - }; - - /** - * Vehicle is capable of performing retrieving target on land. - */ - ugvRetrieve: { - retrieveTarget: UGVRetrieveTarget; - deliverTarget: DeliverTarget; - }; - - /** - * Vehicle is capable of performing retrieving target underwater. - */ - uuvRetrieve: { - retrieveTarget: UUVRetrieveTarget; - }; - - /** - * Vehicle is capable of performing quick scan. - */ - quickScan: { - quickScan: QuickScan; - }; - - /** - * Vehicle is capable of performing detailed search. - */ - detailedSearch: { - detailedSearch: DetailedSearch; - }; -} - -function contains(array: string[], value: string): boolean { - return array.indexOf(value) >= 0; -} - -/** - * Type guard to see if a task is valid for a certain job. - */ -export function isValidTaskForJob(task: Task, jobType: string): boolean { - const { taskType } = task.missionInfo; - - switch (jobType) { - case 'isrSearch': - return contains(['takeoff', 'loiter', 'isrSearch', 'land'], taskType); - case 'payloadDrop': - return contains(['takeoff', 'loiter', 'payloadDrop', 'land'], taskType); - case 'ugvRetrieve': - return contains(['retrieveTarget', 'deliverTarget'], taskType); - case 'uuvRetrieve': - return taskType === 'retrieveTarget'; - case 'quickScan': - return taskType === 'quickScan'; - case 'detailedSearch': - return taskType === 'detailedSearch'; - default: - return false; - } -} - -export default { - isValidTaskForJob, -}; diff --git a/src/common/struct/Messages.ts b/src/common/struct/Messages.ts deleted file mode 100644 index 2767b6e3..00000000 --- a/src/common/struct/Messages.ts +++ /dev/null @@ -1,149 +0,0 @@ -/* - * Definitions for all messages that are sent between GCS and vehicles. See the wiki for more - * information: https://github.com/NGCP/GCS/wiki/GCS-JSON-Messages - * - * These definitions will not have the time, sid, tid, and id fields. - */ - -// TODO: Remove disable line comment when issue gets fixed (https://github.com/benmosher/eslint-plugin-import/pull/1304) -import { VehicleStatus } from '../../util/types'; // eslint-disable-line import/named - -// From GCS to vehicles. - -interface MessageBase { - /** - * Type of message. - */ - type: string; -} - -export interface StartMessage extends MessageBase { - type: 'start'; - - /** - * Name of job to perform. - */ - jobType: string; - - /** - * Any other information the vehicle should have before performing the job. - */ - options?: object; -} - -export interface AddMissionMessage extends MessageBase { - type: 'addMission'; - - /** - * Information related to accomplishing specific job. - */ - missionInfo: object; -} - -export interface PauseMessage extends MessageBase { - type: 'pause'; -} - -export interface ResumeMessage extends MessageBase { - type: 'resume'; -} - -export interface StopMessage extends MessageBase { - type: 'stop'; -} - -export interface ConnectionAckMessage extends MessageBase { - type: 'connectionAck'; -} - -// From vehicles to GCS. - -export interface UpdateMessage extends MessageBase { - type: 'update'; - - /** - * Latitude of the vehicle. - */ - lat: number; - - /** - * Longitude of the vehicle. - */ - lng: number; - - /** - * Altitude of the vehicle. - */ - alt?: number; - - /** - * Current vehicle heading. Value is in degrees. - */ - heading?: number; - - /** - * Current battery of the vehicle, expressed as a decimal. Will vary from 0 to 1. - */ - battery?: number; - - /** - * Status of the vehicle. - */ - status: VehicleStatus; - - /** - * Message generated if vehicle is in an error state. - */ - errorMessage?: string; -} - -export interface POIMessage extends MessageBase { - type: 'poi'; - - /** - * Latitude of the point of interest. - */ - lat: number; - - /** - * Longitude of the point of interest. - */ - lng: number; -} - -export interface CompleteMessage extends MessageBase { - type: 'complete'; -} - -export interface ConnectionMessage extends MessageBase { - type: 'connect'; - - /** - * List of all the different jobs the vehicle is capable of doing. - */ - jobsAvailable: string[]; -} - -// Other message types. - -export interface AckMessage extends MessageBase { - type: 'ack'; - - /** - * ID of the message that is being acknowledged. - */ - ackid: number; -} - -export interface BadMessageMessage extends MessageBase { - type: 'badMessage'; - - /** - * Description of why message was bad. - */ - error: string; -} - -export type Message = StartMessage | AddMissionMessage | PauseMessage | ResumeMessage | StopMessage -| ConnectionAckMessage | UpdateMessage | POIMessage | CompleteMessage | ConnectionMessage -| AckMessage | BadMessageMessage; diff --git a/src/common/struct/Vehicle.ts b/src/common/struct/Vehicle.ts index 6a942891..003477ca 100644 --- a/src/common/struct/Vehicle.ts +++ b/src/common/struct/Vehicle.ts @@ -1,12 +1,17 @@ import { ipcRenderer } from 'electron'; -import { vehicleInfos } from '../../static/index'; +import { vehicleConfig } from '../../static/index'; // TODO: Remove disable line comment when issue gets fixed (https://github.com/benmosher/eslint-plugin-import/pull/1304) -import { VehicleStatus } from '../../util/types'; // eslint-disable-line import/named +import { VehicleObject, VehicleStatus } from '../../types/types'; // eslint-disable-line import/named + +import { + Message, // eslint-disable-line import/named + StartMessage, + Task, // eslint-disable-line import/named + UpdateMessage, +} from '../../types/messages'; -import { isValidTaskForJob, Task } from './Jobs'; // eslint-disable-line import/named -import { Message, StartMessage, UpdateMessage } from './Messages'; // eslint-disable-line import/named import UpdateHandler from './UpdateHandler'; type ErrorCallback = (message?: string) => void; @@ -20,52 +25,6 @@ export interface VehicleOptions { status?: VehicleStatus; } -/** - * Publicly accessible object that contains information about the vehicle. Modifying - * information of this object will not modify the actual vehicle class. - */ -export interface VehicleObject { - /** - * ID of the vehicle. - */ - vehicleId: number; - - /** - * Current status of the vehicle. - */ - status: VehicleStatus; - - /** - * Jobs of the vehicle. - */ - jobs: string[]; - - /** - * Current latitude of the vehicle. Starts at 0. - */ - lat: number; - - /** - * Current longitude of the vehicle. Starts at 0. - */ - lng: number; - - /** - * Current altitude of the vehicle. - */ - alt?: number; - /** - * - * Current battery of the vehicle, expressed as a decimal. Will vary from 0 to 1. - */ - battery?: number; - - /** - * Current vehicle heading. Value is in degrees. - */ - heading?: number; -} - /** * Contains data about a specific physical vehicle that the GCS will need to keep track * of it (missions, information, etc). @@ -74,13 +33,6 @@ export interface VehicleObject { * tasks. */ export default class Vehicle { - /** - * Forwards message to MessageHandler to send through Xbee. - */ - private static sendMessage(message: Message): void { - ipcRenderer.send('post', 'sendMessage', message); - } - /** * ID of the vehicle. */ @@ -181,7 +133,7 @@ export default class Vehicle { this.updateEventHandler.addHandler('battery', (battery, message): boolean => { if (battery > 1 || battery < 0) { - const vehicleInfo = message && message.sid && vehicleInfos[message.sid]; + const vehicleInfo = message && message.sid && vehicleConfig.vehicleInfos[message.sid]; ipcRenderer.send('post', 'updateMessages', { type: 'failure', @@ -254,6 +206,15 @@ export default class Vehicle { this.lastConnectionTime = Date.now(); } + /** + * Forwards message to MessageHandler to send through Xbee. + * + * @param message Message to send. + */ + private sendMessage(message: Message): void { + ipcRenderer.send('post', 'sendMessage', this.vehicleId, message); + } + /** * Notifies the vehicle that it will be performing a certain mission. We let the vehicle know of * the job type too, so that it knows which tasks to expect from us and discard any other @@ -291,7 +252,7 @@ export default class Vehicle { if (options) startMessage.options = options; // Sends the start message to the vehicle with corresponding job name. - Vehicle.sendMessage(startMessage); + this.sendMessage(startMessage); /* * Create handler that will sets the readyForMission back to true once the vehicle @@ -323,11 +284,11 @@ export default class Vehicle { * Returns false if the vehicle has not been assigned a mission or if the provided task * is not compatible with its job. */ - if (!this.readyForMission || !isValidTaskForJob(task, this.assignedJob)) { + if (!this.readyForMission || !vehicleConfig.isTaskTypeForJob(task.taskType, this.assignedJob)) { return false; } - Vehicle.sendMessage({ + this.sendMessage({ type: 'addMission', missionInfo: task, }); @@ -339,7 +300,7 @@ export default class Vehicle { * Sends stop message to vehicle. */ public stop(): void { - Vehicle.sendMessage({ + this.sendMessage({ type: 'stop', }); diff --git a/src/main/index.ts b/src/main/index.ts index 263deabd..ac79f62c 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -13,7 +13,7 @@ import { import fs from 'fs'; import moment from 'moment'; -import { images, locations } from '../static/index'; +import { imageConfig, locationConfig } from '../static/index'; /** * This key is required to enable geolocation in the application. @@ -44,7 +44,7 @@ const HEIGHT = 576; const isDevelopment = process.env.NODE_ENV !== 'production'; // TODO: Put icon tray back to macOS but resize it so that its not huge on macOS's menu. -const icon = nativeImage.createFromDataURL(images.icon as string); +const icon = nativeImage.createFromDataURL(imageConfig.icon as string); /** * Variable to keep track when the app will quit, which is different from hiding the app. @@ -251,7 +251,7 @@ function setLocationMenu(): void { // Cast the submenu variable to locationMenu as a MenuItemContsturctorOptions array. const locationMenu: MenuItemConstructorOptions[] = submenu as MenuItemConstructorOptions[]; - if (!locations || Object.keys(locations).length === 0) { + if (!locationConfig.locations || Object.keys(locationConfig.locations).length === 0) { locationMenu.push({ label: 'No locations defined', enabled: false, @@ -259,12 +259,12 @@ function setLocationMenu(): void { return; } - Object.keys(locations).forEach((label): void => { + Object.keys(locationConfig.locations).forEach((label): void => { locationMenu.push({ label, click: (menuItem): void => { if (mainWindow) { - mainWindow.webContents.send('updateMapLocation', locations[menuItem.label]); + mainWindow.webContents.send('updateMapLocation', locationConfig.locations[menuItem.label]); } }, }); diff --git a/src/renderer/fixtures/updateMessagesFixtures.ts b/src/renderer/fixtures/updateMessagesFixtures.ts index 6de0ca0c..a62442f3 100644 --- a/src/renderer/fixtures/updateMessagesFixtures.ts +++ b/src/renderer/fixtures/updateMessagesFixtures.ts @@ -1,7 +1,7 @@ import { ipcRenderer } from 'electron'; // TODO: Remove disable line comment when issue gets fixed (https://github.com/benmosher/eslint-plugin-import/pull/1304) -import { MessageType } from '../../util/types'; // eslint-disable-line import/named +import { MessageType } from '../../types/types'; // eslint-disable-line import/named const fixtures: { type?: MessageType; message: string }[] = [ { diff --git a/src/renderer/fixtures/updateVehiclesFixtures.ts b/src/renderer/fixtures/updateVehiclesFixtures.ts index 2a145555..c75616b2 100644 --- a/src/renderer/fixtures/updateVehiclesFixtures.ts +++ b/src/renderer/fixtures/updateVehiclesFixtures.ts @@ -1,11 +1,11 @@ import { ipcRenderer } from 'electron'; -import Vehicle, { VehicleObject, VehicleOptions } from '../../common/struct/Vehicle'; +import Vehicle, { VehicleOptions } from '../../common/struct/Vehicle'; -import { startLocation, vehicleStatuses } from '../../static/index'; +import { locationConfig, vehicleConfig } from '../../static/index'; // TODO: Remove disable line comment when issue gets fixed (https://github.com/benmosher/eslint-plugin-import/pull/1304) -import { VehicleStatus } from '../../util/types'; // eslint-disable-line import/named +import { VehicleObject, VehicleStatus } from '../../types/types'; // eslint-disable-line import/named const fixtureOptions: VehicleOptions[] = [ { @@ -38,13 +38,13 @@ const fixtureOptions: VehicleOptions[] = [ }, ]; -const status = Object.keys(vehicleStatuses); +const status = Object.keys(vehicleConfig.vehicleStatuses); const vehicles = fixtureOptions.map((options): Vehicle => new Vehicle(options)); vehicles.forEach((vehicle): void => { vehicle.getUpdateEventHandler().events({ - lat: startLocation.lat + (Math.random() / 1000) - 0.0005, - lng: startLocation.lng + (Math.random() / 1000) - 0.0005, + lat: locationConfig.startLocation.lat + (Math.random() / 1000) - 0.0005, + lng: locationConfig.startLocation.lng + (Math.random() / 1000) - 0.0005, }); }); diff --git a/src/renderer/index.js b/src/renderer/index.js index 6259a269..4724aed8 100644 --- a/src/renderer/index.js +++ b/src/renderer/index.js @@ -4,7 +4,7 @@ import { ipcRenderer } from 'electron'; import React, { Component } from 'react'; import ReactDOM from 'react-dom'; -import { fixtures, geolocation } from '../static/index'; +import { config } from '../static/index'; import MainWindow from './mainWindow/MainWindow'; import MissionWindow from './missionWindow/MissionWindow'; @@ -35,12 +35,12 @@ function runOnce() { require('../common/Orchestrator'); // Set up geolocation if geolocation is enabled in config. - if (geolocation) { + if (config.geolocation) { ipcRenderer.send('post', 'setMapToUserLocation'); } // Sets up fixtures if in development and fixtures are enabled in config. - if (isDevelopment && fixtures) { + if (isDevelopment && config.fixtures) { require('./fixtures/index'); } diff --git a/src/renderer/mainWindow/MainWindow.tsx b/src/renderer/mainWindow/MainWindow.tsx index 8647588a..c3ab60c1 100644 --- a/src/renderer/mainWindow/MainWindow.tsx +++ b/src/renderer/mainWindow/MainWindow.tsx @@ -1,6 +1,6 @@ import React, { ReactNode } from 'react'; -import { ThemeProps } from '../../util/types'; +import { ThemeProps } from '../../types/types'; import LogContainer from './log/LogContainer'; import MapContainer from './map/MapContainer'; diff --git a/src/renderer/mainWindow/log/LogContainer.tsx b/src/renderer/mainWindow/log/LogContainer.tsx index b8e8e645..bcfea271 100644 --- a/src/renderer/mainWindow/log/LogContainer.tsx +++ b/src/renderer/mainWindow/log/LogContainer.tsx @@ -10,7 +10,7 @@ import { } from 'react-virtualized'; // TODO: Remove disable line comment when issue gets fixed (https://github.com/benmosher/eslint-plugin-import/pull/1304) -import { isMessageType, MessageType, ThemeProps } from '../../../util/types'; // eslint-disable-line import/named +import { isMessageType, MessageType, ThemeProps } from '../../../types/types'; // eslint-disable-line import/named import './log.css'; diff --git a/src/renderer/mainWindow/map/MapContainer.tsx b/src/renderer/mainWindow/map/MapContainer.tsx index dc3f6c1d..a4a48cb2 100644 --- a/src/renderer/mainWindow/map/MapContainer.tsx +++ b/src/renderer/mainWindow/map/MapContainer.tsx @@ -10,16 +10,15 @@ import React, { } from 'react'; import { Map, TileLayer, Viewport } from 'react-leaflet'; -import { VehicleObject } from '../../../common/struct/Vehicle'; - -import { startLocation } from '../../../static/index'; +import { locationConfig } from '../../../static/index'; import { FileLoadOptions, FileSaveOptions, LatLngZoom, ThemeProps, -} from '../../../util/types'; + VehicleObject, +} from '../../../types/types'; import { updateVehicles } from '../../../util/util'; import GeolocationControl from './control/GeolocationControl'; @@ -60,6 +59,8 @@ export default class MapContainer extends Component { public constructor(props: ThemeProps) { super(props); + const { startLocation } = locationConfig; + this.state = { vehicles: {}, ...startLocation, diff --git a/src/renderer/mainWindow/map/VehicleMarker.tsx b/src/renderer/mainWindow/map/VehicleMarker.tsx index ab5ef20e..b7ee1dd8 100644 --- a/src/renderer/mainWindow/map/VehicleMarker.tsx +++ b/src/renderer/mainWindow/map/VehicleMarker.tsx @@ -2,21 +2,18 @@ import L from 'leaflet'; import React, { PureComponent, ReactNode } from 'react'; import { Marker, Tooltip } from 'react-leaflet'; -import { VehicleObject } from '../../../common/struct/Vehicle'; - import { - images, + imageConfig, RecursiveImageSignature, + vehicleConfig, VehicleInfo, - vehicleInfos, VehicleStatus, - vehicleStatuses, } from '../../../static/index'; // TODO: Remove disable line comment when issue gets fixed (https://github.com/benmosher/eslint-plugin-import/pull/1304) -import { MessageType } from '../../../util/types'; // eslint-disable-line import/named +import { MessageType, VehicleObject } from '../../../types/types'; // eslint-disable-line import/named -const vehicleIcons = (images.markers as RecursiveImageSignature) +const vehicleIcons = (imageConfig.markers as RecursiveImageSignature) .vehicles as RecursiveImageSignature; export interface VehicleMarkerProps { @@ -29,12 +26,12 @@ export interface VehicleMarkerProps { export default class VehicleMarker extends PureComponent { public render(): ReactNode { const { vehicle } = this.props; - const { name, type } = vehicleInfos[vehicle.vehicleId] as VehicleInfo; + const { name, type } = vehicleConfig.vehicleInfos[vehicle.vehicleId] as VehicleInfo; const { type: vehicleStatusTypeString, message, - } = vehicleStatuses[vehicle.status] as VehicleStatus; + } = vehicleConfig.vehicleStatuses[vehicle.status] as VehicleStatus; const vehicleStatusType = vehicleStatusTypeString as MessageType; const vehicleType = `${type}${vehicleStatusType === 'failure' ? '_red' : ''}`; @@ -42,7 +39,7 @@ export default class VehicleMarker extends PureComponent { { */ private static nameRenderer(props: TableCellProps): string { const { rowData: vehicle }: { rowData: VehicleObject } = props; - const { name } = vehicleInfos[vehicle.vehicleId] as VehicleInfo; + const { name } = vehicleConfig.vehicleInfos[vehicle.vehicleId] as VehicleInfo; return name; } @@ -61,7 +58,7 @@ export default class VehicleTable extends Component { */ private static statusRenderer(props: TableCellProps): ReactNode { const { rowData }: { rowData: VehicleObject } = props; - const vehicleStatus = vehicleStatuses[rowData.status] as VehicleStatus; + const vehicleStatus = vehicleConfig.vehicleStatuses[rowData.status] as VehicleStatus; return {vehicleStatus.message}; } diff --git a/src/renderer/missionWindow/missions/Mission.jsx b/src/renderer/missionWindow/missions/Mission.jsx index e58720e1..6b3a3b77 100644 --- a/src/renderer/missionWindow/missions/Mission.jsx +++ b/src/renderer/missionWindow/missions/Mission.jsx @@ -2,8 +2,6 @@ import { ipcRenderer } from 'electron'; import PropTypes from 'prop-types'; import React, { Component } from 'react'; -import { mission as missionUtil } from '../../../util/util'; - // TODO: Make this component more pretty. const Default = ({ job }) => (
@@ -11,7 +9,7 @@ const Default = ({ job }) => (

{job.description}

- +
); diff --git a/src/renderer/missionWindow/missions/jobs/Job.jsx b/src/renderer/missionWindow/missions/jobs/Job.jsx index a210e5a4..d45db00f 100644 --- a/src/renderer/missionWindow/missions/jobs/Job.jsx +++ b/src/renderer/missionWindow/missions/jobs/Job.jsx @@ -2,8 +2,6 @@ import { ipcRenderer } from 'electron'; import PropTypes from 'prop-types'; import React, { Component } from 'react'; -import { job as jobUtil, mission } from '../../../../util/util'; - const propTypes = { job: PropTypes.shape({ name: PropTypes.string.isRequired, @@ -60,24 +58,26 @@ export default class Job extends Component { } sendStartJob() { - const { job } = this.props; + // const { job } = this.props; this.setState({ startJobButton: false, pauseJobButton: true, }); - jobUtil.sendStartJob({ - jobType: job.name, - // TODO: Add missionInfo. - }); + /* + * jobUtil.sendStartJob({ + * jobType: job.name, + * // TODO: Add missionInfo. + * }); + */ } sendStopMission() { // Reset state for future use of this job. this.setState({ ...defaultState }); - mission.sendStopMission(); + // mission.sendStopMission(); } sendPauseJob() { @@ -86,7 +86,7 @@ export default class Job extends Component { resumeJobButton: true, }); - jobUtil.sendPauseJob(); + // jobUtil.sendPauseJob(); } sendResumeJob() { @@ -95,7 +95,7 @@ export default class Job extends Component { pauseJobButton: true, }); - jobUtil.sendResumeJob(); + // jobUtil.sendResumeJob(); } sendNextJob() { @@ -109,7 +109,7 @@ export default class Job extends Component { // Reset state for future use of this job. this.setState({ ...defaultState }); - mission.sendCompleteMission(); + // mission.sendCompleteMission(); } render() { diff --git a/src/static/config.json b/src/static/config.json index e1316b67..23b70a60 100644 --- a/src/static/config.json +++ b/src/static/config.json @@ -1,4 +1,7 @@ { "fixtures": false, - "geolocation": true + "geolocation": true, + + "vehicleDisconnectionTime": 20, + "messageSendRate": 0.1 } diff --git a/src/static/index.ts b/src/static/index.ts index a5228f11..a9cb6052 100644 --- a/src/static/index.ts +++ b/src/static/index.ts @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/camelcase */ -import { LatLngZoom } from '../util/types'; +import { LatLngZoom } from '../types/types'; import ngcp_calpoly from './images/logo/ngcp_calpoly.png'; import ngcp_pomona from './images/logo/ngcp_pomona.png'; @@ -25,17 +25,17 @@ import arrow from './images/arrow.png'; import icon from './images/icon.png'; import pin from './images/pin.png'; -import { fixtures, geolocation } from './config.json'; +import config from './config.json'; import { startLocation as startLocationString, locations as locationsObject } from './locations.json'; import { - vehicleDisconnectionTime, vehicleIds as vehicleIdsObject, vehicleInfos as vehicleInfosObject, + vehicleJobs as vehicleJobsObject, vehicleStatuses as vehicleStatusesObject, } from './vehicle.json'; -// Export json objects with signature to allow us to access it with TypeScript. -export const locations: { +// Add signature to json objects to allow us to access it with TypeScript. +const locations: { [name: string]: { lat: number; lng: number; zoom?: number } | undefined; } = locationsObject; @@ -52,10 +52,14 @@ export interface VehicleInfo { 'type': string; } -export const vehicleInfos: { +const vehicleInfos: { [id: string]: VehicleInfo | undefined; } = vehicleInfosObject; +const vehicleJobs: { + [jobType: string]: string[] | undefined; +} = vehicleJobsObject; + /** * Object contained in vehicleStatuses in vehicle.json. */ @@ -64,19 +68,47 @@ export interface VehicleStatus { message: string; } -export const vehicleStatuses: { +const vehicleStatuses: { [status: string]: VehicleStatus | undefined; } = vehicleStatusesObject; // Add logic to set startLocation. -export const startLocation: LatLngZoom = startLocationString && locations[startLocationString] +const startLocation: LatLngZoom = startLocationString && locations[startLocationString] ? locations[startLocationString] as LatLngZoom : { lat: 0, lng: 0, zoom: 18, }; -export { fixtures, geolocation, vehicleDisconnectionTime }; +/** + * Checks if a string is a valid job type. + */ +function isJobType(jobType: string): boolean { + return vehicleJobs[jobType] !== undefined; +} + +/** + * Checks if a task is a valid task type for that job. + */ +function isTaskTypeForJob(taskType: string, jobType: string): boolean { + return isJobType(jobType) && (vehicleJobs[jobType] as string[]).indexOf(taskType) >= 0; +} + +export const locationConfig = { + locations, + startLocation, +}; + +export const vehicleConfig = { + vehicleIds, + vehicleInfos, + vehicleStatuses, + vehicleJobs, + isTaskTypeForJob, + isJobType, +}; + +export { default as config } from './config.json'; /** * A given key for an image will give either a string or an object with similar structure. @@ -85,7 +117,7 @@ export interface RecursiveImageSignature { [key: string]: string | RecursiveImageSignature; } -export const images: RecursiveImageSignature = { +export const imageConfig: RecursiveImageSignature = { arrow, icon, pin, @@ -112,13 +144,8 @@ export const images: RecursiveImageSignature = { }; export default { - fixtures, - geolocation, - images, - locations, - startLocation, - vehicleDisconnectionTime, - vehicleIds, - vehicleInfos, - vehicleStatuses, + config, + imageConfig, + locationConfig, + vehicleConfig, }; diff --git a/src/static/vehicle.json b/src/static/vehicle.json index 7b198906..00800e68 100644 --- a/src/static/vehicle.json +++ b/src/static/vehicle.json @@ -1,5 +1,4 @@ { - "vehicleDisconnectionTime": 20, "vehicleIds": { "UAV": 100, "UGV": 200, @@ -46,6 +45,14 @@ "type": "plane" } }, + "vehicleJobs": { + "isrSearch": ["takeoff", "loiter", "isrSearch", "land"], + "payloadDrop": ["takeoff", "loiter", "payloadDrop", "land"], + "ugvRetrieve": ["retrieveTarget", "deliverTarget"], + "uuvRetrieve": ["retrieveTarget"], + "quickScan": ["quickScan"], + "detailedSearch": ["detailedSearch"] + }, "vehicleStatuses": { "ready": { "type": "success", diff --git a/src/types/messages.ts b/src/types/messages.ts new file mode 100644 index 00000000..84fdcf86 --- /dev/null +++ b/src/types/messages.ts @@ -0,0 +1,352 @@ +/* + * Definitions and typeguards for all messages that are sent between GCS and vehicles. + * See the wiki for more information: https://github.com/NGCP/GCS/wiki/GCS-JSON-Messages + * + * Includes definitions for jobs and tasks too, as those are part of a message. + */ + +import { vehicleConfig } from '../static/index'; + +// TODO: Remove disable line comment when issue gets fixed (https://github.com/benmosher/eslint-plugin-import/pull/1304) +import { VehicleStatus } from './types'; // eslint-disable-line import/named + +// Definitions for all tasks. + +interface TaskBase { + taskType: string; +} + +export interface Takeoff extends TaskBase { + taskType: 'takeoff'; + + lat: number; + lng: number; + alt: number; + loiter: { + lat: number; + lng: number; + alt: number; + radius: number; + direction: number; + }; +} + +export interface Loiter extends TaskBase { + taskType: 'loiter'; + + lat: number; + lng: number; + alt: number; + radius: number; + direction: number; +} + +// eslint-disable-next-line @typescript-eslint/interface-name-prefix +export interface ISRSearch extends TaskBase { + taskType: 'isrSearch'; + + alt: number; + waypoints: [ + { + lat: number; + lng: number; + }, + { + lat: number; + lng: number; + }, + { + lat: number; + lng: number; + } + ]; +} + +export interface PayloadDrop extends TaskBase { + taskType: 'payloadDrop'; + + waypoints: [ + { + lat: number; + lng: number; + alt: number; + }, + { + lat: number; + lng: number; + alt: number; + } + ]; +} + +export interface Land extends TaskBase { + taskType: 'land'; + + waypoints: [ + { + lat: number; + lng: number; + alt: number; + }, + { + lat: number; + lng: number; + alt: number; + } + ]; +} + +export interface UGVRetrieveTarget extends TaskBase { + taskType: 'retrieveTarget'; + + lat: number; + lng: number; +} + +export interface DeliverTarget extends TaskBase { + taskType: 'deliverTarget'; + + lat: number; + lng: number; +} + +export interface UUVRetrieveTarget extends TaskBase { + taskType: 'retrieveTarget'; +} + +export interface QuickScan extends TaskBase { + taskType: 'quickScan'; + + searchArea: { + center: [number, number]; + rad1: number; + rad2: number; + }; +} + +export interface DetailedSearch extends TaskBase { + taskType: 'detailedSearch'; + + lat: number; + lng: number; +} + +export type Task = Takeoff | Loiter | ISRSearch | PayloadDrop | Land | UGVRetrieveTarget +| DeliverTarget | UUVRetrieveTarget | QuickScan | DetailedSearch; + +/* + * Definitions for all messages from GCS to vehicles. No need for type guards, TypeScript + * will handle all of it as we write up the message. + */ + +interface MessageBase { + /** + * Type of message. + */ + type: string; +} + +export interface StartMessage extends MessageBase { + type: 'start'; + + /** + * Name of job to perform. + */ + jobType: string; + + /** + * Any other information the vehicle should have before performing the job. + */ + options?: object; +} + +export interface AddMissionMessage extends MessageBase { + type: 'addMission'; + + /** + * Information related to accomplishing specific job. + */ + missionInfo: Task; +} + +export interface PauseMessage extends MessageBase { + type: 'pause'; +} + +export interface ResumeMessage extends MessageBase { + type: 'resume'; +} + +export interface StopMessage extends MessageBase { + type: 'stop'; +} + +export interface ConnectionAckMessage extends MessageBase { + type: 'connectionAck'; +} + +/* + * Definitions for all messages from vehicles to GCS. Type guards are needed for these, + * as we do not know the types of the messages we are receiving and whether or not they're + * valid. + */ + +/** + * Checks if the object is a proper message. + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function isMessage(message: { [key: string]: any }): boolean { + const typeCheck = message.type && [ + 'start', + 'addMission', + 'pause', + 'resume', + 'stop', + 'connectionAck', + 'update', + 'poi', + 'complete', + 'connect', + 'ack', + 'badMessage', + ].indexOf(message.type) >= 0; + + // Check if id is a number. + const idCheck = message.id !== '' && message.id !== true && !Number.isNaN(message.id); + + // Check if tid is a number and that tid is 0 (vehicle id of GCS). + const tidCheck = !Number.isNaN(message.tid) && message.tid === 0; + + // Check if sid is a number and is valid. + const sidCheck = !Number.isNaN(message.sid) + && vehicleConfig.vehicleInfos[message.sid] !== undefined; + + return typeCheck && idCheck && tidCheck && sidCheck; +} + +export interface UpdateMessage extends MessageBase { + type: 'update'; + + /** + * Latitude of the vehicle. + */ + lat: number; + + /** + * Longitude of the vehicle. + */ + lng: number; + + /** + * Altitude of the vehicle. + */ + alt?: number; + + /** + * Current vehicle heading. Value is in degrees. + */ + heading?: number; + + /** + * Current battery of the vehicle, expressed as a decimal. Will vary from 0 to 1. + */ + battery?: number; + + /** + * Status of the vehicle. + */ + status: VehicleStatus; + + /** + * Message generated if vehicle is in an error state. + */ + errorMessage?: string; +} + +/* eslint-disable @typescript-eslint/no-unused-vars */ + +// TODO: Create type guards for messages received. +export function isUpdateMessage(message: Message): boolean { + return false; +} + +export interface POIMessage extends MessageBase { + type: 'poi'; + + /** + * Latitude of the point of interest. + */ + lat: number; + + /** + * Longitude of the point of interest. + */ + lng: number; +} + +export function isPOIMessage(message: Message): boolean { + return false; +} + +export interface CompleteMessage extends MessageBase { + type: 'complete'; +} + +export function isCompleteMessage(message: Message): boolean { + return false; +} + +export interface ConnectMessage extends MessageBase { + type: 'connect'; + + /** + * List of all the different jobs the vehicle is capable of doing. + */ + jobsAvailable: string[]; +} + +export function isConnectMessage(message: Message): boolean { + return false; +} + +// 4. Definitions for all other message types. + +export interface AcknowledgementMessage extends MessageBase { + type: 'ack'; + + /** + * ID of the message that is being acknowledged. + */ + ackid: number; +} + +export function isAcknowledgementMessage(message: Message): boolean { + return false; +} + +export interface BadMessageMessage extends MessageBase { + type: 'badMessage'; + + /** + * Description of why message was bad. + */ + error: string; +} + +export function isBadMessageMessage(message: Message): boolean { + return false; +} + +export type Message = StartMessage | AddMissionMessage | PauseMessage | ResumeMessage | StopMessage +| ConnectionAckMessage | UpdateMessage | POIMessage | CompleteMessage | ConnectMessage +| AcknowledgementMessage | BadMessageMessage; + +export default { + isMessage, + isUpdateMessage, + isPOIMessage, + isCompleteMessage, + isConnectMessage, + isAcknowledgementMessage, + isBadMessageMessage, +}; diff --git a/src/util/types.ts b/src/types/types.ts similarity index 73% rename from src/util/types.ts rename to src/types/types.ts index 02a039e0..bf2ad7ad 100644 --- a/src/util/types.ts +++ b/src/types/types.ts @@ -1,16 +1,8 @@ -/* - * Types for variables used in various parts in GCS. - */ - /** - * Types for all vehicle status. These statuses are generated by the vehicle depending on the - * mission or its status, generated by the Orchestrator. + * Status of the vehicle, lets us know of where the vehicle is in its mission. */ export type VehicleStatus = 'ready' | 'error' | 'disconnected' | 'waiting' | 'running' | 'paused'; -/** - * Type guard for VehicleStatus. - */ export function isVehicleStatus(status: string): boolean { return status === 'ready' || status === 'error' || status === 'disconnected' || status === 'waiting' || status === 'running' || status === 'paused'; } @@ -25,9 +17,6 @@ export interface ThemeProps { theme: 'light' | 'dark'; } -/** - * Type guard to ThemeProps. Checks if the theme property is "light" or "dark". - */ export function isThemeProps(props: { theme: string }): boolean { return props.theme === 'light' || props.theme === 'dark'; } @@ -37,15 +26,12 @@ export function isThemeProps(props: { theme: string }): boolean { */ export type MessageType = '' | 'success' | 'failure' | 'progress'; -/** - * Type guard to MessageType. checks if the type is "success", "failure", "progress", or "". - */ export function isMessageType(type: string): boolean { return type === '' || type === 'success' || type === 'failure' || type === 'progress'; } /** - * Types for maps to use. + * Latitude, longitude and zoom. */ export interface LatLngZoom { /** @@ -64,6 +50,52 @@ export interface LatLngZoom { zoom?: number; } +/** + * Vehicle object for all classes to use. This is necessary because Vehicle class will + * be uncasted when being sent through ipcRenderer. + */ +export interface VehicleObject { + /** + * ID of the vehicle. + */ + vehicleId: number; + + /** + * Current status of the vehicle. + */ + status: VehicleStatus; + + /** + * Jobs of the vehicle. + */ + jobs: string[]; + + /** + * Current latitude of the vehicle. Starts at 0. + */ + lat: number; + + /** + * Current longitude of the vehicle. Starts at 0. + */ + lng: number; + + /** + * Current altitude of the vehicle. + */ + alt?: number; + /** + * + * Current battery of the vehicle, expressed as a decimal. Will vary from 0 to 1. + */ + battery?: number; + + /** + * Current vehicle heading. Value is in degrees. + */ + heading?: number; +} + /** * Status of a mission. */ diff --git a/src/util/util.ts b/src/util/util.ts index ccbf105f..c10a0fff 100644 --- a/src/util/util.ts +++ b/src/util/util.ts @@ -1,7 +1,6 @@ -import { ipcRenderer } from 'electron'; import { Component } from 'react'; -import { VehicleObject } from '../common/struct/Vehicle'; +import { VehicleObject } from '../types/types'; /** * Updates vehicles being shown. This is run on map and vehicle containers. @@ -20,59 +19,20 @@ export function updateVehicles( component.setState({ vehicles: currentVehicles }); } -/* - * These functions will allow us to maintain mission states (starting missions, job, etc.) - * I'll add explanation here soon. All explanations are in Slack right now. - * The way we should name functions that interact with ipcRenderer are the following: - * Assuming we have the notification "startMission", the function startMission() is the - * callback to when the notification is received. The function sendStartMission() is the - * function that sends out this notification. +/** + * Checks if the string is a JSON. Should be called before isMessage(). */ +export function isJSON(message: string): boolean { + if (!message) return false; -function sendStartMission(): void { - ipcRenderer.send('post', 'startMission'); -} - -function sendStopMission(): void { - ipcRenderer.send('post', 'stopMission'); -} - -function sendCompleteMission(index: number): void { - ipcRenderer.send('post', 'completeMission', index); -} - -export const mission = { - sendStartMission, - sendStopMission, - sendCompleteMission, -}; - -function sendStartJob(data: { - jobType: string; - missionInfo: {}; -}): void { - ipcRenderer.send('post', 'startJob', data); -} - -function sendPauseJob(): void { - ipcRenderer.send('post', 'pauseJob'); -} - -function sendResumeJob(): void { - ipcRenderer.send('post', 'resumeJob'); -} - -function sendCompleteJob(): void { - ipcRenderer.send('post', 'completeJob'); + try { + JSON.parse(message); + } catch (e) { + return false; + } + return true; } -export const job = { - sendStartJob, - sendPauseJob, - sendResumeJob, - sendCompleteJob, -}; - /* eslint-disable no-bitwise */ /** @@ -109,13 +69,11 @@ function toFloat(hexString: string): number { return 0; } -export const floatHex = { toHexString, toFloat }; - /* eslint-enable no-bitwise */ export default { - floatHex, - job, - mission, + isJSON, + toFloat, + toHexString, updateVehicles, }; From 37ea608d5286c40fab45513466343e62caf7051f Mon Sep 17 00:00:00 2001 From: Paul Matthew Barrameda Date: Mon, 8 Apr 2019 15:11:09 -0700 Subject: [PATCH 057/152] Version bump --- package.json | 16 ++++++++-------- src/main/index.ts | 2 +- src/renderer/index.js | 6 +++--- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/package.json b/package.json index e9a66360..7aec8b38 100644 --- a/package.json +++ b/package.json @@ -1,13 +1,13 @@ { "name": "GCS", - "version": "0.6.2", + "version": "0.6.3", "license": "MIT", "author": "Northrop Grumman Collaboration Project", "description": "Ground Control Station for autonomous vehicle platforms in NGCP", "repository": "https://github.com/NGCP/GCS", "scripts": { "postinstall": "electron-rebuild", - "test": "npm run test:lint & npm run test:types & npm run test:unit", + "test": "npm run test:lint && npm run test:types && npm run test:unit", "test:lint": "eslint --ext .js,.jsx,.ts,.tsx \".\" & stylelint \"**/*.css\"", "test:types": "tsc --project . --outDir dist --pretty", "test:unit": "mocha --require ts-node/register test/**/*.test.ts", @@ -27,16 +27,16 @@ "react-leaflet-control": "^2.1.1", "react-virtualized": "^9.21.0", "serialport": "^7.1.4", - "source-map-support": "^0.5.11", + "source-map-support": "^0.5.12", "xbee-api": "^0.6.0" }, "devDependencies": { "@babel/preset-react": "^7.0.0", "@types/chai": "^4.1.7", "@types/mocha": "^5.2.6", - "@types/node": "^11.13.0", + "@types/node": "^11.13.1", "@types/pouchdb": "^6.3.3", - "@types/react": "^16.8.12", + "@types/react": "^16.8.13", "@types/react-dom": "^16.8.3", "@types/react-leaflet": "^2.2.1", "@types/react-virtualized": "^9.21.1", @@ -58,13 +58,13 @@ "eslint-plugin-import": "^2.16.0", "eslint-plugin-jsx-a11y": "^6.2.1", "eslint-plugin-react": "^7.12.4", - "mocha": "^6.0.2", + "mocha": "^6.1.2", "stylelint": "^9.10.1", "stylelint-config-standard": "^18.2.0", - "stylelint-order": "^2.1.0", + "stylelint-order": "^2.2.0", "ts-mocha": "^6.0.0", "ts-node": "^8.0.3", - "typescript": "^3.3.4000", + "typescript": "^3.4.2", "webpack": "^4.29.6" }, "electronWebpack": { diff --git a/src/main/index.ts b/src/main/index.ts index ac79f62c..f9875f05 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -147,7 +147,7 @@ function loadConfig(): void { }); // Returns if the user chooses to close the window instead of choosing a file path. - if (filePaths.length === 0) return; + if (!filePaths || filePaths.length === 0) return; // TODO: Add type for data. const data = JSON.parse(fs.readFileSync(filePaths[0]).toString()); diff --git a/src/renderer/index.js b/src/renderer/index.js index 4724aed8..7b22c788 100644 --- a/src/renderer/index.js +++ b/src/renderer/index.js @@ -4,7 +4,7 @@ import { ipcRenderer } from 'electron'; import React, { Component } from 'react'; import ReactDOM from 'react-dom'; -import { config } from '../static/index'; +import statics from '../static/index'; import MainWindow from './mainWindow/MainWindow'; import MissionWindow from './missionWindow/MissionWindow'; @@ -35,12 +35,12 @@ function runOnce() { require('../common/Orchestrator'); // Set up geolocation if geolocation is enabled in config. - if (config.geolocation) { + if (statics.config.geolocation) { ipcRenderer.send('post', 'setMapToUserLocation'); } // Sets up fixtures if in development and fixtures are enabled in config. - if (isDevelopment && config.fixtures) { + if (isDevelopment && statics.config.fixtures) { require('./fixtures/index'); } From 4e16d2b988c0dddc9d143557fe218a3ee64a47a7 Mon Sep 17 00:00:00 2001 From: Paul Matthew Barrameda Date: Mon, 8 Apr 2019 15:33:36 -0700 Subject: [PATCH 058/152] Added type guards to messages --- src/types/messages.ts | 28 ++++++++++++++++++++-------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/src/types/messages.ts b/src/types/messages.ts index 84fdcf86..b2961f57 100644 --- a/src/types/messages.ts +++ b/src/types/messages.ts @@ -8,7 +8,7 @@ import { vehicleConfig } from '../static/index'; // TODO: Remove disable line comment when issue gets fixed (https://github.com/benmosher/eslint-plugin-import/pull/1304) -import { VehicleStatus } from './types'; // eslint-disable-line import/named +import { isVehicleStatus, VehicleStatus } from './types'; // eslint-disable-line import/named // Definitions for all tasks. @@ -259,6 +259,8 @@ export interface UpdateMessage extends MessageBase { /** * Message generated if vehicle is in an error state. + * + * This field is REQUIRED if the vehicle is in an error state. */ errorMessage?: string; } @@ -267,7 +269,11 @@ export interface UpdateMessage extends MessageBase { // TODO: Create type guards for messages received. export function isUpdateMessage(message: Message): boolean { - return false; + return message.type === 'update' + && !Number.isNaN(message.lat) + && !Number.isNaN(message.lng) + && isVehicleStatus(message.status) + && message.status === 'error' ? message.errorMessage !== undefined : true; } export interface POIMessage extends MessageBase { @@ -285,7 +291,9 @@ export interface POIMessage extends MessageBase { } export function isPOIMessage(message: Message): boolean { - return false; + return message.type === 'poi' + && !Number.isNaN(message.lat) + && !Number.isNaN(message.lng); } export interface CompleteMessage extends MessageBase { @@ -293,7 +301,7 @@ export interface CompleteMessage extends MessageBase { } export function isCompleteMessage(message: Message): boolean { - return false; + return message.type === 'complete'; } export interface ConnectMessage extends MessageBase { @@ -306,7 +314,7 @@ export interface ConnectMessage extends MessageBase { } export function isConnectMessage(message: Message): boolean { - return false; + return message.type === 'connect'; } // 4. Definitions for all other message types. @@ -321,7 +329,8 @@ export interface AcknowledgementMessage extends MessageBase { } export function isAcknowledgementMessage(message: Message): boolean { - return false; + return message.type === 'ack' + && !Number.isNaN(message.ackid); } export interface BadMessageMessage extends MessageBase { @@ -329,12 +338,15 @@ export interface BadMessageMessage extends MessageBase { /** * Description of why message was bad. + * + * It is required but the message should still be processed as a bad message + * if no string was provided by the vehicle. */ - error: string; + error?: string; } export function isBadMessageMessage(message: Message): boolean { - return false; + return message.type === 'badMessage'; } export type Message = StartMessage | AddMissionMessage | PauseMessage | ResumeMessage | StopMessage From 40e35ca0edf9463b6679fc907a459df1dc94ff73 Mon Sep 17 00:00:00 2001 From: Paul Matthew Barrameda Date: Mon, 8 Apr 2019 16:00:26 -0700 Subject: [PATCH 059/152] Updated type guards Fixed one small thing with connect message as well as removed the requirement for an errorMessage when a vehicle is in an error state. Also removed the requirement for an error field in badMessage message, but we will be filling that field in whenever we send a badMessage. This is in the case a vehicle does not provide an error field in the badMessage message. --- src/types/messages.ts | 34 +++++++++++++++++++++++++++------- 1 file changed, 27 insertions(+), 7 deletions(-) diff --git a/src/types/messages.ts b/src/types/messages.ts index b2961f57..b87a2b27 100644 --- a/src/types/messages.ts +++ b/src/types/messages.ts @@ -260,20 +260,26 @@ export interface UpdateMessage extends MessageBase { /** * Message generated if vehicle is in an error state. * - * This field is REQUIRED if the vehicle is in an error state. + * It is best practice to provide this value to be able to see why the the vehicle's + * status is error. */ errorMessage?: string; } /* eslint-disable @typescript-eslint/no-unused-vars */ -// TODO: Create type guards for messages received. export function isUpdateMessage(message: Message): boolean { + // Check if message type is "update". return message.type === 'update' + + // Check if lat is a number. && !Number.isNaN(message.lat) + + // Check if lng is a number. && !Number.isNaN(message.lng) - && isVehicleStatus(message.status) - && message.status === 'error' ? message.errorMessage !== undefined : true; + + // Check if status is a valid vehicle status. + && isVehicleStatus(message.status); } export interface POIMessage extends MessageBase { @@ -291,8 +297,13 @@ export interface POIMessage extends MessageBase { } export function isPOIMessage(message: Message): boolean { + // Check if message type is "poi". return message.type === 'poi' + + // Check if lat is a number. && !Number.isNaN(message.lat) + + // Check if lng is a number. && !Number.isNaN(message.lng); } @@ -301,6 +312,7 @@ export interface CompleteMessage extends MessageBase { } export function isCompleteMessage(message: Message): boolean { + // Check if message type is "complete". return message.type === 'complete'; } @@ -314,7 +326,11 @@ export interface ConnectMessage extends MessageBase { } export function isConnectMessage(message: Message): boolean { - return message.type === 'connect'; + // Check if message type is "connect". + return message.type === 'connect' + + // Check if jobsAvailable is a string array of valid job types. + && message.jobsAvailable.every(vehicleConfig.isJobType); } // 4. Definitions for all other message types. @@ -329,7 +345,10 @@ export interface AcknowledgementMessage extends MessageBase { } export function isAcknowledgementMessage(message: Message): boolean { + // Check if message type is "ack". return message.type === 'ack' + + // Check if ackid is a number. && !Number.isNaN(message.ackid); } @@ -339,13 +358,14 @@ export interface BadMessageMessage extends MessageBase { /** * Description of why message was bad. * - * It is required but the message should still be processed as a bad message - * if no string was provided by the vehicle. + * It is best practice to provide this value to be able to see why the message received + * is bad. */ error?: string; } export function isBadMessageMessage(message: Message): boolean { + // Check if message type is "badMessage". return message.type === 'badMessage'; } From dd994d4321b31738b728e775040741f8a7fee375 Mon Sep 17 00:00:00 2001 From: Paul Matthew Barrameda Date: Mon, 8 Apr 2019 22:00:07 -0700 Subject: [PATCH 060/152] Finished messages typings Got lazy commenting the code at the end but screw it. I think I commented the important parts, but commenting all code is better. --- src/common/MessageHandler.ts | 4 +- src/common/Xbee.ts | 17 +- src/static/index.ts | 4 +- src/static/vehicle.json | 5 + src/types/messages.ts | 453 ++++++++++++++++++++++++++--------- src/util/util.ts | 40 ---- 6 files changed, 348 insertions(+), 175 deletions(-) diff --git a/src/common/MessageHandler.ts b/src/common/MessageHandler.ts index 3c83220d..05a41f72 100644 --- a/src/common/MessageHandler.ts +++ b/src/common/MessageHandler.ts @@ -4,10 +4,10 @@ import { config } from '../static/index'; // TODO: Remove disable line comment when issue gets fixed (https://github.com/benmosher/eslint-plugin-import/pull/1304) import DictionaryList from './struct/DictionaryList'; -import { Message } from '../types/messages'; // eslint-disable-line import/named +import { JSONMessage, Message } from '../types/messages'; // eslint-disable-line import/named import UpdateHandler from './struct/UpdateHandler'; -import xbee, { JSONMessage } from './Xbee'; // eslint-disable-line import/named +import xbee from './Xbee'; // eslint-disable-line import/named export default class MessageHandler { /** diff --git a/src/common/Xbee.ts b/src/common/Xbee.ts index c512a0d0..e88a6dd7 100644 --- a/src/common/Xbee.ts +++ b/src/common/Xbee.ts @@ -7,22 +7,7 @@ import { constants as C, Frame, XBeeAPI } from 'xbee-api'; import { VehicleInfo, vehicleConfig } from '../static/index'; // TODO: Remove disable line comment when issue gets fixed (https://github.com/benmosher/eslint-plugin-import/pull/1304) -import { Message } from '../types/messages'; // eslint-disable-line import/named - -/** - * Requirements for all messages that are sent/received. - */ -interface MessageRequirements { - id: number; - sid: 0; - tid: number; - time: number; -} - -/** - * Message with id, sid, tid, and time. - */ -export type JSONMessage = Message & MessageRequirements; +import { JSONMessage } from '../types/messages'; // eslint-disable-line import/named /* * TODO: Add a feature with Vehicle container to change this dynamically and reconnect. diff --git a/src/static/index.ts b/src/static/index.ts index a9cb6052..aef0c11b 100644 --- a/src/static/index.ts +++ b/src/static/index.ts @@ -100,12 +100,12 @@ export const locationConfig = { }; export const vehicleConfig = { + isJobType, + isTaskTypeForJob, vehicleIds, vehicleInfos, vehicleStatuses, vehicleJobs, - isTaskTypeForJob, - isJobType, }; export { default as config } from './config.json'; diff --git a/src/static/vehicle.json b/src/static/vehicle.json index 00800e68..e945a3d4 100644 --- a/src/static/vehicle.json +++ b/src/static/vehicle.json @@ -9,6 +9,11 @@ "Blimp": 600 }, "vehicleInfos": { + "0": { + "macAddress": "", + "name": "GCS", + "type": "station" + }, "100": { "macAddress": "", "name": "UAV", diff --git a/src/types/messages.ts b/src/types/messages.ts index b87a2b27..fe2470e6 100644 --- a/src/types/messages.ts +++ b/src/types/messages.ts @@ -2,7 +2,7 @@ * Definitions and typeguards for all messages that are sent between GCS and vehicles. * See the wiki for more information: https://github.com/NGCP/GCS/wiki/GCS-JSON-Messages * - * Includes definitions for jobs and tasks too, as those are part of a message. + * Includes definitions for tasks too, as those are part of a message. */ import { vehicleConfig } from '../static/index'; @@ -10,134 +10,291 @@ import { vehicleConfig } from '../static/index'; // TODO: Remove disable line comment when issue gets fixed (https://github.com/benmosher/eslint-plugin-import/pull/1304) import { isVehicleStatus, VehicleStatus } from './types'; // eslint-disable-line import/named +/** + * A float represented as a hexadecimal string. + * This is used to reduce the size of messages being sent between vehicles. + * Uses IEEE 754 standard to convert float to hex (and vice versa). + */ +export type HexFloat = string; + +/** + * Type guard for a hex float. + */ +export function isHexFloat(hex: string): boolean { + const modifiedHex = hex.startsWith('0x') ? hex.slice(2) : hex; + return modifiedHex.match(/^[0-9a-fA-F]+$/) !== null; +} + +/** + * Converts a float number to a hex float. + */ +/* eslint-disable no-bitwise */ +export function toHexFloat(float: number): HexFloat { + const getHex = (index: number): string => `00${index.toString(16)}`.slice(-2); + + const view = new DataView(new ArrayBuffer(4)); + view.setFloat32(0, float); + return `0x${[0, 0, 0, 0].map((_, index): string => getHex(view.getUint8(index))).join('')}`; +} + +/** + * Converts a hex string to a float number. + */ +export function toFloat(hex: HexFloat): number { + const int = parseInt(hex, 16); + if (int === 0) return 0; + + const sign = (int >>> 31) ? -1 : 1; + let exp = ((int >>> 23) & 0xff) - 127; + const mantissa = ((int & 0x7fffff) + 0x800000).toString(2); + let float = 0; + + for (let i = 0; i < mantissa.length; i += 1, exp -= 1) { + if (parseInt(mantissa[i], 10)) { + float += 2 ** exp; + } + } + + return float * sign; +} +/* eslint-enable no-bitwise */ + // Definitions for all tasks. interface TaskBase { taskType: string; } -export interface Takeoff extends TaskBase { +export interface TakeoffTask extends TaskBase { taskType: 'takeoff'; - lat: number; - lng: number; - alt: number; + lat: HexFloat; + lng: HexFloat; + alt: HexFloat; loiter: { - lat: number; - lng: number; - alt: number; - radius: number; - direction: number; + lat: HexFloat; + lng: HexFloat; + alt: HexFloat; + radius: HexFloat; + direction: HexFloat; }; } -export interface Loiter extends TaskBase { +export function isTakeoffTask(task: Task): boolean { + return task.taskType === 'takeoff' + && isHexFloat(task.lat) + && isHexFloat(task.lng) + && isHexFloat(task.alt) + + && task.loiter + && isHexFloat(task.loiter.lat) + && isHexFloat(task.loiter.lng) + && isHexFloat(task.loiter.alt) + && isHexFloat(task.loiter.radius) + && isHexFloat(task.loiter.direction); +} + +export interface LoiterTask extends TaskBase { taskType: 'loiter'; - lat: number; - lng: number; - alt: number; - radius: number; - direction: number; + lat: HexFloat; + lng: HexFloat; + alt: HexFloat; + radius: HexFloat; + direction: HexFloat; +} + +export function isLoiterTask(task: Task): boolean { + return task.taskType === 'loiter' + && isHexFloat(task.lat) + && isHexFloat(task.lng) + && isHexFloat(task.alt) + && isHexFloat(task.radius) + && isHexFloat(task.direction); } // eslint-disable-next-line @typescript-eslint/interface-name-prefix -export interface ISRSearch extends TaskBase { +export interface ISRSearchTask extends TaskBase { taskType: 'isrSearch'; - alt: number; + alt: HexFloat; waypoints: [ { - lat: number; - lng: number; + lat: HexFloat; + lng: HexFloat; }, { - lat: number; - lng: number; + lat: HexFloat; + lng: HexFloat; }, { - lat: number; - lng: number; + lat: HexFloat; + lng: HexFloat; } ]; } -export interface PayloadDrop extends TaskBase { +export function isISRSearchTask(task: Task): boolean { + return task.taskType === 'isrSearch' + && isHexFloat(task.alt) + + && task.waypoints && task.waypoints.length === 3 + && task.waypoints.every( + (waypoint): boolean => isHexFloat(waypoint.lat) && isHexFloat(waypoint.lng), + ); +} + +export interface PayloadDropTask extends TaskBase { taskType: 'payloadDrop'; waypoints: [ { - lat: number; - lng: number; - alt: number; + lat: HexFloat; + lng: HexFloat; + alt: HexFloat; }, { - lat: number; - lng: number; - alt: number; + lat: HexFloat; + lng: HexFloat; + alt: HexFloat; } ]; } -export interface Land extends TaskBase { +export function isPayloadDropTask(task: Task): boolean { + return task.taskType === 'payloadDrop' + && task.waypoints && task.waypoints.length === 2 + && task.waypoints.every( + (waypoint): boolean => isHexFloat(waypoint.lat) + && isHexFloat(waypoint.lng) + && isHexFloat(waypoint.alt), + ); +} + +export interface LandTask extends TaskBase { taskType: 'land'; waypoints: [ { - lat: number; - lng: number; - alt: number; + lat: HexFloat; + lng: HexFloat; + alt: HexFloat; }, { - lat: number; - lng: number; - alt: number; + lat: HexFloat; + lng: HexFloat; + alt: HexFloat; } ]; } -export interface UGVRetrieveTarget extends TaskBase { +export function isLandTask(task: Task): boolean { + return task.taskType === 'land' + && task.waypoints && task.waypoints.length === 2 + && task.waypoints.every( + (waypoint): boolean => isHexFloat(waypoint.lat) + && isHexFloat(waypoint.lng) + && isHexFloat(waypoint.alt), + ); +} + +export interface UGVRetrieveTargetTask extends TaskBase { taskType: 'retrieveTarget'; - lat: number; - lng: number; + lat: HexFloat; + lng: HexFloat; +} + +export function isUGVRetrieveTargetTask(task: Task): boolean { + if (task.taskType !== 'retrieveTarget') return false; + + const retrieveTargetTask = task as UGVRetrieveTargetTask; + + return isHexFloat(retrieveTargetTask.lat) && isHexFloat(retrieveTargetTask.lng); } -export interface DeliverTarget extends TaskBase { +export interface DeliverTargetTask extends TaskBase { taskType: 'deliverTarget'; - lat: number; - lng: number; + lat: HexFloat; + lng: HexFloat; +} + +export function isDeliverTargetTask(task: Task): boolean { + return task.taskType === 'deliverTarget' + && isHexFloat(task.lat) + && isHexFloat(task.lng); } -export interface UUVRetrieveTarget extends TaskBase { +export interface UUVRetrieveTargetTask extends TaskBase { taskType: 'retrieveTarget'; } -export interface QuickScan extends TaskBase { +export function isUUVRetrieveTargetTask(task: Task): boolean { + if (task.taskType !== 'retrieveTarget') return false; + + // Cast task to UGV retrieve to ensure that this task is not UGV task, and is UUV. + const retrieveTargetTask = task as UGVRetrieveTargetTask; + + // Check if values for lat and lng are undefined, since UUV version should not have these. + return !retrieveTargetTask.lat && !retrieveTargetTask.lng; +} + +export interface QuickScanTask extends TaskBase { taskType: 'quickScan'; searchArea: { - center: [number, number]; - rad1: number; - rad2: number; + center: [HexFloat, HexFloat]; + rad1: HexFloat; + rad2: HexFloat; }; } -export interface DetailedSearch extends TaskBase { +export function isQuickScanTask(task: Task): boolean { + return task.taskType === 'quickScan' + && task.searchArea + && task.searchArea.center && task.searchArea.center.length === 2 + && isHexFloat(task.searchArea.rad1) + && isHexFloat(task.searchArea.rad2); +} + +export interface DetailedSearchTask extends TaskBase { taskType: 'detailedSearch'; - lat: number; - lng: number; + lat: HexFloat; + lng: HexFloat; } -export type Task = Takeoff | Loiter | ISRSearch | PayloadDrop | Land | UGVRetrieveTarget -| DeliverTarget | UUVRetrieveTarget | QuickScan | DetailedSearch; +export function isDetailedSearchTask(task: Task): boolean { + return task.taskType === 'detailedSearch' + && isHexFloat(task.lat) + && isHexFloat(task.lng); +} -/* - * Definitions for all messages from GCS to vehicles. No need for type guards, TypeScript - * will handle all of it as we write up the message. +export type Task = TakeoffTask | LoiterTask | ISRSearchTask | PayloadDropTask | LandTask +| UGVRetrieveTargetTask | DeliverTargetTask | UUVRetrieveTargetTask | QuickScanTask +| DetailedSearchTask; + +/** + * Checks if an object is a task. To be a task, the object must match one of the tasks above. */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function isTask(task: { [key: string]: any }): boolean { + if (!task.taskType) return false; + + const specificTask = task as Task; + + return isTakeoffTask(specificTask) + || isLoiterTask(specificTask) + || isISRSearchTask(specificTask) + || isPayloadDropTask(specificTask) + || isLandTask(specificTask) + || isUGVRetrieveTargetTask(specificTask) + || isDeliverTargetTask(specificTask) + || isUUVRetrieveTargetTask(specificTask) + || isQuickScanTask(specificTask) + || isDetailedSearchTask(specificTask); +} interface MessageBase { /** @@ -146,6 +303,13 @@ interface MessageBase { type: string; } +/* + * Definitions for all messages from GCS to vehicles. No need for type guards, TypeScript + * will handle all of it as we write up the message. + * + * MessageHex types are the same as their normal Message class, however, their + */ + export interface StartMessage extends MessageBase { type: 'start'; @@ -157,7 +321,12 @@ export interface StartMessage extends MessageBase { /** * Any other information the vehicle should have before performing the job. */ - options?: object; + options?: any; // eslint-disable-line @typescript-eslint/no-explicit-any +} + +export function isStartMessage(message: Message): boolean { + return message.type === 'start' + && vehicleConfig.isJobType(message.jobType); } export interface AddMissionMessage extends MessageBase { @@ -169,88 +338,72 @@ export interface AddMissionMessage extends MessageBase { missionInfo: Task; } +export function isAddMissionMessage(message: Message): boolean { + return message.type === 'addMission' + && message.missionInfo && isTask(message.missionInfo); +} + export interface PauseMessage extends MessageBase { type: 'pause'; } +export function isPauseMessage(message: Message): boolean { + return message.type === 'pause'; +} + export interface ResumeMessage extends MessageBase { type: 'resume'; } +export function isResumeMessage(message: Message): boolean { + return message.type === 'resume'; +} + export interface StopMessage extends MessageBase { type: 'stop'; } +export function isStopMessage(message: Message): boolean { + return message.type === 'stop'; +} + export interface ConnectionAckMessage extends MessageBase { type: 'connectionAck'; } -/* - * Definitions for all messages from vehicles to GCS. Type guards are needed for these, - * as we do not know the types of the messages we are receiving and whether or not they're - * valid. - */ - -/** - * Checks if the object is a proper message. - */ -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export function isMessage(message: { [key: string]: any }): boolean { - const typeCheck = message.type && [ - 'start', - 'addMission', - 'pause', - 'resume', - 'stop', - 'connectionAck', - 'update', - 'poi', - 'complete', - 'connect', - 'ack', - 'badMessage', - ].indexOf(message.type) >= 0; - - // Check if id is a number. - const idCheck = message.id !== '' && message.id !== true && !Number.isNaN(message.id); - - // Check if tid is a number and that tid is 0 (vehicle id of GCS). - const tidCheck = !Number.isNaN(message.tid) && message.tid === 0; - - // Check if sid is a number and is valid. - const sidCheck = !Number.isNaN(message.sid) - && vehicleConfig.vehicleInfos[message.sid] !== undefined; - - return typeCheck && idCheck && tidCheck && sidCheck; +export function isConnectionAckMessage(message: Message): boolean { + return message.type === 'connectionAck'; } +// Definitions for all messages from vehicles to GCS. + export interface UpdateMessage extends MessageBase { type: 'update'; /** * Latitude of the vehicle. */ - lat: number; + lat: HexFloat; /** * Longitude of the vehicle. */ - lng: number; + lng: HexFloat; /** * Altitude of the vehicle. */ - alt?: number; + alt?: HexFloat; /** * Current vehicle heading. Value is in degrees. */ - heading?: number; + heading?: HexFloat; /** * Current battery of the vehicle, expressed as a decimal. Will vary from 0 to 1. */ - battery?: number; + battery?: HexFloat; /** * Status of the vehicle. @@ -266,20 +419,33 @@ export interface UpdateMessage extends MessageBase { errorMessage?: string; } -/* eslint-disable @typescript-eslint/no-unused-vars */ - export function isUpdateMessage(message: Message): boolean { // Check if message type is "update". - return message.type === 'update' + const mandatoryCheck = message.type === 'update' - // Check if lat is a number. - && !Number.isNaN(message.lat) + // Check if lat is a hex float. + && isHexFloat(message.lat) - // Check if lng is a number. - && !Number.isNaN(message.lng) + // Check if lng is a hex float. + && isHexFloat(message.lng) // Check if status is a valid vehicle status. && isVehicleStatus(message.status); + + if (!mandatoryCheck) return false; + + const updateMessage = message as UpdateMessage; + + // Check if alt is a hex float. + if (updateMessage.alt && !isHexFloat(updateMessage.alt)) return false; + + // Check if heading is a hex float. + if (updateMessage.heading && !isHexFloat(updateMessage.heading)) return false; + + // Check if battery is a hex float. + if (updateMessage.battery && !isHexFloat(updateMessage.battery)) return false; + + return true; } export interface POIMessage extends MessageBase { @@ -288,23 +454,23 @@ export interface POIMessage extends MessageBase { /** * Latitude of the point of interest. */ - lat: number; + lat: HexFloat; /** * Longitude of the point of interest. */ - lng: number; + lng: HexFloat; } export function isPOIMessage(message: Message): boolean { // Check if message type is "poi". return message.type === 'poi' - // Check if lat is a number. - && !Number.isNaN(message.lat) + // Check if lat is a hex float. + && isHexFloat(message.lat) - // Check if lng is a number. - && !Number.isNaN(message.lng); + // Check if lng is a hex float. + && isHexFloat(message.lng); } export interface CompleteMessage extends MessageBase { @@ -333,7 +499,7 @@ export function isConnectMessage(message: Message): boolean { && message.jobsAvailable.every(vehicleConfig.isJobType); } -// 4. Definitions for all other message types. +// Definitions for all other message types. export interface AcknowledgementMessage extends MessageBase { type: 'ack'; @@ -373,12 +539,69 @@ export type Message = StartMessage | AddMissionMessage | PauseMessage | ResumeMe | ConnectionAckMessage | UpdateMessage | POIMessage | CompleteMessage | ConnectMessage | AcknowledgementMessage | BadMessageMessage; +/** + * Logic for this function is slightly different from isTask since the GCS does not need to know + * which task it is sending to the vehicle. It just needs to know that the task is a valid task, + * and contains a valid taskType and task information for a task. + * + * For this function, GCS does need to know what messages it receives. GCS needs to check if + * an object is a message (if the object has a type field then it is a message). If not, GCS + * can alert the sender that the object sent is not a message. + * + * After this function, the GCS will check which message the sender has sent with the functions + * above, and if the functions fail (which means that the fields that have been included with + * the message are wrong), then the GCS will tell the sender that the format for the message + * is wrong. + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function isMessage(message: { [key: string]: any }): boolean { + return message.type && [ + 'start', + 'addMission', + 'pause', + 'resume', + 'stop', + 'connectionAck', + 'update', + 'poi', + 'complete', + 'connect', + 'ack', + 'badMessage', + ].indexOf(message.type) >= 0; +} + +export type JSONMessage = Message & { + id: number; + tid: number; + sid: number; + time: number; +}; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function isJSONMessage(message: { [key: string]: any }): boolean { + if (!isMessage(message)) return false; + + // Check if id is a number. + const idCheck = message.id !== '' && message.id !== true && !Number.isNaN(message.id); + + // Check if tid is a number and is valid. + const tidCheck = !Number.isNaN(message.tid) + && vehicleConfig.vehicleInfos[message.tid] !== undefined; + + // Check if sid is a number and is valid. + const sidCheck = !Number.isNaN(message.sid) + && vehicleConfig.vehicleInfos[message.sid] !== undefined; + + return idCheck && tidCheck && sidCheck; +} + export default { - isMessage, isUpdateMessage, isPOIMessage, isCompleteMessage, isConnectMessage, isAcknowledgementMessage, isBadMessageMessage, + isJSONMessage, }; diff --git a/src/util/util.ts b/src/util/util.ts index c10a0fff..8549ff81 100644 --- a/src/util/util.ts +++ b/src/util/util.ts @@ -33,47 +33,7 @@ export function isJSON(message: string): boolean { return true; } -/* eslint-disable no-bitwise */ - -/** - * Converts a float number to a hex string. - */ -function toHexString(float: number): string { - const getHex = (index: number): string => `00${index.toString(16)}`.slice(-2); - - const view = new DataView(new ArrayBuffer(4)); - view.setFloat32(0, float); - return [0, 0, 0, 0].map((_, index): string => getHex(view.getUint8(index))).join(''); -} - -/** - * Converts a hex string to a float number. - */ -function toFloat(hexString: string): number { - const int = parseInt(hexString, 16); - if (int > 0 || int < 0) { - const sign = (int >>> 31) ? -1 : 1; - let exp = ((int >>> 23) & 0xff) - 127; - const mantissa = ((int & 0x7fffff) + 0x800000).toString(2); - let float = 0; - - for (let i = 0; i < mantissa.length; i += 1, exp -= 1) { - if (parseInt(mantissa[i], 10)) { - float += 2 ** exp; - } - } - - return float * sign; - } - - return 0; -} - -/* eslint-enable no-bitwise */ - export default { isJSON, - toFloat, - toHexString, updateVehicles, }; From f96550620e12445d35b68224d6f1a32dc896bb3f Mon Sep 17 00:00:00 2001 From: Paul Matthew Barrameda Date: Mon, 8 Apr 2019 22:09:51 -0700 Subject: [PATCH 061/152] Reallocated type guard exports to a common variable Also removed unnecessary and blatantly obvious comments. Comments that describe each function would make this file better, but this should be the final version. Lets hope that no changes are needed in the future --- src/types/messages.ts | 88 +++++++++++++++++++++---------------------- 1 file changed, 44 insertions(+), 44 deletions(-) diff --git a/src/types/messages.ts b/src/types/messages.ts index fe2470e6..7d14e052 100644 --- a/src/types/messages.ts +++ b/src/types/messages.ts @@ -80,7 +80,7 @@ export interface TakeoffTask extends TaskBase { }; } -export function isTakeoffTask(task: Task): boolean { +function isTakeoffTask(task: Task): boolean { return task.taskType === 'takeoff' && isHexFloat(task.lat) && isHexFloat(task.lng) @@ -104,7 +104,7 @@ export interface LoiterTask extends TaskBase { direction: HexFloat; } -export function isLoiterTask(task: Task): boolean { +function isLoiterTask(task: Task): boolean { return task.taskType === 'loiter' && isHexFloat(task.lat) && isHexFloat(task.lng) @@ -134,7 +134,7 @@ export interface ISRSearchTask extends TaskBase { ]; } -export function isISRSearchTask(task: Task): boolean { +function isISRSearchTask(task: Task): boolean { return task.taskType === 'isrSearch' && isHexFloat(task.alt) @@ -161,7 +161,7 @@ export interface PayloadDropTask extends TaskBase { ]; } -export function isPayloadDropTask(task: Task): boolean { +function isPayloadDropTask(task: Task): boolean { return task.taskType === 'payloadDrop' && task.waypoints && task.waypoints.length === 2 && task.waypoints.every( @@ -188,7 +188,7 @@ export interface LandTask extends TaskBase { ]; } -export function isLandTask(task: Task): boolean { +function isLandTask(task: Task): boolean { return task.taskType === 'land' && task.waypoints && task.waypoints.length === 2 && task.waypoints.every( @@ -205,7 +205,7 @@ export interface UGVRetrieveTargetTask extends TaskBase { lng: HexFloat; } -export function isUGVRetrieveTargetTask(task: Task): boolean { +function isUGVRetrieveTargetTask(task: Task): boolean { if (task.taskType !== 'retrieveTarget') return false; const retrieveTargetTask = task as UGVRetrieveTargetTask; @@ -220,7 +220,7 @@ export interface DeliverTargetTask extends TaskBase { lng: HexFloat; } -export function isDeliverTargetTask(task: Task): boolean { +function isDeliverTargetTask(task: Task): boolean { return task.taskType === 'deliverTarget' && isHexFloat(task.lat) && isHexFloat(task.lng); @@ -230,7 +230,7 @@ export interface UUVRetrieveTargetTask extends TaskBase { taskType: 'retrieveTarget'; } -export function isUUVRetrieveTargetTask(task: Task): boolean { +function isUUVRetrieveTargetTask(task: Task): boolean { if (task.taskType !== 'retrieveTarget') return false; // Cast task to UGV retrieve to ensure that this task is not UGV task, and is UUV. @@ -250,7 +250,7 @@ export interface QuickScanTask extends TaskBase { }; } -export function isQuickScanTask(task: Task): boolean { +function isQuickScanTask(task: Task): boolean { return task.taskType === 'quickScan' && task.searchArea && task.searchArea.center && task.searchArea.center.length === 2 @@ -265,7 +265,7 @@ export interface DetailedSearchTask extends TaskBase { lng: HexFloat; } -export function isDetailedSearchTask(task: Task): boolean { +function isDetailedSearchTask(task: Task): boolean { return task.taskType === 'detailedSearch' && isHexFloat(task.lat) && isHexFloat(task.lng); @@ -279,7 +279,7 @@ export type Task = TakeoffTask | LoiterTask | ISRSearchTask | PayloadDropTask | * Checks if an object is a task. To be a task, the object must match one of the tasks above. */ // eslint-disable-next-line @typescript-eslint/no-explicit-any -export function isTask(task: { [key: string]: any }): boolean { +function isTask(task: { [key: string]: any }): boolean { if (!task.taskType) return false; const specificTask = task as Task; @@ -296,6 +296,20 @@ export function isTask(task: { [key: string]: any }): boolean { || isDetailedSearchTask(specificTask); } +export const taskTypeGuard = { + isTakeoffTask, + isLoiterTask, + isISRSearchTask, + isPayloadDropTask, + isLandTask, + isUGVRetrieveTargetTask, + isDeliverTargetTask, + isUUVRetrieveTargetTask, + isQuickScanTask, + isDetailedSearchTask, + isTask, +}; + interface MessageBase { /** * Type of message. @@ -324,7 +338,7 @@ export interface StartMessage extends MessageBase { options?: any; // eslint-disable-line @typescript-eslint/no-explicit-any } -export function isStartMessage(message: Message): boolean { +function isStartMessage(message: Message): boolean { return message.type === 'start' && vehicleConfig.isJobType(message.jobType); } @@ -338,7 +352,7 @@ export interface AddMissionMessage extends MessageBase { missionInfo: Task; } -export function isAddMissionMessage(message: Message): boolean { +function isAddMissionMessage(message: Message): boolean { return message.type === 'addMission' && message.missionInfo && isTask(message.missionInfo); } @@ -347,7 +361,7 @@ export interface PauseMessage extends MessageBase { type: 'pause'; } -export function isPauseMessage(message: Message): boolean { +function isPauseMessage(message: Message): boolean { return message.type === 'pause'; } @@ -355,7 +369,7 @@ export interface ResumeMessage extends MessageBase { type: 'resume'; } -export function isResumeMessage(message: Message): boolean { +function isResumeMessage(message: Message): boolean { return message.type === 'resume'; } @@ -363,7 +377,7 @@ export interface StopMessage extends MessageBase { type: 'stop'; } -export function isStopMessage(message: Message): boolean { +function isStopMessage(message: Message): boolean { return message.type === 'stop'; } @@ -371,7 +385,7 @@ export interface ConnectionAckMessage extends MessageBase { type: 'connectionAck'; } -export function isConnectionAckMessage(message: Message): boolean { +function isConnectionAckMessage(message: Message): boolean { return message.type === 'connectionAck'; } @@ -419,30 +433,18 @@ export interface UpdateMessage extends MessageBase { errorMessage?: string; } -export function isUpdateMessage(message: Message): boolean { - // Check if message type is "update". +function isUpdateMessage(message: Message): boolean { const mandatoryCheck = message.type === 'update' - - // Check if lat is a hex float. && isHexFloat(message.lat) - - // Check if lng is a hex float. && isHexFloat(message.lng) - - // Check if status is a valid vehicle status. && isVehicleStatus(message.status); if (!mandatoryCheck) return false; const updateMessage = message as UpdateMessage; - // Check if alt is a hex float. if (updateMessage.alt && !isHexFloat(updateMessage.alt)) return false; - - // Check if heading is a hex float. if (updateMessage.heading && !isHexFloat(updateMessage.heading)) return false; - - // Check if battery is a hex float. if (updateMessage.battery && !isHexFloat(updateMessage.battery)) return false; return true; @@ -462,14 +464,9 @@ export interface POIMessage extends MessageBase { lng: HexFloat; } -export function isPOIMessage(message: Message): boolean { - // Check if message type is "poi". +function isPOIMessage(message: Message): boolean { return message.type === 'poi' - - // Check if lat is a hex float. && isHexFloat(message.lat) - - // Check if lng is a hex float. && isHexFloat(message.lng); } @@ -477,8 +474,7 @@ export interface CompleteMessage extends MessageBase { type: 'complete'; } -export function isCompleteMessage(message: Message): boolean { - // Check if message type is "complete". +function isCompleteMessage(message: Message): boolean { return message.type === 'complete'; } @@ -492,7 +488,6 @@ export interface ConnectMessage extends MessageBase { } export function isConnectMessage(message: Message): boolean { - // Check if message type is "connect". return message.type === 'connect' // Check if jobsAvailable is a string array of valid job types. @@ -511,10 +506,7 @@ export interface AcknowledgementMessage extends MessageBase { } export function isAcknowledgementMessage(message: Message): boolean { - // Check if message type is "ack". return message.type === 'ack' - - // Check if ackid is a number. && !Number.isNaN(message.ackid); } @@ -531,7 +523,6 @@ export interface BadMessageMessage extends MessageBase { } export function isBadMessageMessage(message: Message): boolean { - // Check if message type is "badMessage". return message.type === 'badMessage'; } @@ -596,12 +587,21 @@ export function isJSONMessage(message: { [key: string]: any }): boolean { return idCheck && tidCheck && sidCheck; } -export default { +export const messageTypeGuard = { + isStartMessage, + isAddMissionMessage, + isPauseMessage, + isResumeMessage, + isStopMessage, + isConnectionAckMessage, isUpdateMessage, isPOIMessage, isCompleteMessage, isConnectMessage, isAcknowledgementMessage, isBadMessageMessage, + isMessage, isJSONMessage, }; + +export default { taskTypeGuard, messageTypeGuard }; From 5cec45cb96e615580e768dcb44ace14cad633186 Mon Sep 17 00:00:00 2001 From: Paul Matthew Barrameda Date: Tue, 9 Apr 2019 22:18:58 -0700 Subject: [PATCH 062/152] Added receiveMessage function to MessageHandler Also updated QuickScan Task definition --- src/common/MessageHandler.ts | 178 +++++++++++++++++++++++++++++++---- src/types/messages.ts | 34 +++++-- src/util/util.ts | 2 +- 3 files changed, 185 insertions(+), 29 deletions(-) diff --git a/src/common/MessageHandler.ts b/src/common/MessageHandler.ts index 05a41f72..1a1da863 100644 --- a/src/common/MessageHandler.ts +++ b/src/common/MessageHandler.ts @@ -1,10 +1,21 @@ import { Event, ipcRenderer } from 'electron'; -import { config } from '../static/index'; +import { + config, + vehicleConfig, + VehicleInfo, +} from '../static/index'; +import { + AcknowledgementMessage, + BadMessageMessage, + messageTypeGuard, + JSONMessage, // eslint-disable-line import/named + Message, // eslint-disable-line import/named +} from '../types/messages'; +import { isJSON } from '../util/util'; // TODO: Remove disable line comment when issue gets fixed (https://github.com/benmosher/eslint-plugin-import/pull/1304) import DictionaryList from './struct/DictionaryList'; -import { JSONMessage, Message } from '../types/messages'; // eslint-disable-line import/named import UpdateHandler from './struct/UpdateHandler'; import xbee from './Xbee'; // eslint-disable-line import/named @@ -27,14 +38,19 @@ export default class MessageHandler { private updateHandler = new UpdateHandler(); /** - * Message id being sent so far. + * ID of message being sent. */ private id = 0; + /** + * Map of last message ID received from specific vehicle. + */ + private receivedMessageId: { [key: string]: number | undefined } = {}; + public constructor() { ipcRenderer.on('sendMessage', (_: Event, vehicleId: number, message: Message): void => this.sendMessage(vehicleId, message)); - ipcRenderer.on('receiveMessage', (_: Event, string: string): void => this.receiveMessage(string)); + ipcRenderer.on('receiveMessage', (_: Event, text: string): void => this.receiveMessage(text)); } /** @@ -86,19 +102,24 @@ export default class MessageHandler { * This will end when the message is acknowledged (the updateHandler will clear * the interval). */ - const expiry = setInterval((): void => { - xbee.sendMessage(jsonMessage); - }, config.messageSendRate * 1000); + const expiry = setInterval( + (): void => xbee.sendMessage(jsonMessage), config.messageSendRate * 1000, + ); /* - * Removes json message from "outbox" when an ack message is received from the vehicle. + * Removes respective message from "outbox" when an ack message is received + * and value passed is true. + * * If no ack message is received within a certain amount of time (vehicleDisconnectionTime), * then the GCS will disconnect itself from the vehicle. */ - this.updateHandler.addHandler(`${jsonMessage.tid}#${jsonMessage.id}`, (): boolean => { + + const hash = `${jsonMessage.tid}#${jsonMessage.id}`; + + this.updateHandler.addHandler(hash, (value: boolean): boolean => { clearInterval(expiry); this.removeMessage('outbox', jsonMessage.id); - return true; + return value; }, { time: config.vehicleDisconnectionTime * 1000, callback: (): void => { @@ -111,14 +132,25 @@ export default class MessageHandler { /** * Acknowledge a message received. * - * @param message The message to acknowledge. - * TODO: remove ? from message + * @param jsonMessage The message to acknowledge. */ - private acknowledge(message?: JSONMessage): void { // eslint-disable-line - if (!message) return; // TODO: remove this line - this.sendMessage(message.sid, { + private sendAcknowledgement(jsonMessage: JSONMessage): void { + this.sendMessage(jsonMessage.sid, { type: 'ack', - ackid: message.id, + ackid: jsonMessage.id, + }); + } + + /** + * Send a bad message. + * + * @param jsonMessage The bad message (needs to have at least an sid field). + * @param error Error message. + */ + private sendBadMessage(jsonMessage: JSONMessage, error?: string): void { + this.sendMessage(jsonMessage.sid, { + type: 'badMessage', + error, }); } @@ -128,7 +160,117 @@ export default class MessageHandler { * @param string The raw string. Either can contain the message (a valid JSON) or some * random stuff. */ - private receiveMessage(string: string): void { - if (string === 'asdffdasfdas') this.acknowledge(); // TODO: remove this line + private receiveMessage(text: string): void { + // Filter out text that are not valid json. + if (!isJSON(text)) { + ipcRenderer.send('post', 'updateMessages', { + message: `Received text from Xbee that is not a JSON, could not send bad message to sender ${text}`, + }); + return; + } + + // Filter out json that are not valid messages. + const json = JSON.parse(text); + if (!messageTypeGuard.isJSONMessage(json)) { + if (json.sid) { + this.sendBadMessage(json, 'Invalid message, does not meet requirements for a message'); + } else { + ipcRenderer.send('post', 'updateMessages', { + message: `Received JSON from Xbee that is not a valid message, could not send bad message to sender: ${text}`, + }); + } + } + + const jsonMessage = json as JSONMessage; + + /* + * Update the last message id received from the vehicle. + * This is to be able to properly acknowledge a message. See the note on the following: + * + * https://ground-control-station.readthedocs.io/en/latest/communications/other-messages.html#acknowledgement-message + */ + if (!this.receivedMessageId[jsonMessage.sid] + || this.receivedMessageId[jsonMessage.sid] as number < jsonMessage.id + ) { + this.receivedMessageId[jsonMessage.sid] = jsonMessage.id; + } + + /* + * Handles acknowledgement messages. + * DO NOT ACKNOWLEDGE. + * Stops sending the message that the ack message has ascknowledged. + */ + if (messageTypeGuard.isAcknowledgementMessage(jsonMessage)) { + const { ackid } = jsonMessage as AcknowledgementMessage; + + const hash = `${jsonMessage.sid}#${ackid}`; + this.updateHandler.event(hash, true); + return; + } + + + /** + * Handles bad messages. + * DO NOT ACKNOWLEDGE. + * Logs error to log container. + */ + if (messageTypeGuard.isBadMessageMessage(jsonMessage)) { + const { error } = jsonMessage as BadMessageMessage; + const { name } = vehicleConfig.vehicleInfos[jsonMessage.sid] as VehicleInfo; + + ipcRenderer.send('post', 'updateMessages', { + type: 'failure', + message: `Received bad message from ${name}: ${error || 'No error message was specified'}`, + }); + return; + } + + /* + * Handles connect messages. + * DO NOT ACKNOWLEDGE. ORCHESTRATOR WILL BUILD CONNECTIONACK MESSAGE. + * + * Forward message to Orchestrator. + */ + if (messageTypeGuard.isConnectMessage(jsonMessage)) { + return; + } + + /* + * Acknowledge the message. + */ + if (this.receivedMessageId[jsonMessage.sid] as number <= jsonMessage.id) { + this.sendAcknowledgement(jsonMessage); + } + + /* + * Handles complete messages. + * Forward message to Orchestrator. + */ + if (messageTypeGuard.isCompleteMessage(jsonMessage)) { + return; + } + + /* + * Handles point of interest messages. + * Forward message to Orchestrator. + */ + if (messageTypeGuard.isPOIMessage(jsonMessage)) { + return; + } + + /* + * Handles update messages. + * Forward message to Orchestrator. + */ + if (messageTypeGuard.isUpdateMessage(jsonMessage)) { + return; + } + + /* + * Any message that reaches here is a message (has a valid type, id, sid, tid, and time) + * but does not have valid properties, or is a message GCS should not receive from vehicles + * (e.g. GCS should not receive a "start" message from a vehicle). + */ + this.sendBadMessage(jsonMessage, `Message of type ${jsonMessage.type} is invalid`); } } diff --git a/src/types/messages.ts b/src/types/messages.ts index 7d14e052..42389b29 100644 --- a/src/types/messages.ts +++ b/src/types/messages.ts @@ -1,8 +1,9 @@ /* * Definitions and typeguards for all messages that are sent between GCS and vehicles. - * See the wiki for more information: https://github.com/NGCP/GCS/wiki/GCS-JSON-Messages + * https://ground-control-station.readthedocs.io/en/latest/communications/messages.html * * Includes definitions for tasks too, as those are part of a message. + * https://ground-control-station.readthedocs.io/en/latest/communications/jobs.html */ import { vehicleConfig } from '../static/index'; @@ -243,19 +244,32 @@ function isUUVRetrieveTargetTask(task: Task): boolean { export interface QuickScanTask extends TaskBase { taskType: 'quickScan'; - searchArea: { - center: [HexFloat, HexFloat]; - rad1: HexFloat; - rad2: HexFloat; - }; + waypoints: [ + { + lat: HexFloat; + lng: HexFloat; + }, + { + lat: HexFloat; + lng: HexFloat; + }, + { + lat: HexFloat; + lng: HexFloat; + }, + { + lat: HexFloat; + lng: HexFloat; + } + ]; } function isQuickScanTask(task: Task): boolean { return task.taskType === 'quickScan' - && task.searchArea - && task.searchArea.center && task.searchArea.center.length === 2 - && isHexFloat(task.searchArea.rad1) - && isHexFloat(task.searchArea.rad2); + && task.waypoints && task.waypoints.length === 4 + && task.waypoints.every( + (waypoint): boolean => isHexFloat(waypoint.lat) && isHexFloat(waypoint.lng), + ); } export interface DetailedSearchTask extends TaskBase { diff --git a/src/util/util.ts b/src/util/util.ts index 8549ff81..84bf2aff 100644 --- a/src/util/util.ts +++ b/src/util/util.ts @@ -20,7 +20,7 @@ export function updateVehicles( } /** - * Checks if the string is a JSON. Should be called before isMessage(). + * Checks if the string is a JSON. */ export function isJSON(message: string): boolean { if (!message) return false; From 2ea625b29940417a2635a42a3cab7c1e5b8d6a6b Mon Sep 17 00:00:00 2001 From: Paul Matthew Barrameda Date: Tue, 9 Apr 2019 22:30:22 -0700 Subject: [PATCH 063/152] Updated UpdateHandler Updated to this commit https: //github.com/NGCP/GCS/commit/5e1bd6a8eeeabe3284d9fea0ad416431e61ab571#diff-9c8de213580183bcd8fa27b3fdefce6b Co-Authored-By: Maxence Weyrich --- src/common/MessageHandler.ts | 2 +- src/common/struct/UpdateHandler.ts | 12 ++++++++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/common/MessageHandler.ts b/src/common/MessageHandler.ts index 1a1da863..58f98cfd 100644 --- a/src/common/MessageHandler.ts +++ b/src/common/MessageHandler.ts @@ -18,7 +18,7 @@ import { isJSON } from '../util/util'; import DictionaryList from './struct/DictionaryList'; import UpdateHandler from './struct/UpdateHandler'; -import xbee from './Xbee'; // eslint-disable-line import/named +import xbee from './Xbee'; export default class MessageHandler { /** diff --git a/src/common/struct/UpdateHandler.ts b/src/common/struct/UpdateHandler.ts index 7500f5f5..494e13ce 100644 --- a/src/common/struct/UpdateHandler.ts +++ b/src/common/struct/UpdateHandler.ts @@ -28,6 +28,11 @@ interface Handler { * deleting the handler while the timeout has not executed. */ expiry?: NodeJS.Timeout; + + /** + * Function that will remove the handler. + */ + removeHandler: () => void; } /** @@ -53,7 +58,10 @@ export default class UpdateHandler { handlerCheck: HandlerCheck, timeout?: HandlerTimeoutOptions, ): Handler { - const handler: Handler = { handlerCheck }; + const handler: Handler = { + handlerCheck, + removeHandler: (): void => this.removeHandler(name, handler), + }; /* * If a timeout is provided, the handler will remove itself and run its callback function @@ -124,7 +132,7 @@ export default class UpdateHandler { * @param name The name of the event the handler is in. * @param handler The handler itself. */ - public removeHandler(name: string, handler: Handler): void { + private removeHandler(name: string, handler: Handler): void { // Will clearTimeout the handler if it has an expiry. if (handler.expiry) clearTimeout(handler.expiry); From 542b8a29790b589b5a86c1b1b9a53d703af6eef8 Mon Sep 17 00:00:00 2001 From: Paul Matthew Barrameda Date: Tue, 9 Apr 2019 22:53:16 -0700 Subject: [PATCH 064/152] Fixed UpdateHandler test --- test/common/struct/UpdateHandler.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/common/struct/UpdateHandler.test.ts b/test/common/struct/UpdateHandler.test.ts index ef7a168f..e20e4f4a 100644 --- a/test/common/struct/UpdateHandler.test.ts +++ b/test/common/struct/UpdateHandler.test.ts @@ -294,13 +294,13 @@ describe('UpdateHandler', (): void => { expect(statusCounter).to.equal(1); expect(locationCounter).to.equal(1); - handler.removeHandler('status', statusHandler); + statusHandler.removeHandler(); handler.events({ status: 1, location: 1 }); expect(statusCounter).to.equal(1); expect(locationCounter).to.equal(2); - handler.removeHandler('location', locationHandler); + locationHandler.removeHandler(); handler.events({ status: 1, location: 1 }); expect(statusCounter).to.equal(1); From 78a66110b75a9359ca592ba4fcf64f0a008c30c4 Mon Sep 17 00:00:00 2001 From: Paul Matthew Barrameda Date: Wed, 10 Apr 2019 02:01:16 -0700 Subject: [PATCH 065/152] Changed eslint Was working on Mission, but I'll give it a rest. Will do it tomorrow --- .eslintrc.yml | 3 +++ package.json | 6 +++--- src/common/MessageHandler.ts | 14 +++++++++++--- src/common/Orchestrator.ts | 2 ++ src/common/Xbee.ts | 3 +-- src/common/struct/Vehicle.ts | 11 +++++------ src/renderer/fixtures/updateMessagesFixtures.ts | 3 +-- src/renderer/fixtures/updateVehiclesFixtures.ts | 3 +-- src/renderer/mainWindow/log/LogContainer.tsx | 3 +-- src/renderer/mainWindow/map/VehicleMarker.tsx | 3 +-- src/types/messages.ts | 3 +-- test/common/struct/DictionaryList.test.ts | 3 +-- 12 files changed, 31 insertions(+), 26 deletions(-) diff --git a/.eslintrc.yml b/.eslintrc.yml index 6904f828..afba283c 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -31,6 +31,9 @@ settings: # Additional rules rules: + # Turning off this because its buggy https://github.com/benmosher/eslint-plugin-import/pull/1304 + import/named: off + # Turn off linting about having wrong line ending, handled in .gitattributes linebreak-style: off diff --git a/package.json b/package.json index 7aec8b38..f4dff1ed 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ "@babel/preset-react": "^7.0.0", "@types/chai": "^4.1.7", "@types/mocha": "^5.2.6", - "@types/node": "^11.13.1", + "@types/node": "^11.13.2", "@types/pouchdb": "^6.3.3", "@types/react": "^16.8.13", "@types/react-dom": "^16.8.3", @@ -61,10 +61,10 @@ "mocha": "^6.1.2", "stylelint": "^9.10.1", "stylelint-config-standard": "^18.2.0", - "stylelint-order": "^2.2.0", + "stylelint-order": "^2.2.1", "ts-mocha": "^6.0.0", "ts-node": "^8.0.3", - "typescript": "^3.4.2", + "typescript": "^3.4.3", "webpack": "^4.29.6" }, "electronWebpack": { diff --git a/src/common/MessageHandler.ts b/src/common/MessageHandler.ts index 58f98cfd..a0947f55 100644 --- a/src/common/MessageHandler.ts +++ b/src/common/MessageHandler.ts @@ -1,3 +1,5 @@ +// Do not interact with this file outside the Orchestrator class. + import { Event, ipcRenderer } from 'electron'; import { @@ -9,12 +11,11 @@ import { AcknowledgementMessage, BadMessageMessage, messageTypeGuard, - JSONMessage, // eslint-disable-line import/named - Message, // eslint-disable-line import/named + JSONMessage, + Message, } from '../types/messages'; import { isJSON } from '../util/util'; -// TODO: Remove disable line comment when issue gets fixed (https://github.com/benmosher/eslint-plugin-import/pull/1304) import DictionaryList from './struct/DictionaryList'; import UpdateHandler from './struct/UpdateHandler'; @@ -193,6 +194,13 @@ export default class MessageHandler { || this.receivedMessageId[jsonMessage.sid] as number < jsonMessage.id ) { this.receivedMessageId[jsonMessage.sid] = jsonMessage.id; + + /* + * When this loop passes, it means that GCS is recieving a completely new message + * from the vehicle. We do not want to add messages that have been sent to us + * many times to be put multiple times into the dictionary. + */ + this.messageDictionary.push('received', jsonMessage); } /* diff --git a/src/common/Orchestrator.ts b/src/common/Orchestrator.ts index 6f6fc7ca..3d89ce39 100644 --- a/src/common/Orchestrator.ts +++ b/src/common/Orchestrator.ts @@ -1 +1,3 @@ +// Do not import this class outside src/renderer/index + import './MessageHandler'; diff --git a/src/common/Xbee.ts b/src/common/Xbee.ts index e88a6dd7..74b70a3e 100644 --- a/src/common/Xbee.ts +++ b/src/common/Xbee.ts @@ -6,8 +6,7 @@ import { constants as C, Frame, XBeeAPI } from 'xbee-api'; import { VehicleInfo, vehicleConfig } from '../static/index'; -// TODO: Remove disable line comment when issue gets fixed (https://github.com/benmosher/eslint-plugin-import/pull/1304) -import { JSONMessage } from '../types/messages'; // eslint-disable-line import/named +import { JSONMessage } from '../types/messages'; /* * TODO: Add a feature with Vehicle container to change this dynamically and reconnect. diff --git a/src/common/struct/Vehicle.ts b/src/common/struct/Vehicle.ts index 003477ca..39c5b03a 100644 --- a/src/common/struct/Vehicle.ts +++ b/src/common/struct/Vehicle.ts @@ -2,13 +2,12 @@ import { ipcRenderer } from 'electron'; import { vehicleConfig } from '../../static/index'; -// TODO: Remove disable line comment when issue gets fixed (https://github.com/benmosher/eslint-plugin-import/pull/1304) -import { VehicleObject, VehicleStatus } from '../../types/types'; // eslint-disable-line import/named +import { VehicleObject, VehicleStatus } from '../../types/types'; import { - Message, // eslint-disable-line import/named + Message, StartMessage, - Task, // eslint-disable-line import/named + Task, UpdateMessage, } from '../../types/messages'; @@ -228,7 +227,7 @@ export default class Vehicle { * @param disconnectionCallback Optional callback when vehicle disconnects. * @param errorCallback Optional callback when vehicle goes in an error state. */ - public assignMission( + public assignJob( jobType: string, options?: object, completionCallback?: () => void, @@ -279,7 +278,7 @@ export default class Vehicle { * * @param task The task for the vehicle to perform. Must support the vehicle's job. */ - public addMission(task: Task): boolean { + public assignTask(task: Task): boolean { /* * Returns false if the vehicle has not been assigned a mission or if the provided task * is not compatible with its job. diff --git a/src/renderer/fixtures/updateMessagesFixtures.ts b/src/renderer/fixtures/updateMessagesFixtures.ts index a62442f3..bf6003c6 100644 --- a/src/renderer/fixtures/updateMessagesFixtures.ts +++ b/src/renderer/fixtures/updateMessagesFixtures.ts @@ -1,7 +1,6 @@ import { ipcRenderer } from 'electron'; -// TODO: Remove disable line comment when issue gets fixed (https://github.com/benmosher/eslint-plugin-import/pull/1304) -import { MessageType } from '../../types/types'; // eslint-disable-line import/named +import { MessageType } from '../../types/types'; const fixtures: { type?: MessageType; message: string }[] = [ { diff --git a/src/renderer/fixtures/updateVehiclesFixtures.ts b/src/renderer/fixtures/updateVehiclesFixtures.ts index c75616b2..90523386 100644 --- a/src/renderer/fixtures/updateVehiclesFixtures.ts +++ b/src/renderer/fixtures/updateVehiclesFixtures.ts @@ -4,8 +4,7 @@ import Vehicle, { VehicleOptions } from '../../common/struct/Vehicle'; import { locationConfig, vehicleConfig } from '../../static/index'; -// TODO: Remove disable line comment when issue gets fixed (https://github.com/benmosher/eslint-plugin-import/pull/1304) -import { VehicleObject, VehicleStatus } from '../../types/types'; // eslint-disable-line import/named +import { VehicleObject, VehicleStatus } from '../../types/types'; const fixtureOptions: VehicleOptions[] = [ { diff --git a/src/renderer/mainWindow/log/LogContainer.tsx b/src/renderer/mainWindow/log/LogContainer.tsx index bcfea271..fb6afd4b 100644 --- a/src/renderer/mainWindow/log/LogContainer.tsx +++ b/src/renderer/mainWindow/log/LogContainer.tsx @@ -9,8 +9,7 @@ import { ListRowProps, } from 'react-virtualized'; -// TODO: Remove disable line comment when issue gets fixed (https://github.com/benmosher/eslint-plugin-import/pull/1304) -import { isMessageType, MessageType, ThemeProps } from '../../../types/types'; // eslint-disable-line import/named +import { isMessageType, MessageType, ThemeProps } from '../../../types/types'; import './log.css'; diff --git a/src/renderer/mainWindow/map/VehicleMarker.tsx b/src/renderer/mainWindow/map/VehicleMarker.tsx index b7ee1dd8..4caea019 100644 --- a/src/renderer/mainWindow/map/VehicleMarker.tsx +++ b/src/renderer/mainWindow/map/VehicleMarker.tsx @@ -10,8 +10,7 @@ import { VehicleStatus, } from '../../../static/index'; -// TODO: Remove disable line comment when issue gets fixed (https://github.com/benmosher/eslint-plugin-import/pull/1304) -import { MessageType, VehicleObject } from '../../../types/types'; // eslint-disable-line import/named +import { MessageType, VehicleObject } from '../../../types/types'; const vehicleIcons = (imageConfig.markers as RecursiveImageSignature) .vehicles as RecursiveImageSignature; diff --git a/src/types/messages.ts b/src/types/messages.ts index 42389b29..75d14c63 100644 --- a/src/types/messages.ts +++ b/src/types/messages.ts @@ -8,8 +8,7 @@ import { vehicleConfig } from '../static/index'; -// TODO: Remove disable line comment when issue gets fixed (https://github.com/benmosher/eslint-plugin-import/pull/1304) -import { isVehicleStatus, VehicleStatus } from './types'; // eslint-disable-line import/named +import { isVehicleStatus, VehicleStatus } from './types'; /** * A float represented as a hexadecimal string. diff --git a/test/common/struct/DictionaryList.test.ts b/test/common/struct/DictionaryList.test.ts index 5f0f88cb..30ff9d11 100644 --- a/test/common/struct/DictionaryList.test.ts +++ b/test/common/struct/DictionaryList.test.ts @@ -1,8 +1,7 @@ import { expect } from 'chai'; import { beforeEach, describe, it } from 'mocha'; -// TODO: Remove disable line comment when issue gets fixed (https://github.com/benmosher/eslint-plugin-import/pull/1304) -import DictionaryList, { Callback } from '../../../src/common/struct/DictionaryList'; // eslint-disable-line import/named +import DictionaryList, { Callback } from '../../../src/common/struct/DictionaryList'; let dict: DictionaryList; From 5a555963cd89802bfc1fcd5e6a7c43db29024d42 Mon Sep 17 00:00:00 2001 From: Paul Matthew Barrameda Date: Wed, 10 Apr 2019 19:07:59 -0700 Subject: [PATCH 066/152] Added JobType type --- src/common/struct/Vehicle.ts | 27 ++++++++++++--------------- src/static/index.ts | 15 +++++++++++++-- src/types/messages.ts | 6 +++--- 3 files changed, 28 insertions(+), 20 deletions(-) diff --git a/src/common/struct/Vehicle.ts b/src/common/struct/Vehicle.ts index 39c5b03a..6b36d9f8 100644 --- a/src/common/struct/Vehicle.ts +++ b/src/common/struct/Vehicle.ts @@ -1,6 +1,6 @@ import { ipcRenderer } from 'electron'; -import { vehicleConfig } from '../../static/index'; +import { JobType, vehicleConfig } from '../../static/index'; import { VehicleObject, VehicleStatus } from '../../types/types'; @@ -20,7 +20,7 @@ type ErrorCallback = (message?: string) => void; */ export interface VehicleOptions { sid: number; - jobs: string[]; + jobs: JobType[]; status?: VehicleStatus; } @@ -40,12 +40,7 @@ export default class Vehicle { /** * Current assigned job. */ - private assignedJob: string = ''; - - /** - * Whether or not the vehicle is ready to be assigned a mission. - */ - private readyForMission: boolean = true; + private assignedJob: JobType | undefined; /** * Current status of the vehicle. @@ -228,19 +223,21 @@ export default class Vehicle { * @param errorCallback Optional callback when vehicle goes in an error state. */ public assignJob( - jobType: string, + jobType: JobType, options?: object, completionCallback?: () => void, disconnectionCallback?: () => void, errorCallback?: ErrorCallback, ): boolean { - // Returns false if a vehicle has already been assigned a mission. - if (!this.readyForMission) { + /* + * Returns false if a vehicle has already been assigned a mission. + * Vehicle's status is ready if it has not been assigned a mission. + */ + if (this.status !== 'ready') { return false; } this.assignedJob = jobType; - this.readyForMission = false; if (errorCallback) this.errorCallback = errorCallback; const startMessage: StartMessage = { @@ -261,7 +258,6 @@ export default class Vehicle { this.updateEventHandler.addHandler('status', (value): boolean => { if (value === 'ready') { if (completionCallback) completionCallback(); - this.readyForMission = true; } else if (value === 'disconnected') { if (disconnectionCallback) disconnectionCallback(); } @@ -282,8 +278,9 @@ export default class Vehicle { /* * Returns false if the vehicle has not been assigned a mission or if the provided task * is not compatible with its job. + * Vehicle's status is "waiting" if it has been assigned a mission but is awaiting a task. */ - if (!this.readyForMission || !vehicleConfig.isTaskTypeForJob(task.taskType, this.assignedJob)) { + if (this.status !== 'waiting' || !this.assignedJob || !vehicleConfig.isTaskTypeForJob(task.taskType, this.assignedJob)) { return false; } @@ -303,6 +300,6 @@ export default class Vehicle { type: 'stop', }); - this.assignedJob = ''; + this.assignedJob = undefined; } } diff --git a/src/static/index.ts b/src/static/index.ts index aef0c11b..756c2620 100644 --- a/src/static/index.ts +++ b/src/static/index.ts @@ -52,6 +52,12 @@ export interface VehicleInfo { 'type': string; } +/** + * All valid job types. This should always match up to the job types in vehicle.json. + */ +export type JobType = 'isrSearch' | 'payloadDrop' | 'ugvRetrieve' | 'uuvRetrieve' +| 'quickScan' | 'detailedSearch'; + const vehicleInfos: { [id: string]: VehicleInfo | undefined; } = vehicleInfosObject; @@ -84,13 +90,18 @@ const startLocation: LatLngZoom = startLocationString && locations[startLocation * Checks if a string is a valid job type. */ function isJobType(jobType: string): boolean { - return vehicleJobs[jobType] !== undefined; + return jobType === 'isrSearch' + || jobType === 'payloadDrop' + || jobType === 'ugvRetrieve' + || jobType === 'uuvRetrieve' + || jobType === 'quickScan' + || jobType === 'detailedSearch'; } /** * Checks if a task is a valid task type for that job. */ -function isTaskTypeForJob(taskType: string, jobType: string): boolean { +function isTaskTypeForJob(taskType: string, jobType: JobType): boolean { return isJobType(jobType) && (vehicleJobs[jobType] as string[]).indexOf(taskType) >= 0; } diff --git a/src/types/messages.ts b/src/types/messages.ts index 75d14c63..9e338551 100644 --- a/src/types/messages.ts +++ b/src/types/messages.ts @@ -6,7 +6,7 @@ * https://ground-control-station.readthedocs.io/en/latest/communications/jobs.html */ -import { vehicleConfig } from '../static/index'; +import { JobType, vehicleConfig } from '../static/index'; import { isVehicleStatus, VehicleStatus } from './types'; @@ -343,7 +343,7 @@ export interface StartMessage extends MessageBase { /** * Name of job to perform. */ - jobType: string; + jobType: JobType; /** * Any other information the vehicle should have before performing the job. @@ -497,7 +497,7 @@ export interface ConnectMessage extends MessageBase { /** * List of all the different jobs the vehicle is capable of doing. */ - jobsAvailable: string[]; + jobsAvailable: JobType[]; } export function isConnectMessage(message: Message): boolean { From abd6cd44c27433830c58c94d7e90a00f3801d704 Mon Sep 17 00:00:00 2001 From: Paul Matthew Barrameda Date: Thu, 11 Apr 2019 00:04:32 -0700 Subject: [PATCH 067/152] Fixed updateVehiclesFixtures to have proper jobTypes --- src/renderer/fixtures/updateVehiclesFixtures.ts | 6 +++--- src/static/index.ts | 2 +- src/types/messages.ts | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/renderer/fixtures/updateVehiclesFixtures.ts b/src/renderer/fixtures/updateVehiclesFixtures.ts index 90523386..e8e65746 100644 --- a/src/renderer/fixtures/updateVehiclesFixtures.ts +++ b/src/renderer/fixtures/updateVehiclesFixtures.ts @@ -9,15 +9,15 @@ import { VehicleObject, VehicleStatus } from '../../types/types'; const fixtureOptions: VehicleOptions[] = [ { sid: 100, - jobs: ['takeoff', 'loiter', 'isrSearch', 'payloadDrop', 'land'], + jobs: ['isrSearch', 'payloadDrop'], }, { sid: 200, - jobs: ['retrieveTarget', 'deliverTarget'], + jobs: ['ugvRetrieve'], }, { sid: 300, - jobs: ['retrieveTarget'], + jobs: ['uuvRetrieve'], }, { sid: 400, diff --git a/src/static/index.ts b/src/static/index.ts index 756c2620..dc35d337 100644 --- a/src/static/index.ts +++ b/src/static/index.ts @@ -102,7 +102,7 @@ function isJobType(jobType: string): boolean { * Checks if a task is a valid task type for that job. */ function isTaskTypeForJob(taskType: string, jobType: JobType): boolean { - return isJobType(jobType) && (vehicleJobs[jobType] as string[]).indexOf(taskType) >= 0; + return (vehicleJobs[jobType] as string[]).includes(taskType); } export const locationConfig = { diff --git a/src/types/messages.ts b/src/types/messages.ts index 9e338551..48270e9d 100644 --- a/src/types/messages.ts +++ b/src/types/messages.ts @@ -572,7 +572,7 @@ export function isMessage(message: { [key: string]: any }): boolean { 'connect', 'ack', 'badMessage', - ].indexOf(message.type) >= 0; + ].includes(message.type); } export type JSONMessage = Message & { From 1003c88e739cd6a972e776531a57412d9c3fb173 Mon Sep 17 00:00:00 2001 From: Paul Matthew Barrameda Date: Thu, 11 Apr 2019 23:44:03 -0700 Subject: [PATCH 068/152] Added mission parameters Mission parameters is a mix of information required, as well as options. A selected number of missions can be chosen to perform at once, but since the output of one mission goes into the input of the next, the user only needs to add mission info for the first mission. The user will need to input mission options for all missions, however. --- src/types/messages.ts | 114 ++++++++++++++++++++++++++++++++++-------- 1 file changed, 92 insertions(+), 22 deletions(-) diff --git a/src/types/messages.ts b/src/types/messages.ts index 48270e9d..19f9f83c 100644 --- a/src/types/messages.ts +++ b/src/types/messages.ts @@ -65,9 +65,7 @@ interface TaskBase { taskType: string; } -export interface TakeoffTask extends TaskBase { - taskType: 'takeoff'; - +interface TakeoffTaskParameters { lat: HexFloat; lng: HexFloat; alt: HexFloat; @@ -80,6 +78,10 @@ export interface TakeoffTask extends TaskBase { }; } +export interface TakeoffTask extends TaskBase, TakeoffTaskParameters { + taskType: 'takeoff'; +} + function isTakeoffTask(task: Task): boolean { return task.taskType === 'takeoff' && isHexFloat(task.lat) @@ -94,9 +96,7 @@ function isTakeoffTask(task: Task): boolean { && isHexFloat(task.loiter.direction); } -export interface LoiterTask extends TaskBase { - taskType: 'loiter'; - +interface LoiterTaskParameters { lat: HexFloat; lng: HexFloat; alt: HexFloat; @@ -104,6 +104,10 @@ export interface LoiterTask extends TaskBase { direction: HexFloat; } +export interface LoiterTask extends TaskBase, LoiterTaskParameters { + taskType: 'loiter'; +} + function isLoiterTask(task: Task): boolean { return task.taskType === 'loiter' && isHexFloat(task.lat) @@ -114,9 +118,7 @@ function isLoiterTask(task: Task): boolean { } // eslint-disable-next-line @typescript-eslint/interface-name-prefix -export interface ISRSearchTask extends TaskBase { - taskType: 'isrSearch'; - +interface ISRSearchTaskParameters { alt: HexFloat; waypoints: [ { @@ -134,6 +136,11 @@ export interface ISRSearchTask extends TaskBase { ]; } +// eslint-disable-next-line @typescript-eslint/interface-name-prefix +export interface ISRSearchTask extends TaskBase, ISRSearchTaskParameters { + taskType: 'isrSearch'; +} + function isISRSearchTask(task: Task): boolean { return task.taskType === 'isrSearch' && isHexFloat(task.alt) @@ -144,9 +151,7 @@ function isISRSearchTask(task: Task): boolean { ); } -export interface PayloadDropTask extends TaskBase { - taskType: 'payloadDrop'; - +interface PayloadDropTaskParameters { waypoints: [ { lat: HexFloat; @@ -161,6 +166,10 @@ export interface PayloadDropTask extends TaskBase { ]; } +export interface PayloadDropTask extends TaskBase, PayloadDropTaskParameters { + taskType: 'payloadDrop'; +} + function isPayloadDropTask(task: Task): boolean { return task.taskType === 'payloadDrop' && task.waypoints && task.waypoints.length === 2 @@ -171,9 +180,7 @@ function isPayloadDropTask(task: Task): boolean { ); } -export interface LandTask extends TaskBase { - taskType: 'land'; - +interface LandTaskParameters { waypoints: [ { lat: HexFloat; @@ -188,6 +195,10 @@ export interface LandTask extends TaskBase { ]; } +export interface LandTask extends TaskBase, LandTaskParameters { + taskType: 'land'; +} + function isLandTask(task: Task): boolean { return task.taskType === 'land' && task.waypoints && task.waypoints.length === 2 @@ -198,13 +209,15 @@ function isLandTask(task: Task): boolean { ); } -export interface UGVRetrieveTargetTask extends TaskBase { - taskType: 'retrieveTarget'; - +interface UGVRetrieveTargetTaskParameters extends TaskBase { lat: HexFloat; lng: HexFloat; } +export interface UGVRetrieveTargetTask extends TaskBase, UGVRetrieveTargetTaskParameters { + taskType: 'retrieveTarget'; +} + function isUGVRetrieveTargetTask(task: Task): boolean { if (task.taskType !== 'retrieveTarget') return false; @@ -213,19 +226,25 @@ function isUGVRetrieveTargetTask(task: Task): boolean { return isHexFloat(retrieveTargetTask.lat) && isHexFloat(retrieveTargetTask.lng); } -export interface DeliverTargetTask extends TaskBase { +interface DeliverTargetTaskParameters { taskType: 'deliverTarget'; lat: HexFloat; lng: HexFloat; } +export interface DeliverTargetTask extends TaskBase, DeliverTargetTaskParameters { + taskType: 'deliverTarget'; +} + function isDeliverTargetTask(task: Task): boolean { return task.taskType === 'deliverTarget' && isHexFloat(task.lat) && isHexFloat(task.lng); } +// UUVRetrieveTargetTask does not have any parameters. + export interface UUVRetrieveTargetTask extends TaskBase { taskType: 'retrieveTarget'; } @@ -240,9 +259,7 @@ function isUUVRetrieveTargetTask(task: Task): boolean { return !retrieveTargetTask.lat && !retrieveTargetTask.lng; } -export interface QuickScanTask extends TaskBase { - taskType: 'quickScan'; - +export interface QuickScanTaskParameters { waypoints: [ { lat: HexFloat; @@ -263,6 +280,10 @@ export interface QuickScanTask extends TaskBase { ]; } +export interface QuickScanTask extends TaskBase, QuickScanTaskParameters { + taskType: 'quickScan'; +} + function isQuickScanTask(task: Task): boolean { return task.taskType === 'quickScan' && task.waypoints && task.waypoints.length === 4 @@ -271,6 +292,11 @@ function isQuickScanTask(task: Task): boolean { ); } +/* + * DetailedSearchTask parameters are provided by QuickScan and are generated + * autonomously, no need for user interaction. + */ + export interface DetailedSearchTask extends TaskBase { taskType: 'detailedSearch'; @@ -323,6 +349,50 @@ export const taskTypeGuard = { isTask, }; +// eslint-disable-next-line @typescript-eslint/interface-name-prefix +export interface ISRSearchMissionParameters { + takeoff?: TakeoffTaskParameters; + isrSearch: ISRSearchTaskParameters; + land?: LandTaskParameters; +} + +export interface VTOLSearchMissionParameters { + quickScan: QuickScanTaskParameters; +} + +export interface UGVRetreiveMissionParameters { + retrieveTarget: UGVRetreiveMissionParameters; + deliverTarget: DeliverTargetTaskParameters; +} + +// UUVRetrieveMission does not need parameters, as its one task does not need any. + +/** + * Options for the mission. All variables are optional so the UI should + * be able to detech which of the variables are required or not. + */ +export interface MissionOptions { + isrSearch?: { + /** + * Will not require takeoff parameters if this is true. + */ + noTakeoff: boolean; + + /** + * Will not require land parameters if this is true. + */ + noLand: boolean; + }; +} + +/** + * Type of information that should be passed from mission window to the Orchestrator. + */ +export interface MissionParameters { + info: ISRSearchMissionParameters; + options: MissionOptions; +} + interface MessageBase { /** * Type of message. From 02c32864b77f0f57760c5f095fac88dbcc25be00 Mon Sep 17 00:00:00 2001 From: Paul Matthew Barrameda Date: Thu, 11 Apr 2019 23:57:58 -0700 Subject: [PATCH 069/152] Added options to PayloadDrop mission (and added PayloadDropMissionParameters) Also decided to make all information in `ISRSearchMissionParameters` required. The UI should just fill in 0 for all of those fields if the option for that is off. The Mission class will ignore the parameters completely. --- src/types/messages.ts | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/src/types/messages.ts b/src/types/messages.ts index 19f9f83c..11246ac2 100644 --- a/src/types/messages.ts +++ b/src/types/messages.ts @@ -351,15 +351,21 @@ export const taskTypeGuard = { // eslint-disable-next-line @typescript-eslint/interface-name-prefix export interface ISRSearchMissionParameters { - takeoff?: TakeoffTaskParameters; + takeoff: TakeoffTaskParameters; isrSearch: ISRSearchTaskParameters; - land?: LandTaskParameters; + land: LandTaskParameters; } export interface VTOLSearchMissionParameters { quickScan: QuickScanTaskParameters; } +export interface PayloadDropMissionParameters { + takeoff: TakeoffTaskParameters; + payloadDrop: PayloadDropTaskParameters; + land: LandTaskParameters; +} + export interface UGVRetreiveMissionParameters { retrieveTarget: UGVRetreiveMissionParameters; deliverTarget: DeliverTargetTaskParameters; @@ -383,6 +389,18 @@ export interface MissionOptions { */ noLand: boolean; }; + + payloadDrop?: { + /** + * Will not require takeoff parameters if this is true. + */ + noTakeoff: boolean; + + /** + * Will not require land parameters if this is true. + */ + noLand: boolean; + }; } /** From 412a7471b0b90e7bdb4de3c81dd94b39a7bfff70 Mon Sep 17 00:00:00 2001 From: Paul Matthew Barrameda Date: Fri, 12 Apr 2019 03:29:30 -0700 Subject: [PATCH 070/152] Added Mission class (WIP) Need sleep will finish tomorrow, as well as Orchestrator --- src/common/struct/Mission.ts | 193 +++++++++++++++++++++++++++++++++++ src/common/struct/Vehicle.ts | 4 +- src/types/messages.ts | 7 +- 3 files changed, 201 insertions(+), 3 deletions(-) create mode 100644 src/common/struct/Mission.ts diff --git a/src/common/struct/Mission.ts b/src/common/struct/Mission.ts new file mode 100644 index 00000000..a99d9679 --- /dev/null +++ b/src/common/struct/Mission.ts @@ -0,0 +1,193 @@ +import { ipcRenderer } from 'electron'; + +import { JobType, vehicleConfig, VehicleInfo } from '../../static/index'; + +import { + MissionOptions, + MissionParameters, +// Task, +} from '../../types/messages'; +import { VehicleStatus } from '../../types/types'; + +// import DictionaryList from './DictionaryList'; +import Vehicle from './Vehicle'; + +interface VehicleSignature { [vehicleId: string]: Vehicle } + +interface ActiveVehicleSignature { [vehicleId: string]: JobType } + +type MissionStatus = VehicleStatus | 'initializing'; + +/** + * Mission backend for the GCS. Automatically creates and assigns tasks. No provided value should + * be undefined (specifically the vehicle mapping). + */ +export default abstract class Mission { + /** + * Name of mission. + */ + protected abstract missionName: string; + + /** + * Related job types to the mission. + */ + protected abstract jobTypes: Set; + + /** + * All connected vehicles (at least when this class was created). + */ + private vehicles: VehicleSignature; + + /** + * Current status of mission. + */ + private status: MissionStatus = 'ready'; + + /** + * Options for the mission. Generated from the constructor's parameters variable. + * This can determine which tasks are generated as well as other potential things. + * Subclasses implement which tasks get generated so this variable is protected. + */ + protected options: MissionOptions; + + /** + * Map of the id of the vehicle that is currently performing all tasks + * related to the job type value. + * + * The values put together as an array must include all of the mission's job types. + */ + private activeVehicles: ActiveVehicleSignature; + + /** + * All tasks waiting to be executed, mapped by their job type. + * + * Note that these tasks will be set in the Mission superclass, but each subclass is responsible + * of implementing the list of Tasks that are to be performed, in the order they are to be + * performed. + */ + // private waitingTasks: DictionaryList; + + /** + * All vehicles waiting to be assigned a task, mapped by their job type. + */ + // private waitingVehicles: DictionaryList; + + public constructor( + vehicles: VehicleSignature, + parameters: MissionParameters, + activeVehicles: ActiveVehicleSignature, + ) { + this.vehicles = vehicles; + this.options = parameters.options; + this.activeVehicles = activeVehicles; + } + + public initialize(): void { + // Fails to initialize if mission is not in the "ready" state. + if (this.status !== 'ready') { + ipcRenderer.send('post', 'updateMessages', { + type: 'failure', + message: `Something wrong happened in ${this.missionName}`, + }); + return; + } + + // Fails to initialize if provided job types do not match up to this mission's job types + if (!this.checkJobTypesAndActiveVehicles()) { + ipcRenderer.send('post', 'updateMessages', { + type: 'failure', + message: `Provided jobs and/or assigned vehicles for ${this.missionName} is incorrect. Stopping mission`, + }); + return; + } + + let pendingVehicleIds = Object.keys(this.activeVehicles); + const assignedVehicleIds: string[] = []; + + // TODO: improve readability by creating more functions. + + // Assign jobs to vehicles (only if that vehicle's job is relatable to this mission). + pendingVehicleIds.forEach((vehicleId): void => { + const jobType = this.activeVehicles[vehicleId]; + if (this.jobTypes.has(jobType)) { + this.vehicles[vehicleId].assignJob(jobType, (): void => { + // Remove pendingId when vehicle is assigned the job successfully. + pendingVehicleIds = pendingVehicleIds.filter((v): boolean => v !== vehicleId); + assignedVehicleIds.push(vehicleId); + + // Go to "waiting" state when all vehicles have been assigned a job. + if (pendingVehicleIds.length === 0) { + this.status = 'waiting'; + + ipcRenderer.send('post', 'updateMessages', { + type: 'success', + message: `Assigned jobs to all vehicles for ${this.missionName}`, + }); + } + }, (): void => { + /* + * Stop mission if any vehicle failed to get the job. + * TODO: Clear the outbox of the message handler, to stop any other start messages + * to come to any othe vehicle. + */ + this.stopMission(); + + ipcRenderer.send('post', 'updateMessages', { + type: 'success', + message: `Assigning jobs took too long, stopped ${this.missionName}`, + }); + }, (message): void => { + ipcRenderer.send('post', 'updateMessages', { + type: 'success', + message: `${(vehicleConfig.vehicleInfos[vehicleId] as VehicleInfo).name} has entered an error state in ${this.missionName}: ${message || 'No error message specified'}`, + }); + }); + } + }); + + this.status = 'initializing'; + } + + /** + * Check to ensure the following: + * 1. All job types provided cover the required job types for this mission. + * 2. There is a vehicle assigned to each job type. + * + * @param activeVehicles User provided map of vehicle to their job type, function will check + * if this mapping is valid. + */ + private checkJobTypesAndActiveVehicles(): boolean { + const vehicleIds = Object.keys(this.activeVehicles); + + // Create a set of the job types provided by activeVehicles + const activeJobTypes = new Set(); + vehicleIds.forEach((vehicleId): void => { + activeJobTypes.add(this.activeVehicles[vehicleId]); + }); + + // Check #1: ensure the jobs in the activeVehicles cover the required jobs for this mission. + if (!Array.from(this.jobTypes).every((jobType): boolean => activeJobTypes.has(jobType))) { + return false; + } + + /* + * Check #2: if all jobs in the activeVehicles cover the required jobs for the mission, then + * there is a vehicle mapped for every job already. We need to check that all these vehicles + * are in the "ready" state too. + */ + if (!vehicleIds.every((vehicleId): boolean => this.vehicles[vehicleId].getStatus() === 'ready')) { + return false; + } + + return true; + } + + /** + * Stops the mission. + */ + private stopMission(): void { + Object.keys(this.activeVehicles).forEach((vehicleId): void => { + this.vehicles[vehicleId].stop(); + }); + } +} diff --git a/src/common/struct/Vehicle.ts b/src/common/struct/Vehicle.ts index 6b36d9f8..2cf31270 100644 --- a/src/common/struct/Vehicle.ts +++ b/src/common/struct/Vehicle.ts @@ -217,17 +217,17 @@ export default class Vehicle { * Will return true if the mission was assigned successfully. * * @param jobType The job that will be used to accomplish the mission. - * @param options Optional information vehicle will need before performing any tasks. * @param completionCallback Optional callback when vehicle finishes/terminates the mission. * @param disconnectionCallback Optional callback when vehicle disconnects. * @param errorCallback Optional callback when vehicle goes in an error state. + * @param options Optional information vehicle will need before performing any tasks. */ public assignJob( jobType: JobType, - options?: object, completionCallback?: () => void, disconnectionCallback?: () => void, errorCallback?: ErrorCallback, + options?: object, ): boolean { /* * Returns false if a vehicle has already been assigned a mission. diff --git a/src/types/messages.ts b/src/types/messages.ts index 11246ac2..78f23e38 100644 --- a/src/types/messages.ts +++ b/src/types/messages.ts @@ -407,7 +407,12 @@ export interface MissionOptions { * Type of information that should be passed from mission window to the Orchestrator. */ export interface MissionParameters { - info: ISRSearchMissionParameters; + info: ISRSearchMissionParameters + | VTOLSearchMissionParameters + | PayloadDropTaskParameters + | UGVRetreiveMissionParameters + | {}; // The {} stands for UUVRetrieveMissionParameters, an empty object. + options: MissionOptions; } From 917f944e51733fd6de23b3d88572cb379534f55a Mon Sep 17 00:00:00 2001 From: Paul Matthew Barrameda Date: Fri, 12 Apr 2019 11:46:03 -0700 Subject: [PATCH 071/152] Version bump + updated MessageHandler MessageHandler will forward all messages to the Orchestrator to acknowledge, as MessageHandler does not know if the message's source vehicle is connected or not. --- package.json | 6 +-- src/common/MessageHandler.ts | 95 ++++++++++++------------------------ src/util/util.ts | 2 +- 3 files changed, 34 insertions(+), 69 deletions(-) diff --git a/package.json b/package.json index f4dff1ed..b57c07b9 100644 --- a/package.json +++ b/package.json @@ -34,10 +34,10 @@ "@babel/preset-react": "^7.0.0", "@types/chai": "^4.1.7", "@types/mocha": "^5.2.6", - "@types/node": "^11.13.2", + "@types/node": "^11.13.4", "@types/pouchdb": "^6.3.3", "@types/react": "^16.8.13", - "@types/react-dom": "^16.8.3", + "@types/react-dom": "^16.8.4", "@types/react-leaflet": "^2.2.1", "@types/react-virtualized": "^9.21.1", "@types/serialport": "^7.0.2", @@ -58,7 +58,7 @@ "eslint-plugin-import": "^2.16.0", "eslint-plugin-jsx-a11y": "^6.2.1", "eslint-plugin-react": "^7.12.4", - "mocha": "^6.1.2", + "mocha": "^6.1.3", "stylelint": "^9.10.1", "stylelint-config-standard": "^18.2.0", "stylelint-order": "^2.2.1", diff --git a/src/common/MessageHandler.ts b/src/common/MessageHandler.ts index a0947f55..edd4ce7b 100644 --- a/src/common/MessageHandler.ts +++ b/src/common/MessageHandler.ts @@ -2,14 +2,9 @@ import { Event, ipcRenderer } from 'electron'; -import { - config, - vehicleConfig, - VehicleInfo, -} from '../static/index'; +import { config } from '../static/index'; import { AcknowledgementMessage, - BadMessageMessage, messageTypeGuard, JSONMessage, Message, @@ -130,18 +125,6 @@ export default class MessageHandler { }); } - /** - * Acknowledge a message received. - * - * @param jsonMessage The message to acknowledge. - */ - private sendAcknowledgement(jsonMessage: JSONMessage): void { - this.sendMessage(jsonMessage.sid, { - type: 'ack', - ackid: jsonMessage.id, - }); - } - /** * Send a bad message. * @@ -185,7 +168,7 @@ export default class MessageHandler { const jsonMessage = json as JSONMessage; /* - * Update the last message id received from the vehicle. + * Update the last message id received from the vehicle or discard message completely. * This is to be able to properly acknowledge a message. See the note on the following: * * https://ground-control-station.readthedocs.io/en/latest/communications/other-messages.html#acknowledgement-message @@ -193,18 +176,19 @@ export default class MessageHandler { if (!this.receivedMessageId[jsonMessage.sid] || this.receivedMessageId[jsonMessage.sid] as number < jsonMessage.id ) { - this.receivedMessageId[jsonMessage.sid] = jsonMessage.id; - /* - * When this loop passes, it means that GCS is recieving a completely new message - * from the vehicle. We do not want to add messages that have been sent to us - * many times to be put multiple times into the dictionary. + * If this statement passes, it means that GCS is recieving a completely new message + * from the vehicle. */ + this.receivedMessageId[jsonMessage.sid] = jsonMessage.id; this.messageDictionary.push('received', jsonMessage); + } else { + // If the statement fails, it means that the GCS is receiving an old message. Discard it. + return; } /* - * Handles acknowledgement messages. + * Handles acknowledgement messages from any source (even vehicles not connected). * DO NOT ACKNOWLEDGE. * Stops sending the message that the ack message has ascknowledged. */ @@ -216,64 +200,45 @@ export default class MessageHandler { return; } - - /** - * Handles bad messages. - * DO NOT ACKNOWLEDGE. - * Logs error to log container. - */ - if (messageTypeGuard.isBadMessageMessage(jsonMessage)) { - const { error } = jsonMessage as BadMessageMessage; - const { name } = vehicleConfig.vehicleInfos[jsonMessage.sid] as VehicleInfo; - - ipcRenderer.send('post', 'updateMessages', { - type: 'failure', - message: `Received bad message from ${name}: ${error || 'No error message was specified'}`, - }); - return; - } - /* - * Handles connect messages. - * DO NOT ACKNOWLEDGE. ORCHESTRATOR WILL BUILD CONNECTIONACK MESSAGE. - * - * Forward message to Orchestrator. + * All messages that get here will be forwarded to the Orchestrator and will + * be acknowledged there, because the MessageHandler does not know if the message + * comes from a connected vehicle or not. If the vehicle is not connected, the + * Orchestrator will simply ignore the message. */ if (messageTypeGuard.isConnectMessage(jsonMessage)) { + ipcRenderer.send('post', 'handleConnectMessage', jsonMessage); return; } - /* - * Acknowledge the message. - */ - if (this.receivedMessageId[jsonMessage.sid] as number <= jsonMessage.id) { - this.sendAcknowledgement(jsonMessage); - } - - /* - * Handles complete messages. - * Forward message to Orchestrator. - */ if (messageTypeGuard.isCompleteMessage(jsonMessage)) { + ipcRenderer.send('post', 'handleCompleteMessage', jsonMessage); return; } - /* - * Handles point of interest messages. - * Forward message to Orchestrator. - */ if (messageTypeGuard.isPOIMessage(jsonMessage)) { + ipcRenderer.send('post', 'handlePOIMessage', jsonMessage); return; } - /* - * Handles update messages. - * Forward message to Orchestrator. - */ if (messageTypeGuard.isUpdateMessage(jsonMessage)) { + ipcRenderer.send('post', 'handleUpdateMessage', jsonMessage); return; } + // Exception is this message. DO NOT ACKNOWLEDGE. + if (messageTypeGuard.isBadMessageMessage(jsonMessage)) { + ipcRenderer.send('post', 'handleBadMessage', jsonMessage); + + /* + * ipcRenderer.send('post', 'updateMessages', { + * type: 'failure', + * message: `Received bad message from ${name}: ${error || 'No error message + * was specified'}`, + * }); + */ + return; + } /* * Any message that reaches here is a message (has a valid type, id, sid, tid, and time) * but does not have valid properties, or is a message GCS should not receive from vehicles diff --git a/src/util/util.ts b/src/util/util.ts index 84bf2aff..7c79f0df 100644 --- a/src/util/util.ts +++ b/src/util/util.ts @@ -6,7 +6,7 @@ import { VehicleObject } from '../types/types'; * Updates vehicles being shown. This is run on map and vehicle containers. */ export function updateVehicles( - component: Component<{}, { vehicles: { [key: string]: VehicleObject } }>, + component: Component<{}, { vehicles: { [vehicleId: string]: VehicleObject } }>, ...vehicles: VehicleObject[] ): void { const { vehicles: thisVehicles } = component.state; From 11534842720f651a65e7b562c8db7844191677f1 Mon Sep 17 00:00:00 2001 From: Paul Matthew Barrameda Date: Fri, 12 Apr 2019 13:37:07 -0700 Subject: [PATCH 072/152] Fixed vehicle assignJob handler --- src/common/struct/Vehicle.ts | 9 ++++----- src/static/config.json | 2 +- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/common/struct/Vehicle.ts b/src/common/struct/Vehicle.ts index 2cf31270..663e11f0 100644 --- a/src/common/struct/Vehicle.ts +++ b/src/common/struct/Vehicle.ts @@ -251,17 +251,16 @@ export default class Vehicle { this.sendMessage(startMessage); /* - * Create handler that will sets the readyForMission back to true once the vehicle - * goes back to a ready status. Vehicle goes back to "ready" status once it either - * finishes a mission or goes back to a neutral position after its mission is stopped. + * Create handler that will end when vehicle successfully receives the job and goes to a + * waiting state. */ this.updateEventHandler.addHandler('status', (value): boolean => { - if (value === 'ready') { + if (value === 'waiting') { if (completionCallback) completionCallback(); } else if (value === 'disconnected') { if (disconnectionCallback) disconnectionCallback(); } - return value === 'ready'; + return value === 'waiting'; }); return true; diff --git a/src/static/config.json b/src/static/config.json index 23b70a60..41f3b256 100644 --- a/src/static/config.json +++ b/src/static/config.json @@ -3,5 +3,5 @@ "geolocation": true, "vehicleDisconnectionTime": 20, - "messageSendRate": 0.1 + "messageSendRate": 1 } From 821d143d1676a85d411e01d03ccb0a73e38ca4a0 Mon Sep 17 00:00:00 2001 From: tnleang Date: Fri, 12 Apr 2019 14:35:54 -0700 Subject: [PATCH 073/152] Convert missionWindow - Converting missionWindow from js to ts - Reorganize jobs directory - deleted missionContainer in mainWindoow --- src/main/index.ts | 237 +++++++++--------- src/renderer/mainWindow/MainWindow.tsx | 2 - src/renderer/mainWindow/main.css | 8 +- .../mainWindow/mission/MissionContainer.tsx | 227 ----------------- .../mainWindow/mission/MissionTable.tsx | 164 ------------ src/renderer/mainWindow/mission/mission.css | 7 - .../mainWindow/missionWindow/mission.css | 0 src/renderer/missionWindow/MissionWindow.jsx | 52 ---- src/renderer/missionWindow/MissionWindow.tsx | 59 +++++ .../missionParameterLayouts/ISRSearch.tsx | 9 + .../missionParameterLayouts/PayloadDrop.tsx | 9 + .../UGVRetrieveTarget.tsx | 9 + .../UUVRetrieveTarget.tsx | 9 + .../missionParameterLayouts/VTOLSearch.tsx | 9 + .../missionWindow/missions/DeliverPayload.jsx | 47 ---- .../missionWindow/missions/FindTarget.jsx | 63 ----- .../missionWindow/missions/GetTarget.jsx | 39 --- .../missionWindow/missions/Mission.jsx | 107 -------- .../missions/jobs/DeliverTarget.jsx | 20 -- .../missions/jobs/DetailedSearch.jsx | 20 -- .../missionWindow/missions/jobs/ISRSearch.jsx | 20 -- .../missionWindow/missions/jobs/Job.jsx | 147 ----------- .../missionWindow/missions/jobs/Land.jsx | 20 -- .../missionWindow/missions/jobs/Loiter.jsx | 20 -- .../missions/jobs/PayloadDrop.jsx | 20 -- .../missionWindow/missions/jobs/QuickScan.jsx | 20 -- .../missions/jobs/RetrieveTarget.jsx | 20 -- .../missionWindow/missions/jobs/Takeoff.jsx | 20 -- src/types/types.ts | 53 ---- 29 files changed, 228 insertions(+), 1209 deletions(-) delete mode 100644 src/renderer/mainWindow/mission/MissionContainer.tsx delete mode 100644 src/renderer/mainWindow/mission/MissionTable.tsx delete mode 100644 src/renderer/mainWindow/mission/mission.css create mode 100644 src/renderer/mainWindow/missionWindow/mission.css delete mode 100644 src/renderer/missionWindow/MissionWindow.jsx create mode 100644 src/renderer/missionWindow/MissionWindow.tsx create mode 100644 src/renderer/missionWindow/missionParameterLayouts/ISRSearch.tsx create mode 100644 src/renderer/missionWindow/missionParameterLayouts/PayloadDrop.tsx create mode 100644 src/renderer/missionWindow/missionParameterLayouts/UGVRetrieveTarget.tsx create mode 100644 src/renderer/missionWindow/missionParameterLayouts/UUVRetrieveTarget.tsx create mode 100644 src/renderer/missionWindow/missionParameterLayouts/VTOLSearch.tsx delete mode 100644 src/renderer/missionWindow/missions/DeliverPayload.jsx delete mode 100644 src/renderer/missionWindow/missions/FindTarget.jsx delete mode 100644 src/renderer/missionWindow/missions/GetTarget.jsx delete mode 100644 src/renderer/missionWindow/missions/Mission.jsx delete mode 100644 src/renderer/missionWindow/missions/jobs/DeliverTarget.jsx delete mode 100644 src/renderer/missionWindow/missions/jobs/DetailedSearch.jsx delete mode 100644 src/renderer/missionWindow/missions/jobs/ISRSearch.jsx delete mode 100644 src/renderer/missionWindow/missions/jobs/Job.jsx delete mode 100644 src/renderer/missionWindow/missions/jobs/Land.jsx delete mode 100644 src/renderer/missionWindow/missions/jobs/Loiter.jsx delete mode 100644 src/renderer/missionWindow/missions/jobs/PayloadDrop.jsx delete mode 100644 src/renderer/missionWindow/missions/jobs/QuickScan.jsx delete mode 100644 src/renderer/missionWindow/missions/jobs/RetrieveTarget.jsx delete mode 100644 src/renderer/missionWindow/missions/jobs/Takeoff.jsx diff --git a/src/main/index.ts b/src/main/index.ts index f9875f05..34790260 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -159,119 +159,7 @@ function loadConfig(): void { if (missionWindow) { missionWindow.webContents.send('loadConfig', data); } -} - -/** - * Reference to the menu displayed on main window. - */ -const menu: MenuItemConstructorOptions[] = [ - { - label: 'File', - submenu: [ - { - label: 'Open File...', - accelerator: 'CommandOrControl+O', - click: (): void => { loadConfig(); }, - }, - { type: 'separator' }, - { role: 'close' }, - { type: 'separator' }, - { - label: 'Save As...', - accelerator: 'CommandOrControl+S', - click: (): void => { saveConfig(); }, - }, - ], - }, - { - label: 'Edit', - submenu: [ - { role: 'undo' }, - { role: 'redo' }, - { type: 'separator' }, - { role: 'cut' }, - { role: 'copy' }, - { role: 'paste' }, - { role: 'pasteandmatchstyle' }, - { role: 'selectall' }, - ], - }, - { - label: 'View', - submenu: [ - { role: 'reload' }, - { type: 'separator' }, - { role: 'togglefullscreen' }, - { type: 'separator' }, - { role: 'toggledevtools' }, - ], - }, - { - label: 'Locations', - submenu: [ - { - label: 'My Location', - click: (): void => { - if (mainWindow) { - mainWindow.webContents.send('setMapToUserLocation'); - } - }, - }, - { type: 'separator' }, - ], - }, - { - role: 'window', - submenu: [ - { role: 'minimize' }, - ], - }, - { - role: 'help', - submenu: [ - { - label: 'Help', - click: (): void => { shell.openExternal('https://github.com/NGCP/missioncontrol'); }, - }, - ], - }, -]; - -/** - * Adds a list of locations on the menu to allow user to pan to specific location in the map. - * The list of locations comes from ../../resources/locations.json. - */ -function setLocationMenu(): void { - const location = menu.find((m): boolean => m.label === 'Locations'); - if (!location) return; - - const { submenu } = location; - if (!submenu) return; - - // Cast the submenu variable to locationMenu as a MenuItemContsturctorOptions array. - const locationMenu: MenuItemConstructorOptions[] = submenu as MenuItemConstructorOptions[]; - - if (!locationConfig.locations || Object.keys(locationConfig.locations).length === 0) { - locationMenu.push({ - label: 'No locations defined', - enabled: false, - }); - return; - } - - Object.keys(locationConfig.locations).forEach((label): void => { - locationMenu.push({ - label, - click: (menuItem): void => { - if (mainWindow) { - mainWindow.webContents.send('updateMapLocation', locationConfig.locations[menuItem.label]); - } - }, - }); - }); -} - -/** +}/** * Hides the mission window. */ function hideMissionWindow(): void { @@ -332,8 +220,8 @@ function createMissionWindow(): void { title: 'NGCP Mission User Interface', icon, show: false, - width: Math.floor(WIDTH / 3), - minWidth: Math.floor(WIDTH / 3), + width: Math.floor(WIDTH * 2 / 3), + minWidth: Math.floor(WIDTH * 2 / 3), height: HEIGHT, autoHideMenuBar: true, minHeight: HEIGHT, @@ -386,6 +274,125 @@ function showMissionWindow(): void { } } + +/** + * Reference to the menu displayed on main window. + */ +const menu: MenuItemConstructorOptions[] = [ + { + label: 'File', + submenu: [ + { + label: 'Open File...', + accelerator: 'CommandOrControl+O', + click: (): void => { loadConfig(); }, + }, + { type: 'separator' }, + { role: 'close' }, + { type: 'separator' }, + { + label: 'Save As...', + accelerator: 'CommandOrControl+S', + click: (): void => { saveConfig(); }, + }, + ], + }, + { + label: 'Edit', + submenu: [ + { role: 'undo' }, + { role: 'redo' }, + { type: 'separator' }, + { role: 'cut' }, + { role: 'copy' }, + { role: 'paste' }, + { role: 'pasteandmatchstyle' }, + { role: 'selectall' }, + ], + }, + { + label: 'View', + submenu: [ + { role: 'reload' }, + { type: 'separator' }, + { role: 'togglefullscreen' }, + { type: 'separator' }, + { role: 'toggledevtools' }, + ], + }, + { + label: 'Locations', + submenu: [ + { + label: 'My Location', + click: (): void => { + if (mainWindow) { + mainWindow.webContents.send('setMapToUserLocation'); + } + }, + }, + { type: 'separator' }, + ], + }, + { + role: 'window', + submenu: [ + { role: 'minimize' }, + { + label: 'Mission', + click: (): void => { + if (mainWindow) { + showMissionWindow(); + } + }, + }, + ], + }, + { + role: 'help', + submenu: [ + { + label: 'Help', + click: (): void => { shell.openExternal('https://github.com/NGCP/missioncontrol'); }, + }, + ], + }, +]; + +/** + * Adds a list of locations on the menu to allow user to pan to specific location in the map. + * The list of locations comes from ../../resources/locations.json. + */ +function setLocationMenu(): void { + const location = menu.find((m): boolean => m.label === 'Locations'); + if (!location) return; + + const { submenu } = location; + if (!submenu) return; + + // Cast the submenu variable to locationMenu as a MenuItemContsturctorOptions array. + const locationMenu: MenuItemConstructorOptions[] = submenu as MenuItemConstructorOptions[]; + + if (!locationConfig.locations || Object.keys(locationConfig.locations).length === 0) { + locationMenu.push({ + label: 'No locations defined', + enabled: false, + }); + return; + } + + Object.keys(locationConfig.locations).forEach((label): void => { + locationMenu.push({ + label, + click: (menuItem): void => { + if (mainWindow) { + mainWindow.webContents.send('updateMapLocation', locationConfig.locations[menuItem.label]); + } + }, + }); + }); +} + /** * Small menu displayed on the bottom-right corner of windows, or upper-right corner of macOS. */ diff --git a/src/renderer/mainWindow/MainWindow.tsx b/src/renderer/mainWindow/MainWindow.tsx index c3ab60c1..602f373d 100644 --- a/src/renderer/mainWindow/MainWindow.tsx +++ b/src/renderer/mainWindow/MainWindow.tsx @@ -4,7 +4,6 @@ import { ThemeProps } from '../../types/types'; import LogContainer from './log/LogContainer'; import MapContainer from './map/MapContainer'; -import MissionContainer from './mission/MissionContainer'; import VehicleContainer from './vehicle/VehicleContainer'; import './main.css'; @@ -19,7 +18,6 @@ export default function MainWindow(props: ThemeProps): ReactNode {
-
); diff --git a/src/renderer/mainWindow/main.css b/src/renderer/mainWindow/main.css index 6029f7fb..be511314 100644 --- a/src/renderer/mainWindow/main.css +++ b/src/renderer/mainWindow/main.css @@ -8,10 +8,6 @@ grid-area: map; } -.missionContainer { - grid-area: mission; -} - .vehicleContainer { grid-area: vehicle; } @@ -23,8 +19,8 @@ grid-template-areas: 'map map map vehicle' 'map map map vehicle' - 'map map map mission' - 'log log log mission'; + 'map map map vehicle' + 'log log log vehicle'; grid-template-columns: repeat(4, 1fr); grid-template-rows: repeat(4, 1fr); height: 100%; diff --git a/src/renderer/mainWindow/mission/MissionContainer.tsx b/src/renderer/mainWindow/mission/MissionContainer.tsx deleted file mode 100644 index 3d525cba..00000000 --- a/src/renderer/mainWindow/mission/MissionContainer.tsx +++ /dev/null @@ -1,227 +0,0 @@ -import { Event, ipcRenderer } from 'electron'; -import React, { Component, ReactNode } from 'react'; - -import MissionTable from './MissionTable'; - -import { Mission, MissionStatus, ThemeProps } from '../../../types/types'; - -import './mission.css'; - -const statusTypes: { [key: string]: MissionStatus } = { - notStarted: { - name: 'notStarted', - message: 'Not started', - type: 'failure', - }, - started: { - name: 'started', - message: 'Started', - type: 'progress', - }, - paused: { - name: 'paused', - message: 'Paused', - type: 'progress', - }, - completed: { - name: 'completed', - message: 'Completed', - type: 'success', - }, -}; - -/** - * State of the mission container. - */ -interface State { - /** - * Index of the mission that is currently opened in mission window. - * Value of -1 means that no mission is currently opened. - */ - openedMissionIndex: number; - - /** - * Array of missions that needs to be performed. - */ - missions: Mission[]; -} - -/** - * Container that is in charge of managing which missions are not started, started, - * and completed. Any other code that wants to interact with missions must - * go through the mission container. - * - * For example, if the mission window wants to know which missions are not done or not done, - * the mission container can provide this information. - */ -export default class MissionContainer extends Component { - public constructor(props: ThemeProps) { - super(props); - - this.state = { - openedMissionIndex: -1, - - /* - * Missions to perform: - * - * 1) isrSearch: UAV will search for the target. The jobs are the following: - * step 1: Assign `takeoff` to UAV. - * step 2: Assign `isrSearch` to UAV. - * step 3: Assign `quickSearch` to VTOL 1. - * step 4: Assign `detailedSearch` to VTOL 2. - * step 5: Assign `land` to UAV. (optional) - * - * 2) payloadDrop: UAV will drop a payload to the target. The jobs are the following: - * step 1: Assign `takeoff` to UAV. (not needed if UAV did not land from isrSearch) - * step 2: Assign `payloadDrop` to UAV. - * step 3: Assign `land` to UAV. - * - * 3) retrieveTarget: UGV/UUV will go and retrieve the target. The jobs are the following: - * step 1: Assign `retrieveTarget` to UGV/UUV. - * - * 4) deliverTarget: UGV/UUV will go and deliver the target. The jobs are the following: - * step 1: Assign `deliverTarget` to UGV/UUV. - */ - missions: [ - { - name: 'findTarget', - description: "Find target's location", - status: statusTypes.notStarted, - }, - { - name: 'deliverPayload', - description: 'Deliver payload to target', - status: statusTypes.notStarted, - }, - { - name: 'getTarget', - description: 'Get the target', - status: statusTypes.notStarted, - }, - ], - }; - - this.setSelectedMission = this.setSelectedMission.bind(this); - this.updateMission = this.updateMission.bind(this); - } - - public componentDidMount(): void { - ipcRenderer.on('setSelectedMission', (_: Event, index: number): void => this.setSelectedMission(index)); - - ipcRenderer.on('startMission', (): void => this.updateMission('started')); - ipcRenderer.on('stopMission', (): void => this.updateMission('notStarted')); - ipcRenderer.on('completeMission', (): void => this.updateMission('completed')); - - ipcRenderer.on('pauseJob', (): void => this.updateMission('paused')); - ipcRenderer.on('resumeJob', (): void => this.updateMission('started')); - } - - /** - * Selects mission to show on mission window by its index in the missions state. - * Also allows the mission container to update its display on which mission is being shown on the - * mission window. - * - * In other words, function will command the mission window to change which mission to display, - * as well as update the mission container to show which mission is currently opened. - * - * @param index Index that points to mission to open, -1 means mission window was closed. - */ - private setSelectedMission(index: number): void { - const { missions, openedMissionIndex } = this.state; - - // Opens mission window for specified mission. - if (index !== -1) { - // Does not open any window for completed missions. - if (missions[index].status.name === 'completed') return; - - /* - * Passes index to mission window so that when mission is completed, the mission - * window can forward that index back to the mission container to update. - * This helps us have multiple missions running at once. - */ - ipcRenderer.send('post', 'showMissionWindow', missions[index]); - } - - /* - * Updates the mission container to reflect opened/closed mission window. - * Exits function if the same mission was chosen. - */ - if (openedMissionIndex === index) return; - - const newMissions = missions; - - /* - * Set previously opened mission to its default closed state. - * The mission container will not display opened to this mission. - * (eg. "Not started (Open)" => "Not started") - */ - if (openedMissionIndex !== -1) { - const { status } = newMissions[openedMissionIndex]; - newMissions[openedMissionIndex].status = statusTypes[status.name]; - } - - /* - * Set new opened mission to opened. If the mission window is being closed, - * then this if statement will not execute. - * The mission container will display opened to this mission. - * (eg. "Not started" => "Not started (Open)") - */ - if (index !== -1) { - const { status } = newMissions[index]; - - newMissions[index].status = { - ...status, - message: `${statusTypes[status.name].message} (Open)`, - }; - } - - this.setState({ - openedMissionIndex: index, - missions: newMissions, - }); - } - - /** - * Updates mission description in mission container whenever a start/stop/pause/resume - * mission notification is sent. - * - * @param statusName Consists of notStarted/started/paused. See the componentDidMount - * function to see which name corresponds with which notification. - */ - private updateMission(statusName: 'notStarted' |'started' | 'paused' | 'completed'): void { - const { missions, openedMissionIndex } = this.state; - const newMissions = missions; - - if (statusName === 'completed' && missions[openedMissionIndex].status.name !== 'started') { - throw new Error('Mission must be started before completing it'); - } - - /* - * All of the actions that change status are done while mission window is opened, - * so we can assume that the mission is currently open, hence we can add (Open) to the - * end of the mission description. - */ - newMissions[openedMissionIndex].status = { - ...statusTypes[statusName], - message: `${statusTypes[statusName].message} (Open)`, - }; - - // Closes the mission window on stop mission. - if (statusName === 'notStarted' || statusName === 'completed') { - ipcRenderer.send('post', 'hideMissionWindow'); - } - - this.setState({ missions: newMissions }); - } - - public render(): ReactNode { - const { theme } = this.props; - const { missions } = this.state; - - return ( -
- -
- ); - } -} diff --git a/src/renderer/mainWindow/mission/MissionTable.tsx b/src/renderer/mainWindow/mission/MissionTable.tsx deleted file mode 100644 index 61f1ce00..00000000 --- a/src/renderer/mainWindow/mission/MissionTable.tsx +++ /dev/null @@ -1,164 +0,0 @@ -import { ipcRenderer } from 'electron'; -import React, { Component, ReactNode } from 'react'; -import { - AutoSizer, - CellMeasurer, - CellMeasurerCache, - Column, - Index, - RowMouseEventHandlerParams, - Table, - TableCellProps, -} from 'react-virtualized'; - -import { Mission, ThemeProps } from '../../../types/types'; - -interface WidthSignature { - [column: string]: number; -} - -const width: WidthSignature = { - description: 0.6, - status: 0.4, -}; - -/** - * Props for the misson table. - */ -export interface MissionTableProps extends ThemeProps { - /** - * Array of missions that needs to be displayed. - */ - missions: Mission[]; -} - -/** - * Table to render all missions in the mission container. - */ -export default class MissionTable extends Component { - /** - * Renderer for the status column. - */ - private static statusRenderer(props: TableCellProps): ReactNode { - const { dataKey, rowData } = props; - const { type, message } = rowData.status; - - return {message}; - } - - /** - * Callback when a row is clicked in the table. - */ - private static onRowClick(info: RowMouseEventHandlerParams): void { - const { index } = info; - ipcRenderer.send('post', 'setSelectedMission', index); - } - - public constructor(props: MissionTableProps) { - super(props); - - this.width = width; - - this.heightCache = new CellMeasurerCache({ - fixedWidth: true, - minHeight: 40, - }); - - this.rowGetter = this.rowGetter.bind(this); - this.rowClassName = this.rowClassName.bind(this); - this.descriptionRenderer = this.descriptionRenderer.bind(this); - } - - /** - * Width for columns in table. - */ - private width: WidthSignature; - - /** - * Cache that stores the height for rows. Allows the messages to have proper height. - */ - private heightCache: CellMeasurerCache; - - /** - * Callback to obtain data for a certain row in the table. - */ - private rowGetter({ index }: Index): Mission { - const { missions } = this.props; - return missions[index]; - } - - /** - * Callback to generate classname for a certain row in the table. - */ - private rowClassName({ index }: Index): string { - const { theme } = this.props; - - if (theme === 'dark' && index === -1) { - return 'ReactVirtualized__Table__headerRow_dark'; - } if (theme === 'dark') { - return 'ReactVirtualized__Table__row_dark'; - } - return ''; - } - - /** - * Callback to render data for the description column. - */ - private descriptionRenderer(props: TableCellProps): ReactNode { - const { - cellData, - dataKey, - parent, - rowIndex, - } = props; - - return ( - -
- {cellData ? String(cellData) : ''} -
-
- ); - } - - public render(): ReactNode { - const { theme, missions } = this.props; - - return ( - - {({ height, width: tableWidth }): ReactNode => ( -
- - -
- )} -
- ); - } -} diff --git a/src/renderer/mainWindow/mission/mission.css b/src/renderer/mainWindow/mission/mission.css deleted file mode 100644 index d8ec80da..00000000 --- a/src/renderer/mainWindow/mission/mission.css +++ /dev/null @@ -1,7 +0,0 @@ -/* Styles for mission container */ - -.missionContainer .ReactVirtualized__Table__rowColumn .descriptionColumn, -.missionContainer .ReactVirtualized__Table__rowColumn .statusColumn { - padding: 1em 0; - white-space: normal; -} diff --git a/src/renderer/mainWindow/missionWindow/mission.css b/src/renderer/mainWindow/missionWindow/mission.css new file mode 100644 index 00000000..e69de29b diff --git a/src/renderer/missionWindow/MissionWindow.jsx b/src/renderer/missionWindow/MissionWindow.jsx deleted file mode 100644 index 8fde70d0..00000000 --- a/src/renderer/missionWindow/MissionWindow.jsx +++ /dev/null @@ -1,52 +0,0 @@ -import { ipcRenderer } from 'electron'; -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; - -import DeliverPayload from './missions/DeliverPayload'; -import FindTarget from './missions/FindTarget'; -import GetTarget from './missions/GetTarget'; - -import './mission.css'; - -const layouts = { - deliverPayload: DeliverPayload, - findTarget: FindTarget, - getTarget: GetTarget, -}; - -const propTypes = { - theme: PropTypes.oneOf(['light', 'dark']).isRequired, -}; - -const div = () =>
; - -export default class MissionWindow extends Component { - constructor(props) { - super(props); - - this.state = { - openedMission: {}, - }; - } - - componentDidMount() { - ipcRenderer.on('showMissionWindow', (event, mission) => { - this.setState({ openedMission: mission }); - }); - } - - render() { - const { theme } = this.props; - const { openedMission } = this.state; - - const Layout = layouts[openedMission.name] || div; - - return ( -
- -
- ); - } -} - -MissionWindow.propTypes = propTypes; diff --git a/src/renderer/missionWindow/MissionWindow.tsx b/src/renderer/missionWindow/MissionWindow.tsx new file mode 100644 index 00000000..db0c0e5d --- /dev/null +++ b/src/renderer/missionWindow/MissionWindow.tsx @@ -0,0 +1,59 @@ +// import { ipcRenderer } from 'electron'; +import React, { Component, ReactNode } from 'react'; + +import { + ISRSearchMissionParameters, + PayloadDropMissionParameters, + UGVRetreiveMissionParameters, +} from '../../types/messages'; +import { ThemeProps } from '../../types/types'; + +import ISRSearch from './missionParameterLayouts/ISRSearch'; +import PayLoadDrop from './missionParameterLayouts/PayloadDrop'; +import UGVRetrieveTarget from './missionParameterLayouts/UGVRetrieveTarget'; + +const layouts: { [missionName: string]: () => JSX.Element } = { + payloadDrop: PayLoadDrop, + isrSearch: ISRSearch, + ugvRetrieve: UGVRetrieveTarget, +}; + +interface Parameters { + isrSearch: ISRSearchMissionParameters; + payloadDrop: PayloadDropMissionParameters; + ugvRetrieve: UGVRetreiveMissionParameters; +} + +type MissionName = 'isrSearch' | 'vtolSearch' | 'payloadDrop' | 'ugvRetrieve' | 'uuvRetrieve'; + +interface State { + firstMission: MissionName; + parameters?: Parameters; +} + +/** + * + */ +export default class MissionWindow extends Component { + public constructor(props: ThemeProps) { + super(props); + + this.state = { + firstMission: 'isrSearch', + }; + } + + public render(): ReactNode { + const { theme } = this.props; + const { firstMission } = this.state; + + const Layout = layouts[firstMission]; + + return ( +
+ + +
+ ); + } +} diff --git a/src/renderer/missionWindow/missionParameterLayouts/ISRSearch.tsx b/src/renderer/missionWindow/missionParameterLayouts/ISRSearch.tsx new file mode 100644 index 00000000..68e2970c --- /dev/null +++ b/src/renderer/missionWindow/missionParameterLayouts/ISRSearch.tsx @@ -0,0 +1,9 @@ +import React from 'react'; + +export const jobs = ['isrSearch']; + +export default function ISRSearch(): JSX.Element { + return ( +
ISRSearch
+ ); +} diff --git a/src/renderer/missionWindow/missionParameterLayouts/PayloadDrop.tsx b/src/renderer/missionWindow/missionParameterLayouts/PayloadDrop.tsx new file mode 100644 index 00000000..9b165e68 --- /dev/null +++ b/src/renderer/missionWindow/missionParameterLayouts/PayloadDrop.tsx @@ -0,0 +1,9 @@ +import React from 'react'; + +export const jobs = ['payloadDrop']; + +export default function ISRSearch(): JSX.Element { + return ( +
Payload Drop
+ ); +} diff --git a/src/renderer/missionWindow/missionParameterLayouts/UGVRetrieveTarget.tsx b/src/renderer/missionWindow/missionParameterLayouts/UGVRetrieveTarget.tsx new file mode 100644 index 00000000..46192c8b --- /dev/null +++ b/src/renderer/missionWindow/missionParameterLayouts/UGVRetrieveTarget.tsx @@ -0,0 +1,9 @@ +import React from 'react'; + +export const jobs = ['ugvRetrieve']; + +export default function ISRSearch(): JSX.Element { + return ( +
ISRSearch
+ ); +} diff --git a/src/renderer/missionWindow/missionParameterLayouts/UUVRetrieveTarget.tsx b/src/renderer/missionWindow/missionParameterLayouts/UUVRetrieveTarget.tsx new file mode 100644 index 00000000..88be2536 --- /dev/null +++ b/src/renderer/missionWindow/missionParameterLayouts/UUVRetrieveTarget.tsx @@ -0,0 +1,9 @@ +import React from 'react'; + +export const jobs = ['uuvRetrieve']; + +export default function ISRSearch(): JSX.Element { + return ( +
ISRSearch
+ ); +} diff --git a/src/renderer/missionWindow/missionParameterLayouts/VTOLSearch.tsx b/src/renderer/missionWindow/missionParameterLayouts/VTOLSearch.tsx new file mode 100644 index 00000000..6e09e9d5 --- /dev/null +++ b/src/renderer/missionWindow/missionParameterLayouts/VTOLSearch.tsx @@ -0,0 +1,9 @@ +import React from 'react'; + +export const jobs = ['quickScan', 'detailedSearch']; + +export default function ISRSearch(): JSX.Element { + return ( +
ISRSearch
+ ); +} diff --git a/src/renderer/missionWindow/missions/DeliverPayload.jsx b/src/renderer/missionWindow/missions/DeliverPayload.jsx deleted file mode 100644 index 3da2d244..00000000 --- a/src/renderer/missionWindow/missions/DeliverPayload.jsx +++ /dev/null @@ -1,47 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; - -import Land from './jobs/Land'; -import PayloadDrop from './jobs/PayloadDrop'; -import Takeoff from './jobs/Takeoff'; - -import Mission from './Mission'; - -const jobs = [ - { - name: 'takeoff', - description: 'Takeoff', - layout: Takeoff, - optional: true, - pausable: false, - }, - { - name: 'payloadDrop', - description: 'Drop payload to target', - layout: PayloadDrop, - optional: false, - pausable: false, - }, - { - name: 'land', - description: 'Land', - layout: Land, - optional: false, - pausable: false, - }, -]; - -const propTypes = { - mission: PropTypes.shape({ - name: PropTypes.string.isRequired, - description: PropTypes.string.isRequired, - }).isRequired, -}; - -export default function GetTarget({ mission }) { - return ( - - ); -} - -GetTarget.propTypes = propTypes; diff --git a/src/renderer/missionWindow/missions/FindTarget.jsx b/src/renderer/missionWindow/missions/FindTarget.jsx deleted file mode 100644 index 33f2f99c..00000000 --- a/src/renderer/missionWindow/missions/FindTarget.jsx +++ /dev/null @@ -1,63 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; - -import DetailedSearch from './jobs/DetailedSearch'; -import Land from './jobs/Land'; -import ISRSearch from './jobs/ISRSearch'; -import QuickScan from './jobs/QuickScan'; -import Takeoff from './jobs/Takeoff'; - -import Mission from './Mission'; - -const jobs = [ - { - name: 'takeoff', - description: 'Takeoff', - layout: Takeoff, - optional: false, - pausable: false, - }, - { - name: 'isrSearch', - description: 'ISR Search', - layout: ISRSearch, - optional: false, - pausable: false, - }, - { - name: 'quickScan', - description: 'Quick Scan', - layout: QuickScan, - optional: false, - pausable: true, - }, - { - name: 'detailedSearch', - description: 'Detailed Search', - layout: DetailedSearch, - optional: false, - pausable: true, - }, - { - name: 'land', - description: 'Land', - layout: Land, - optional: true, - pausable: false, - }, -]; - -const propTypes = { - mission: PropTypes.shape({ - name: PropTypes.string.isRequired, - description: PropTypes.string.isRequired, - }).isRequired, -}; - -export default function GetTarget({ mission }) { - return ( - - ); -} - -GetTarget.propTypes = propTypes; diff --git a/src/renderer/missionWindow/missions/GetTarget.jsx b/src/renderer/missionWindow/missions/GetTarget.jsx deleted file mode 100644 index ac1db4ab..00000000 --- a/src/renderer/missionWindow/missions/GetTarget.jsx +++ /dev/null @@ -1,39 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; - -import DeliverTarget from './jobs/DeliverTarget'; -import RetrieveTarget from './jobs/RetrieveTarget'; - -import Mission from './Mission'; - -const jobs = [ - { - name: 'retrieveTarget', - description: 'Retrieve the target', - layout: RetrieveTarget, - optional: false, - pausable: true, - }, - { - name: 'deliverTarget', - description: 'Deliver the target back to base', - layout: DeliverTarget, - optional: false, - pausable: true, - }, -]; - -const propTypes = { - mission: PropTypes.shape({ - name: PropTypes.string.isRequired, - description: PropTypes.string.isRequired, - }).isRequired, -}; - -export default function GetTarget({ mission }) { - return ( - - ); -} - -GetTarget.propTypes = propTypes; diff --git a/src/renderer/missionWindow/missions/Mission.jsx b/src/renderer/missionWindow/missions/Mission.jsx deleted file mode 100644 index 6b3a3b77..00000000 --- a/src/renderer/missionWindow/missions/Mission.jsx +++ /dev/null @@ -1,107 +0,0 @@ -import { ipcRenderer } from 'electron'; -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; - -// TODO: Make this component more pretty. -const Default = ({ job }) => ( -
-
-

{job.description}

-
-
- -
-
-); - -Default.propTypes = { - job: PropTypes.shape({ - description: PropTypes.string.isRequired, - }).isRequired, -}; - -const propTypes = { - mission: PropTypes.shape({ - name: PropTypes.string.isRequired, - description: PropTypes.string.isRequired, - }).isRequired, - jobs: PropTypes.arrayOf( - PropTypes.shape({ - name: PropTypes.string.isRequired, - description: PropTypes.string.isRequired, - layout: PropTypes.func.isRequired, - pausable: PropTypes.bool.isRequired, - optional: PropTypes.bool.isRequired, - }).isRequired, - ).isRequired, -}; - -export default class Mission extends Component { - constructor(props) { - super(props); - - this.state = { - currentJobIndex: -1, - }; - - this.nextJob = this.nextJob.bind(this); - this.completeJob = this.completeJob.bind(this); - } - - componentDidMount() { - ipcRenderer.on('startMission', () => { - this.setState({ currentJobIndex: 0 }); - }); - - ipcRenderer.on('stopMission', () => { - this.setState({ currentJobIndex: -1 }); - }); - - ipcRenderer.on('completeJob', this.completeJob); - ipcRenderer.on('nextJob', this.nextJob); - } - - nextJob() { - const { currentJobIndex } = this.state; - this.setState({ currentJobIndex: currentJobIndex + 1 }); - } - - completeJob() { - const { currentJobIndex } = this.state; - const { jobs } = this.props; - - console.log('completed', jobs[currentJobIndex].name); - - if (currentJobIndex === jobs.length - 1) { - /* - * This will forward a notification to the job window that will create a button - * that, when clicked, will send the "completeMission" notification. The - * mission container will handle closing the mission window when the notification - * is sent. - */ - ipcRenderer.send('post', 'showCompleteMissionButton'); - } else { - ipcRenderer.send('post', 'showNextJobButton'); - } - } - - render() { - const { currentJobIndex } = this.state; - const { mission, jobs } = this.props; - - console.log('now going to', currentJobIndex >= 0 ? jobs[currentJobIndex].name : 'main page'); - - const Layout = currentJobIndex >= 0 ? jobs[currentJobIndex].layout : Default; - - return ( -
- = 0 ? jobs[currentJobIndex] : mission} - /> -
- ); - } -} - -Mission.propTypes = propTypes; diff --git a/src/renderer/missionWindow/missions/jobs/DeliverTarget.jsx b/src/renderer/missionWindow/missions/jobs/DeliverTarget.jsx deleted file mode 100644 index fcea0133..00000000 --- a/src/renderer/missionWindow/missions/jobs/DeliverTarget.jsx +++ /dev/null @@ -1,20 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; - -import Job from './Job'; - -const propTypes = { - job: PropTypes.shape({ - name: PropTypes.string.isRequired, - description: PropTypes.string.isRequired, - optional: PropTypes.bool.isRequired, - pausable: PropTypes.bool.isRequired, - }).isRequired, - lastJob: PropTypes.bool.isRequired, -}; - -export default function DeliverTarget(props) { - return ; -} - -DeliverTarget.propTypes = propTypes; diff --git a/src/renderer/missionWindow/missions/jobs/DetailedSearch.jsx b/src/renderer/missionWindow/missions/jobs/DetailedSearch.jsx deleted file mode 100644 index fc2b6f1e..00000000 --- a/src/renderer/missionWindow/missions/jobs/DetailedSearch.jsx +++ /dev/null @@ -1,20 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; - -import Job from './Job'; - -const propTypes = { - job: PropTypes.shape({ - name: PropTypes.string.isRequired, - description: PropTypes.string.isRequired, - optional: PropTypes.bool.isRequired, - pausable: PropTypes.bool.isRequired, - }).isRequired, - lastJob: PropTypes.bool.isRequired, -}; - -export default function DetailedSearch(props) { - return ; -} - -DetailedSearch.propTypes = propTypes; diff --git a/src/renderer/missionWindow/missions/jobs/ISRSearch.jsx b/src/renderer/missionWindow/missions/jobs/ISRSearch.jsx deleted file mode 100644 index ff7e71d1..00000000 --- a/src/renderer/missionWindow/missions/jobs/ISRSearch.jsx +++ /dev/null @@ -1,20 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; - -import Job from './Job'; - -const propTypes = { - job: PropTypes.shape({ - name: PropTypes.string.isRequired, - description: PropTypes.string.isRequired, - optional: PropTypes.bool.isRequired, - pausable: PropTypes.bool.isRequired, - }).isRequired, - lastJob: PropTypes.bool.isRequired, -}; - -export default function ISRSearch(props) { - return ; -} - -ISRSearch.propTypes = propTypes; diff --git a/src/renderer/missionWindow/missions/jobs/Job.jsx b/src/renderer/missionWindow/missions/jobs/Job.jsx deleted file mode 100644 index d45db00f..00000000 --- a/src/renderer/missionWindow/missions/jobs/Job.jsx +++ /dev/null @@ -1,147 +0,0 @@ -import { ipcRenderer } from 'electron'; -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; - -const propTypes = { - job: PropTypes.shape({ - name: PropTypes.string.isRequired, - description: PropTypes.string.isRequired, - optional: PropTypes.bool.isRequired, - pausable: PropTypes.bool.isRequired, - }).isRequired, - lastJob: PropTypes.bool.isRequired, -}; - -const defaultState = { - // True if the button should be shown, false if it should be hidden. - startJobButton: true, - stopMissionButton: true, - pauseJobButton: false, - resumeJobButton: false, - nextJobButton: false, - completeMissionButton: false, -}; - -export default class Job extends Component { - constructor(props) { - super(props); - - const { lastJob, job } = this.props; - - this.state = { ...defaultState }; - - this.sendStartJob = this.sendStartJob.bind(this); - this.sendStopMission = this.sendStopMission.bind(this); - this.sendPauseJob = this.sendPauseJob.bind(this); - this.sendResumeJob = this.sendResumeJob.bind(this); - this.sendNextJob = this.sendNextJob.bind(this); - this.sendCompleteMission = this.sendCompleteMission.bind(this); - - this.buttons = { - startJobButton: , - stopMissionButton: , - pauseJobButton: , - resumeJobButton: , - nextJobButton: , - completeMissionButton: , - skipJobButton: , - }; - } - - componentDidMount() { - ipcRenderer.on('showNextJobButton', () => { - this.setState({ nextJobButton: true }); - }); - ipcRenderer.on('showCompleteMissionButton', () => { - this.setState({ completeMissionButton: true }); - }); - } - - sendStartJob() { - // const { job } = this.props; - - this.setState({ - startJobButton: false, - pauseJobButton: true, - }); - - /* - * jobUtil.sendStartJob({ - * jobType: job.name, - * // TODO: Add missionInfo. - * }); - */ - } - - sendStopMission() { - // Reset state for future use of this job. - this.setState({ ...defaultState }); - - // mission.sendStopMission(); - } - - sendPauseJob() { - this.setState({ - pauseJobButton: false, - resumeJobButton: true, - }); - - // jobUtil.sendPauseJob(); - } - - sendResumeJob() { - this.setState({ - resumeJobButton: false, - pauseJobButton: true, - }); - - // jobUtil.sendResumeJob(); - } - - sendNextJob() { - // Reset state for future use of this job. - this.setState({ ...defaultState }); - - ipcRenderer.send('post', 'nextJob'); - } - - sendCompleteMission() { - // Reset state for future use of this job. - this.setState({ ...defaultState }); - - // mission.sendCompleteMission(); - } - - render() { - const { job } = this.props; - const { - startJobButton, - stopMissionButton, - pauseJobButton, - resumeJobButton, - nextJobButton, - completeMissionButton, - } = this.state; - - return ( -
-
-

{job.description}

-
-
- {startJobButton && this.buttons.startJobButton} - {!nextJobButton && !completeMissionButton - && pauseJobButton && this.buttons.pauseJobButton} - {!nextJobButton && !completeMissionButton - && resumeJobButton && this.buttons.resumeJobButton} - {nextJobButton && this.buttons.nextJobButton} - {completeMissionButton && this.buttons.completeMissionButton} - {stopMissionButton && this.buttons.stopMissionButton} - {!nextJobButton && !completeMissionButton && job.optional && this.buttons.skipJobButton} -
-
- ); - } -} - -Job.propTypes = propTypes; diff --git a/src/renderer/missionWindow/missions/jobs/Land.jsx b/src/renderer/missionWindow/missions/jobs/Land.jsx deleted file mode 100644 index 1aa7fb27..00000000 --- a/src/renderer/missionWindow/missions/jobs/Land.jsx +++ /dev/null @@ -1,20 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; - -import Job from './Job'; - -const propTypes = { - job: PropTypes.shape({ - name: PropTypes.string.isRequired, - description: PropTypes.string.isRequired, - optional: PropTypes.bool.isRequired, - pausable: PropTypes.bool.isRequired, - }).isRequired, - lastJob: PropTypes.bool.isRequired, -}; - -export default function Land(props) { - return ; -} - -Land.propTypes = propTypes; diff --git a/src/renderer/missionWindow/missions/jobs/Loiter.jsx b/src/renderer/missionWindow/missions/jobs/Loiter.jsx deleted file mode 100644 index 8e1b5fe8..00000000 --- a/src/renderer/missionWindow/missions/jobs/Loiter.jsx +++ /dev/null @@ -1,20 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; - -import Job from './Job'; - -const propTypes = { - job: PropTypes.shape({ - name: PropTypes.string.isRequired, - description: PropTypes.string.isRequired, - optional: PropTypes.bool.isRequired, - pausable: PropTypes.bool.isRequired, - }).isRequired, - lastJob: PropTypes.bool.isRequired, -}; - -export default function Loiter(props) { - return ; -} - -Loiter.propTypes = propTypes; diff --git a/src/renderer/missionWindow/missions/jobs/PayloadDrop.jsx b/src/renderer/missionWindow/missions/jobs/PayloadDrop.jsx deleted file mode 100644 index b91c914b..00000000 --- a/src/renderer/missionWindow/missions/jobs/PayloadDrop.jsx +++ /dev/null @@ -1,20 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; - -import Job from './Job'; - -const propTypes = { - job: PropTypes.shape({ - name: PropTypes.string.isRequired, - description: PropTypes.string.isRequired, - optional: PropTypes.bool.isRequired, - pausable: PropTypes.bool.isRequired, - }).isRequired, - lastJob: PropTypes.bool.isRequired, -}; - -export default function PayloadDrop(props) { - return ; -} - -PayloadDrop.propTypes = propTypes; diff --git a/src/renderer/missionWindow/missions/jobs/QuickScan.jsx b/src/renderer/missionWindow/missions/jobs/QuickScan.jsx deleted file mode 100644 index 3e9b14d4..00000000 --- a/src/renderer/missionWindow/missions/jobs/QuickScan.jsx +++ /dev/null @@ -1,20 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; - -import Job from './Job'; - -const propTypes = { - job: PropTypes.shape({ - name: PropTypes.string.isRequired, - description: PropTypes.string.isRequired, - optional: PropTypes.bool.isRequired, - pausable: PropTypes.bool.isRequired, - }).isRequired, - lastJob: PropTypes.bool.isRequired, -}; - -export default function QuickScan(props) { - return ; -} - -QuickScan.propTypes = propTypes; diff --git a/src/renderer/missionWindow/missions/jobs/RetrieveTarget.jsx b/src/renderer/missionWindow/missions/jobs/RetrieveTarget.jsx deleted file mode 100644 index 17f7d0af..00000000 --- a/src/renderer/missionWindow/missions/jobs/RetrieveTarget.jsx +++ /dev/null @@ -1,20 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; - -import Job from './Job'; - -const propTypes = { - job: PropTypes.shape({ - name: PropTypes.string.isRequired, - description: PropTypes.string.isRequired, - optional: PropTypes.bool.isRequired, - pausable: PropTypes.bool.isRequired, - }).isRequired, - lastJob: PropTypes.bool.isRequired, -}; - -export default function RetrieveTarget(props) { - return ; -} - -RetrieveTarget.propTypes = propTypes; diff --git a/src/renderer/missionWindow/missions/jobs/Takeoff.jsx b/src/renderer/missionWindow/missions/jobs/Takeoff.jsx deleted file mode 100644 index c51b19b2..00000000 --- a/src/renderer/missionWindow/missions/jobs/Takeoff.jsx +++ /dev/null @@ -1,20 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; - -import Job from './Job'; - -const propTypes = { - job: PropTypes.shape({ - name: PropTypes.string.isRequired, - description: PropTypes.string.isRequired, - optional: PropTypes.bool.isRequired, - pausable: PropTypes.bool.isRequired, - }).isRequired, - lastJob: PropTypes.bool.isRequired, -}; - -export default function Takeoff(props) { - return ; -} - -Takeoff.propTypes = propTypes; diff --git a/src/types/types.ts b/src/types/types.ts index bf2ad7ad..82ec218e 100644 --- a/src/types/types.ts +++ b/src/types/types.ts @@ -96,59 +96,6 @@ export interface VehicleObject { heading?: number; } -/** - * Status of a mission. - */ -export interface MissionStatus { - name: 'notStarted' |'started' | 'paused' | 'completed'; - message: string; - type: MessageType; -} - -/** - * Structure of a typical mission. - */ -export interface Mission { - /** - * Name of the mission. - */ - name?: string; - /** - * Description of the mission. - */ - description: string; - - /** - * Status of the misison. - */ - status: MissionStatus; -} - -/** - * Structure of a typical job. - */ -export interface Job { - /** - * Name of the job. - */ - name: string; - - /** - * Description of the job. - */ - description: string; - - /** - * Whether or not the job can be skipped. - */ - optional?: boolean; - - /** - * Whether or not the user can pause the job. - */ - pausable?: boolean; -} - /** * Data contents for information that is loaded from a configuration file. */ From 5342d89859ce168648cb19ea70e91e448809fbc8 Mon Sep 17 00:00:00 2001 From: Paul Matthew Barrameda Date: Sat, 13 Apr 2019 02:20:03 -0700 Subject: [PATCH 074/152] Added support to successfully build package (npm run build) --- package.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/package.json b/package.json index b57c07b9..7417916d 100644 --- a/package.json +++ b/package.json @@ -67,6 +67,9 @@ "typescript": "^3.4.3", "webpack": "^4.29.6" }, + "build": { + "npmRebuild": false + }, "electronWebpack": { "main": { "webpackConfig": "webpack.config.js" From 7cf99a775caae05943c2e3abf201adce0126f14e Mon Sep 17 00:00:00 2001 From: Paul Matthew Barrameda Date: Sat, 13 Apr 2019 03:03:51 -0700 Subject: [PATCH 075/152] Version bump `eslint-plugin-import` causes TypeError issues https://github.com/benmosher/eslint-plugin-import/issues/1322 --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 7417916d..de25a65a 100644 --- a/package.json +++ b/package.json @@ -55,7 +55,7 @@ "eslint-config-airbnb": "^17.1.0", "eslint-import-resolver-typescript": "^1.1.1", "eslint-plugin-chai-friendly": "^0.4.1", - "eslint-plugin-import": "^2.16.0", + "eslint-plugin-import": "^2.17.0", "eslint-plugin-jsx-a11y": "^6.2.1", "eslint-plugin-react": "^7.12.4", "mocha": "^6.1.3", @@ -65,7 +65,7 @@ "ts-mocha": "^6.0.0", "ts-node": "^8.0.3", "typescript": "^3.4.3", - "webpack": "^4.29.6" + "webpack": "^4.30.0" }, "build": { "npmRebuild": false From 5b3b9069001f833ee29131c4cf8c8c3c2a546cb0 Mon Sep 17 00:00:00 2001 From: Paul Matthew Barrameda Date: Sat, 13 Apr 2019 07:31:59 -0700 Subject: [PATCH 076/152] FInished Mission class I'm tired time to sleep --- .eslintrc.yml | 1 + src/common/MessageHandler.ts | 10 + src/common/struct/Mission.ts | 412 ++++++++++++++++++++++++----- src/common/struct/UpdateHandler.ts | 19 +- src/static/config.json | 3 +- src/types/messages.ts | 19 +- 6 files changed, 392 insertions(+), 72 deletions(-) diff --git a/.eslintrc.yml b/.eslintrc.yml index afba283c..c79416a7 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -33,6 +33,7 @@ settings: rules: # Turning off this because its buggy https://github.com/benmosher/eslint-plugin-import/pull/1304 import/named: off + import/no-useless-path-segments: off # Turn off linting about having wrong line ending, handled in .gitattributes linebreak-style: off diff --git a/src/common/MessageHandler.ts b/src/common/MessageHandler.ts index edd4ce7b..11bb36fb 100644 --- a/src/common/MessageHandler.ts +++ b/src/common/MessageHandler.ts @@ -47,6 +47,8 @@ export default class MessageHandler { ipcRenderer.on('sendMessage', (_: Event, vehicleId: number, message: Message): void => this.sendMessage(vehicleId, message)); ipcRenderer.on('receiveMessage', (_: Event, text: string): void => this.receiveMessage(text)); + + ipcRenderer.on('stopSendingMessages', this.stopSendingMessages); } /** @@ -246,4 +248,12 @@ export default class MessageHandler { */ this.sendBadMessage(jsonMessage, `Message of type ${jsonMessage.type} is invalid`); } + + /** + * Stops sending all messages (literally discards all messages that are being sent, but they + * will still be in the "sent" dictionary). + */ + private stopSendingMessages(): void { + this.updateHandler.clearHandlers(); + } } diff --git a/src/common/struct/Mission.ts b/src/common/struct/Mission.ts index a99d9679..546a9374 100644 --- a/src/common/struct/Mission.ts +++ b/src/common/struct/Mission.ts @@ -1,22 +1,32 @@ import { ipcRenderer } from 'electron'; -import { JobType, vehicleConfig, VehicleInfo } from '../../static/index'; +import { + config, + JobType, + vehicleConfig, + VehicleInfo, +} from '../../static/index'; import { + JSONMessage, + messageTypeGuard, + MissionInformation, MissionOptions, MissionParameters, -// Task, + Task, + UpdateMessage, } from '../../types/messages'; import { VehicleStatus } from '../../types/types'; -// import DictionaryList from './DictionaryList'; +import DictionaryList from './DictionaryList'; import Vehicle from './Vehicle'; +import UpdateHandler from './UpdateHandler'; interface VehicleSignature { [vehicleId: string]: Vehicle } -interface ActiveVehicleSignature { [vehicleId: string]: JobType } +interface VehicleMappingSignature { [vehicleId: string]: JobType } -type MissionStatus = VehicleStatus | 'initializing'; +type MissionStatus = 'ready' | 'initializing' | 'waiting' | 'running' | 'error'; /** * Mission backend for the GCS. Automatically creates and assigns tasks. No provided value should @@ -50,13 +60,26 @@ export default abstract class Mission { */ protected options: MissionOptions; + /** + * Parameters for the mission. Used to generate tasks. + * Subclasses implement which tasks get generated so this variable is protected. + */ + protected parameters: MissionParameters; + /** * Map of the id of the vehicle that is currently performing all tasks - * related to the job type value. + * related to the job type value. This is used from mission initliaization until + * assigning jobs to vehicles. When mission starts, this gets reassigned to + * waitingVehicles * * The values put together as an array must include all of the mission's job types. */ - private activeVehicles: ActiveVehicleSignature; + private vehicleMapping: VehicleMappingSignature; + + /** + * Map of the id of the vehicle to the task it is performing. + */ + private activeTasks = new Map(); /** * All tasks waiting to be executed, mapped by their job type. @@ -65,30 +88,71 @@ export default abstract class Mission { * of implementing the list of Tasks that are to be performed, in the order they are to be * performed. */ - // private waitingTasks: DictionaryList; + private waitingTasks = new DictionaryList(); /** * All vehicles waiting to be assigned a task, mapped by their job type. */ - // private waitingVehicles: DictionaryList; + private waitingVehicles = new DictionaryList(); + + /** + * Callback to occur when + */ + private completionCallback: (information: MissionInformation) => void; + + /** + * Handles different states of the vehicle. + */ + private statusEventHandler = new UpdateHandler(); public constructor( + completionCallback: (information: MissionInformation) => void, vehicles: VehicleSignature, - parameters: MissionParameters, - activeVehicles: ActiveVehicleSignature, + information: MissionInformation, + vehicleMapping: VehicleMappingSignature, ) { + this.completionCallback = completionCallback; this.vehicles = vehicles; - this.options = parameters.options; - this.activeVehicles = activeVehicles; + this.parameters = information.parameters; + this.options = information.options; + this.vehicleMapping = vehicleMapping; + + /* + * Create event handler that will handle all changes of status: + * 1. Will assignJobs if status changes to "initializing". + * 2. Will start mission if status changs to "waiting". + * 3. Will remove this handler if status goes back to "ready", which means: + * - mission was terminated before it completed + * - mission was terminated after it completed + */ + this.statusEventHandler.addHandler('status', (status): boolean => { + this.status = status; + + if (status === 'initializing') { + this.assignJobs(); + } else if (status === 'waiting') { + this.start(); + } + + return status === 'ready'; + }); + + this.initialize(); } - public initialize(): void { - // Fails to initialize if mission is not in the "ready" state. + /** + * Initializes the mission. This is run first, and will run assignJobs if run successfully. + * Will set status to "initlializing" after running successfully. + */ + private initialize(): void { + // Stops mission if mission is not in the "ready" state. if (this.status !== 'ready') { ipcRenderer.send('post', 'updateMessages', { type: 'failure', message: `Something wrong happened in ${this.missionName}`, }); + this.stop(); + this.statusEventHandler.event('status', 'ready'); return; } @@ -98,54 +162,254 @@ export default abstract class Mission { type: 'failure', message: `Provided jobs and/or assigned vehicles for ${this.missionName} is incorrect. Stopping mission`, }); + this.statusEventHandler.event('status', 'ready'); return; } - let pendingVehicleIds = Object.keys(this.activeVehicles); + /* + * Sets mission status to "initializing". This will trigger the assignJobs function. + */ + this.statusEventHandler.event('status', 'initializing'); + } + + /** + * Assigns vehicles the jobs. Will set the mission status to "waiting" once + * all vehicles have been assigned their jobs successfully. + */ + private assignJobs(): void { + // Fails to assign jobs if mission is not in the "initializing" state. + if (this.status !== 'initializing') { + ipcRenderer.send('post', 'updateMessages', { + type: 'failure', + message: `Something wrong happened in ${this.missionName}`, + }); + this.stop(); + this.statusEventHandler.event('status', 'ready'); + return; + } + + let pendingVehicleIds = Object.keys(this.vehicleMapping); const assignedVehicleIds: string[] = []; - // TODO: improve readability by creating more functions. + /* + * The three functions below are callbacks to when the vehicles connect. + * I put them here for readability. + */ + + /** + * Triggers when vehicle successfully receives the job assignment. + */ + const onSuccess = (vehicleId: string): void => { + // Remove pendingId when vehicle is assigned the job successfully. + pendingVehicleIds = pendingVehicleIds.filter((v): boolean => v !== vehicleId); + assignedVehicleIds.push(vehicleId); + + + /* + * Go to "waiting" state when all vehicles have been assigned a job. + * This will trigger this.start(). + */ + if (pendingVehicleIds.length === 0) { + ipcRenderer.send('post', 'updateMessages', { + type: 'success', + message: `Assigned jobs to all vehicles for ${this.missionName}`, + }); + + this.statusEventHandler.event('status', 'waiting'); + } + }; + + /** + * Triggers when vehicle has disconnected. + */ + const onDisconnect = (): void => { + /* + * Stop mission if any vehicle failed to get the job. Also stops sending messages + * to any other vehicle if there is somehow a "start" message on queue. + */ + ipcRenderer.send('post', 'stopSendingMessages'); + this.stop(); + }; + + /** + * Triggers when vehicle enters an error state. + */ + const onError = (vehicleId: string, message?: string): void => { + ipcRenderer.send('post', 'updateMessages', { + type: 'success', + message: `${(vehicleConfig.vehicleInfos[vehicleId] as VehicleInfo).name} has entered an error state in ${this.missionName}: ${message || 'No error message specified'}`, + }); + + this.handleUnresponsiveVehicle(vehicleId); + }; + // Assign jobs to vehicles (only if that vehicle's job is relatable to this mission). pendingVehicleIds.forEach((vehicleId): void => { - const jobType = this.activeVehicles[vehicleId]; + const jobType = this.vehicleMapping[vehicleId]; if (this.jobTypes.has(jobType)) { - this.vehicles[vehicleId].assignJob(jobType, (): void => { - // Remove pendingId when vehicle is assigned the job successfully. - pendingVehicleIds = pendingVehicleIds.filter((v): boolean => v !== vehicleId); - assignedVehicleIds.push(vehicleId); - - // Go to "waiting" state when all vehicles have been assigned a job. - if (pendingVehicleIds.length === 0) { - this.status = 'waiting'; - - ipcRenderer.send('post', 'updateMessages', { - type: 'success', - message: `Assigned jobs to all vehicles for ${this.missionName}`, - }); - } - }, (): void => { - /* - * Stop mission if any vehicle failed to get the job. - * TODO: Clear the outbox of the message handler, to stop any other start messages - * to come to any othe vehicle. - */ - this.stopMission(); + this.vehicles[vehicleId].assignJob(jobType, + (): void => onSuccess(vehicleId), + (): void => onDisconnect(), + (message): void => onError(vehicleId, message)); + } + }); + } - ipcRenderer.send('post', 'updateMessages', { - type: 'success', - message: `Assigning jobs took too long, stopped ${this.missionName}`, - }); - }, (message): void => { - ipcRenderer.send('post', 'updateMessages', { - type: 'success', - message: `${(vehicleConfig.vehicleInfos[vehicleId] as VehicleInfo).name} has entered an error state in ${this.missionName}: ${message || 'No error message specified'}`, - }); - }); + /** + * Gets all the tasks and starts assigning them to vehicles. + */ + private start(): void { + // Fails to initialize if mission is not in the "ready" state. + if (this.status !== 'waiting') { + ipcRenderer.send('post', 'updateMessages', { + type: 'failure', + message: `Something wrong happened in ${this.missionName}`, + }); + this.stop(); + this.statusEventHandler.event('status', 'ready'); + return; + } + + // Adds all the tasks to the waitingTasks, if the jobType relates to this mission. + const jobTasks = this.generateTasks(); + Object.keys(jobTasks).forEach((jobType): void => { + if (vehicleConfig.isJobType(jobType) && this.jobTypes.has(jobType as JobType)) { + this.waitingTasks.push(jobType, ...jobTasks.get(jobType) as Task[]); } }); - this.status = 'initializing'; + // Remaps the vehicleMapping to waitingVehicles (vehicleId => jobType to jobType => Vehicle[]). + Object.keys(this.vehicleMapping).forEach((vehicleId): void => { + const jobType = this.vehicleMapping[vehicleId]; + const vehicle = this.vehicles[vehicleId]; + + this.waitingVehicles.push(jobType, vehicle); + }); + + /* + * Assign tasks to vehicles. Will keep assigning tasks to vehicles until there is either + * waiting vehicles or waiting tasks. + */ + this.waitingVehicles.keys().forEach((jobType): void => { + while ( + (this.waitingTasks.get(jobType) as Task[]).length > 0 + && (this.waitingVehicles.get(jobType) as Vehicle[]).length > 0 + ) { + const task = this.waitingTasks.shift(jobType) as Task; + const vehicle = this.waitingVehicles.shift(jobType) as Vehicle; + + vehicle.assignTask(task); + this.activeTasks.set(vehicle.getVehicleId(), task); + } + }); + + // Update mission status to "running". + this.statusEventHandler.event('status', 'running'); + } + + /** + * This will be called from the Orchestrator. The Orchestrator should make sure that the + * vehicleId being provided is a valid vehicleId of a vehicle that was connected when the + * mission was initialized. + * + * This is the event when a complete message has been sent from a vehicle. The mission + * will either give that vehicle the next task or complete the mission. + * + * Subclasses can override this to check for even more types of messages. + */ + public update(message: JSONMessage): void { + /* + * We run this statement in the case a vehicle goes to error, we keep track of its update + * messages and see if it goes back to "running". Otherwise we will have to stop the + * mission. See the handler that is created in the handleUnresponsiveVehicle() function. + */ + if (this.status === 'error' && messageTypeGuard.isUpdateMessage(message)) { + this.statusEventHandler.event('vehicleStatus', (message as UpdateMessage).status); + } + + // The base mission class only checks for completion messages. + if (messageTypeGuard.isCompleteMessage(message)) { + const jobType = this.vehicleMapping[message.sid]; + + if ((this.waitingTasks.get(jobType) as Task[]).length > 0) { + // Assigns the next task in that job to the vehicle. + const newTask = this.waitingTasks.shift(jobType) as Task; + + this.vehicles[message.sid].assignTask(newTask); + this.activeTasks.set(message.sid, newTask); + } else { + // All tasks in that jobType are complete, so this vehicle will go back to waiting vehicles. + this.waitingVehicles.push(jobType, this.vehicles[message.sid]); + this.activeTasks.delete(message.sid); + } + + /* + * Will execute if mission is finally complete (no waiting tasks, no tasks happening). + * This includes sending completeMission to the user interface and the Orchestrator as well + * as perform a callback function that will use the terminated data (data that was obtained) + * throughout the mission. + */ + if (this.activeTasks.size === 0 && this.waitingTasks.size() === 0) { + ipcRenderer.send('post', 'completeMission', this.missionName); + this.completionCallback(this.generateTerminatedData()); + this.stop(); + this.statusEventHandler.event('status', 'ready'); + } else { + // This should never happen. There is a big issue in the system if this somehow happens. + ipcRenderer.send('post', 'updateMessages', { + type: 'failure', + message: `Mission completion failed in ${this.missionName}. Stopping mission`, + }); + this.stop(); + this.statusEventHandler.event('status', 'ready'); + } + } + } + + /** + * Adds a new task to the mission. Should only be called on its subclasses (probably through + * an overriden update() function). + */ + protected addTask(jobType: string, task: Task): void { + // Fails to initialize if mission is not in the "ready" state. + if (this.status !== 'running') { + ipcRenderer.send('post', 'updateMessages', { + type: 'failure', + message: `Tried to add ${task.taskType} task while ${this.missionName} is not running`, + }); + this.stop(); + this.statusEventHandler.event('status', 'ready'); + return; + } + + /* + * Assign the task right away if there is a vehicle waiting for a task for that + * specific jobType. Otherwise just store it in waiting tasks so that when a vehicle + * is available, it can perform that task. + */ + if (this.waitingVehicles.get(jobType) + && (this.waitingVehicles.get(jobType) as Vehicle[]).length > 0 + ) { + const vehicle = this.waitingVehicles.shift(jobType) as Vehicle; + vehicle.assignTask(task); + this.activeTasks.set(vehicle.getVehicleId(), task); + } else { + this.waitingTasks.push(jobType, task); + } + } + + /** + * Stops the mission (either means mission was stopped manually or mission was completed). + */ + private stop(): void { + Object.keys(this.vehicleMapping).forEach((vehicleId): void => { + this.vehicles[vehicleId].stop(); + }); + /* + * TODO: Print out the output data for this mission, so that we do not have to start over from + * step 1. + */ } /** @@ -153,25 +417,25 @@ export default abstract class Mission { * 1. All job types provided cover the required job types for this mission. * 2. There is a vehicle assigned to each job type. * - * @param activeVehicles User provided map of vehicle to their job type, function will check + * @param vehicleMapping User provided map of vehicle to their job type, function will check * if this mapping is valid. */ private checkJobTypesAndActiveVehicles(): boolean { - const vehicleIds = Object.keys(this.activeVehicles); + const vehicleIds = Object.keys(this.vehicleMapping); - // Create a set of the job types provided by activeVehicles + // Create a set of the job types provided by vehicleMapping const activeJobTypes = new Set(); vehicleIds.forEach((vehicleId): void => { - activeJobTypes.add(this.activeVehicles[vehicleId]); + activeJobTypes.add(this.vehicleMapping[vehicleId]); }); - // Check #1: ensure the jobs in the activeVehicles cover the required jobs for this mission. + // Check #1: ensure the jobs in the vehicleMapping cover the required jobs for this mission. if (!Array.from(this.jobTypes).every((jobType): boolean => activeJobTypes.has(jobType))) { return false; } /* - * Check #2: if all jobs in the activeVehicles cover the required jobs for the mission, then + * Check #2: if all jobs in the vehicleMapping cover the required jobs for the mission, then * there is a vehicle mapped for every job already. We need to check that all these vehicles * are in the "ready" state too. */ @@ -183,11 +447,39 @@ export default abstract class Mission { } /** - * Stops the mission. + * Handles a vehicle in an error state. We will give the vehicle a few seconds to get + * out of error state before ending the mission and log that user should take manual + * control of the vehicle as soon as possible. */ - private stopMission(): void { - Object.keys(this.activeVehicles).forEach((vehicleId): void => { - this.vehicles[vehicleId].stop(); + private handleUnresponsiveVehicle(vehicleId: string): void { + ipcRenderer.send('post', 'updateMessages', { + type: 'progress', + message: `Will see if the vehicle will exit the error state within ${config.vehicleAllowedErrorTime} seconds`, }); + + this.status = 'error'; + + this.statusEventHandler.addHandler('vehicleStatus', + (status: VehicleStatus): boolean => status !== 'error', + { + callback: (): void => { + ipcRenderer.send('post', 'updateMessages', { + type: 'progress', + message: `${(vehicleConfig.vehicleInfos[vehicleId] as VehicleInfo).name} failed to exit the error state on time. Stopping mission. User, take manual control of vehicle as soon as possible`, + }); + }, + time: config.vehicleDisconnectionTime * 1000, + }); } + + /** + * Generates all tasks to perform, given this.options and this.parameters. + */ + protected abstract generateTasks(): DictionaryList; + + /** + * Generates data that comes out of the specific mission, either to be given to the user + * or to the next mission. The data should be formatted to support the next mission. + */ + protected abstract generateTerminatedData(): MissionInformation; } diff --git a/src/common/struct/UpdateHandler.ts b/src/common/struct/UpdateHandler.ts index 494e13ce..c6cd4226 100644 --- a/src/common/struct/UpdateHandler.ts +++ b/src/common/struct/UpdateHandler.ts @@ -100,15 +100,15 @@ export default class UpdateHandler { if (!this.eventDictionary[name]) return; this.eventDictionary[name] = (this.eventDictionary[name] as Handler[]) - .filter((lis): boolean => { - const handled = lis.handlerCheck(value, events); + .filter((h): boolean => { + const handled = h.handlerCheck(value, events); /* * Removes handler if it has not been triggered yet. Will also clearTimeout * the expiry if the handler has one so that the timeout will not trigger afterwards. */ if (handled) { - if (lis.expiry) clearTimeout(lis.expiry); + if (h.expiry) clearTimeout(h.expiry); } return !handled; }); @@ -139,6 +139,17 @@ export default class UpdateHandler { // Removes the handler from the event's list of handlers. if (!this.eventDictionary[name]) return; this.eventDictionary[name] = (this.eventDictionary[name] as Handler[]) - .filter((lis): boolean => lis !== handler); + .filter((h): boolean => h !== handler); + } + + /** + * Removes all handlers in the update handler. + */ + public clearHandlers(): void { + Object.keys(this.eventDictionary).forEach((event): void => { + (this.eventDictionary[event] as Handler[]).forEach((handler): void => { + handler.removeHandler(); + }); + }); } } diff --git a/src/static/config.json b/src/static/config.json index 41f3b256..c1fb6bfd 100644 --- a/src/static/config.json +++ b/src/static/config.json @@ -3,5 +3,6 @@ "geolocation": true, "vehicleDisconnectionTime": 20, - "messageSendRate": 1 + "messageSendRate": 10, + "vehicleAllowedErrorTime": 5 } diff --git a/src/types/messages.ts b/src/types/messages.ts index 78f23e38..0ab817eb 100644 --- a/src/types/messages.ts +++ b/src/types/messages.ts @@ -373,9 +373,12 @@ export interface UGVRetreiveMissionParameters { // UUVRetrieveMission does not need parameters, as its one task does not need any. +export type MissionParameters = ISRSearchMissionParameters | VTOLSearchMissionParameters | PayloadDropTaskParameters +| UGVRetreiveMissionParameters | {}; + /** * Options for the mission. All variables are optional so the UI should - * be able to detech which of the variables are required or not. + * be able to detect which of the variables are required or not. */ export interface MissionOptions { isrSearch?: { @@ -406,13 +409,15 @@ export interface MissionOptions { /** * Type of information that should be passed from mission window to the Orchestrator. */ -export interface MissionParameters { - info: ISRSearchMissionParameters - | VTOLSearchMissionParameters - | PayloadDropTaskParameters - | UGVRetreiveMissionParameters - | {}; // The {} stands for UUVRetrieveMissionParameters, an empty object. +export interface MissionInformation { + /** + * Parameters for the mission. + */ + parameters: MissionParameters; + /** + * Options for the mission. + */ options: MissionOptions; } From f00e8cb40916904248005d312fb53c2c929ee553 Mon Sep 17 00:00:00 2001 From: Paul Matthew Barrameda Date: Sat, 13 Apr 2019 17:34:38 -0700 Subject: [PATCH 077/152] Added more logic to Mission Went on with Max's definition of handleUnresponsiveVehicle. Added support to add tasks to a job by a certain comparison criteria (allows for obtaining the next task in a different way). Ensured that activeVehicleMapping ONLY has the vehicles that are doing a job that relates to the mission. Any other vehicles are completely ignored for the mission (this is in the case the user interface sends extra jobs that are irrelevant to the mission over to the constructor). Doing this allows us to not have to check all the time if the job in the activeVehicleMapping is a related jobType or not. It simply is. Added support to keep track of vehicles of a mission by having a callback on the update messages that are sent from the vehicles. --- src/common/struct/DictionaryList.ts | 54 +++++ src/common/struct/Mission.ts | 326 +++++++++++++++++++--------- src/util/util.ts | 27 +++ 3 files changed, 303 insertions(+), 104 deletions(-) diff --git a/src/common/struct/DictionaryList.ts b/src/common/struct/DictionaryList.ts index 6a2d0f40..b0ad1956 100644 --- a/src/common/struct/DictionaryList.ts +++ b/src/common/struct/DictionaryList.ts @@ -1,4 +1,5 @@ export type Callback = (value: T, index: number, array: T[]) => boolean; +export type CompareFunction = (a: T, b: T) => number; /** * Structure that can store lists in a dictionary. Lists can be obtained with a key string, @@ -38,6 +39,25 @@ export default class DictionaryList { return this.numItems; } + /** + * Appends new elements to the front of the array, and returns the new size of + * the dictioanry. + * + * @param key The key to access the list in the dictionary. + */ + public unshift(key: string, ...values: T[]): number { + if (!this.dictionary[key]) { + this.dictionary[key] = []; + } + + const list = this.dictionary[key] as T[]; + + this.numItems += values.length; + list.unshift(...values); + + return this.numItems; + } + /** * Gets the list stored at the dictionary with the given key. * @@ -54,7 +74,10 @@ export default class DictionaryList { * @param list The list to change the dictionary's list to. */ public set(key: string, list: T[]): void { + const currentLength = this.dictionary[key] ? (this.dictionary[key] as T[]).length : 0; this.dictionary[key] = list; + + this.numItems += list.length - currentLength; } /** @@ -107,6 +130,24 @@ export default class DictionaryList { return list.splice(index, 1)[0]; } + /** + * Inserts elements to dictionary's list with the given key at an index. If the index + * is larger than the list's size, the element will be added to the back of the list. + * + * @param key The key to access the list in the dictionary + * @param index The index to insert the element at. + * @param values The value to add to the list. + */ + public insert(key: string, index: number, ...values: T[]): number { + const list = this.dictionary[key]; + if (!list || index > list.length) return this.push(key, ...values); + + this.numItems += values.length; + list.splice(index, 0, ...values); + + return this.numItems; + } + /** * Removes all elements of an array that meets the condition specified in a callback function. * @@ -147,6 +188,19 @@ export default class DictionaryList { return list.filter(callback); } + /** + * Sorts a list for a given key. + * + * @param key The key to access the list in the dictionary. + * @param compareFunction The function to compare the elements in the array. + */ + public sort(key: string, compareFunction: CompareFunction): T[] { + const list = this.dictionary[key]; + if (!list) return []; + + return list.sort(compareFunction); + } + /** * Gets all keys of the object whose list value has an element or more. */ diff --git a/src/common/struct/Mission.ts b/src/common/struct/Mission.ts index 546a9374..e1af5f20 100644 --- a/src/common/struct/Mission.ts +++ b/src/common/struct/Mission.ts @@ -1,7 +1,6 @@ import { ipcRenderer } from 'electron'; import { - config, JobType, vehicleConfig, VehicleInfo, @@ -16,17 +15,14 @@ import { Task, UpdateMessage, } from '../../types/messages'; -import { VehicleStatus } from '../../types/types'; + +import { searchIndex } from '../../util/util'; import DictionaryList from './DictionaryList'; import Vehicle from './Vehicle'; import UpdateHandler from './UpdateHandler'; -interface VehicleSignature { [vehicleId: string]: Vehicle } - -interface VehicleMappingSignature { [vehicleId: string]: JobType } - -type MissionStatus = 'ready' | 'initializing' | 'waiting' | 'running' | 'error'; +type MissionStatus = 'ready' | 'initializing' | 'waiting' | 'running'; /** * Mission backend for the GCS. Automatically creates and assigns tasks. No provided value should @@ -46,7 +42,7 @@ export default abstract class Mission { /** * All connected vehicles (at least when this class was created). */ - private vehicles: VehicleSignature; + private vehicles: { [vehicleId: string]: Vehicle }; /** * Current status of mission. @@ -72,14 +68,22 @@ export default abstract class Mission { * assigning jobs to vehicles. When mission starts, this gets reassigned to * waitingVehicles * - * The values put together as an array must include all of the mission's job types. + * The values put together as an array must equal all of the mission's job types. + * There will be no job types in this mapping that are irrelevant to the mission. + */ + private activeVehicleMapping: { [vehicleId: string]: JobType } = {}; + + /** + * Comparison to how tasks are added to the waiting task list. If there is no + * comparison function specified for the jobType, then jobs will be added and taken + * on a FILO order (queue). */ - private vehicleMapping: VehicleMappingSignature; + protected abstract addTaskCompare: { [jobType: string]: (a: Task, b: Task) => number }; /** * Map of the id of the vehicle to the task it is performing. */ - private activeTasks = new Map(); + private activeTasks = new Map(); /** * All tasks waiting to be executed, mapped by their job type. @@ -107,15 +111,30 @@ export default abstract class Mission { public constructor( completionCallback: (information: MissionInformation) => void, - vehicles: VehicleSignature, + vehicles: { [vehicleId: string]: Vehicle }, information: MissionInformation, - vehicleMapping: VehicleMappingSignature, + activeVehicleMapping: { [vehicleId: string]: JobType }, ) { this.completionCallback = completionCallback; this.vehicles = vehicles; this.parameters = information.parameters; this.options = information.options; - this.vehicleMapping = vehicleMapping; + + /* + * TODO: Allow the user to select more than one vehicle for a job. The vehicles can be used + * as backup in the case that the first vehicle goes on an error state. + */ + + /* + * Filters out any irrelevant jobs when adding to activeVehicleMapping in the case that + * extra jobs were included in the mapping. + */ + Object.keys(activeVehicleMapping).forEach((vehicleId): void => { + const jobType = activeVehicleMapping[vehicleId]; + if (this.jobTypes.has(jobType)) { + this.activeVehicleMapping[vehicleId] = jobType; + } + }); /* * Create event handler that will handle all changes of status: @@ -151,16 +170,15 @@ export default abstract class Mission { type: 'failure', message: `Something wrong happened in ${this.missionName}`, }); - this.stop(); - this.statusEventHandler.event('status', 'ready'); + this.stop(false); return; } - // Fails to initialize if provided job types do not match up to this mission's job types + // Fails to initialize if provided job types do not match up to this mission's job types. if (!this.checkJobTypesAndActiveVehicles()) { ipcRenderer.send('post', 'updateMessages', { type: 'failure', - message: `Provided jobs and/or assigned vehicles for ${this.missionName} is incorrect. Stopping mission`, + message: `Provided vehicles for ${this.missionName} is incomplete. Stopping mission`, }); this.statusEventHandler.event('status', 'ready'); return; @@ -183,16 +201,15 @@ export default abstract class Mission { type: 'failure', message: `Something wrong happened in ${this.missionName}`, }); - this.stop(); - this.statusEventHandler.event('status', 'ready'); + this.stop(false); return; } - let pendingVehicleIds = Object.keys(this.vehicleMapping); + let pendingVehicleIds = Object.keys(this.activeVehicleMapping); const assignedVehicleIds: string[] = []; /* - * The three functions below are callbacks to when the vehicles connect. + * The three functions below are callbacks to when we assign the vehicle its job. * I put them here for readability. */ @@ -220,7 +237,7 @@ export default abstract class Mission { }; /** - * Triggers when vehicle has disconnected. + * Triggers when vehicle fails to accept the job assignment. */ const onDisconnect = (): void => { /* @@ -228,11 +245,11 @@ export default abstract class Mission { * to any other vehicle if there is somehow a "start" message on queue. */ ipcRenderer.send('post', 'stopSendingMessages'); - this.stop(); + this.stop(false); }; /** - * Triggers when vehicle enters an error state. + * Triggers when vehicle performing job enters an error state. */ const onError = (vehicleId: string, message?: string): void => { ipcRenderer.send('post', 'updateMessages', { @@ -243,16 +260,14 @@ export default abstract class Mission { this.handleUnresponsiveVehicle(vehicleId); }; - - // Assign jobs to vehicles (only if that vehicle's job is relatable to this mission). + // Assign jobs to vehicles. pendingVehicleIds.forEach((vehicleId): void => { - const jobType = this.vehicleMapping[vehicleId]; - if (this.jobTypes.has(jobType)) { - this.vehicles[vehicleId].assignJob(jobType, - (): void => onSuccess(vehicleId), - (): void => onDisconnect(), - (message): void => onError(vehicleId, message)); - } + const jobType = this.activeVehicleMapping[vehicleId]; + + this.vehicles[vehicleId].assignJob(jobType, + (): void => onSuccess(vehicleId), + (): void => onDisconnect(), + (message): void => onError(vehicleId, message)); }); } @@ -266,22 +281,39 @@ export default abstract class Mission { type: 'failure', message: `Something wrong happened in ${this.missionName}`, }); - this.stop(); - this.statusEventHandler.event('status', 'ready'); + this.stop(false); return; } - // Adds all the tasks to the waitingTasks, if the jobType relates to this mission. + // Adds all the tasks to the waitingTasks, if the task's jobType relates to this mission. const jobTasks = this.generateTasks(); Object.keys(jobTasks).forEach((jobType): void => { if (vehicleConfig.isJobType(jobType) && this.jobTypes.has(jobType as JobType)) { - this.waitingTasks.push(jobType, ...jobTasks.get(jobType) as Task[]); + if (this.addTaskCompare[jobType]) { + // Will happen if there is a compare for this job type. + (jobTasks.get(jobType) as Task[]).forEach((task): void => { + const index = searchIndex( + this.waitingTasks.get(jobType) || [], + task, + this.addTaskCompare[jobType], + ); + this.waitingTasks.insert(jobType, index, task); + }); + } else { + this.waitingTasks.push(jobType, ...jobTasks.get(jobType) as Task[]); + } } }); - // Remaps the vehicleMapping to waitingVehicles (vehicleId => jobType to jobType => Vehicle[]). - Object.keys(this.vehicleMapping).forEach((vehicleId): void => { - const jobType = this.vehicleMapping[vehicleId]; + /* + * Remaps the activeVehicleMapping to waitingVehicles + * (vehicleId => jobType to jobType => Vehicle[]). + * + * Using waitingVehicles to assign vehicles tasks is faster than using activeVehicleMapping, + * hence we need waitingVehicles. It is also more easy to understand. + */ + Object.keys(this.activeVehicleMapping).forEach((vehicleId): void => { + const jobType = this.activeVehicleMapping[vehicleId]; const vehicle = this.vehicles[vehicleId]; this.waitingVehicles.push(jobType, vehicle); @@ -289,7 +321,11 @@ export default abstract class Mission { /* * Assign tasks to vehicles. Will keep assigning tasks to vehicles until there is either - * waiting vehicles or waiting tasks. + * no more available waiting vehicles or no more available waiting tasks. + * + * We add these tasks to activeTasks, where we keep track of all tasks that are being executed. + * The mission is done when there are no more tasks inside activeTasks, as well as no more + * tasks in waitingTasks. */ this.waitingVehicles.keys().forEach((jobType): void => { while ( @@ -300,7 +336,7 @@ export default abstract class Mission { const vehicle = this.waitingVehicles.shift(jobType) as Vehicle; vehicle.assignTask(task); - this.activeTasks.set(vehicle.getVehicleId(), task); + this.activeTasks.set(`${vehicle.getVehicleId()}`, task); } }); @@ -313,35 +349,33 @@ export default abstract class Mission { * vehicleId being provided is a valid vehicleId of a vehicle that was connected when the * mission was initialized. * - * This is the event when a complete message has been sent from a vehicle. The mission - * will either give that vehicle the next task or complete the mission. - * * Subclasses can override this to check for even more types of messages. */ public update(message: JSONMessage): void { /* - * We run this statement in the case a vehicle goes to error, we keep track of its update - * messages and see if it goes back to "running". Otherwise we will have to stop the - * mission. See the handler that is created in the handleUnresponsiveVehicle() function. + * The base mission class only checks for update and completion messages. The base class will + * either reassign the vehicle the task, put the vehicle to wait for further tasks, + * or complete the mission when it receives a completion message. The update message + * is to simply keep its track of vehicles up to date. */ - if (this.status === 'error' && messageTypeGuard.isUpdateMessage(message)) { - this.statusEventHandler.event('vehicleStatus', (message as UpdateMessage).status); + + if (messageTypeGuard.isUpdateMessage(message)) { + this.vehicles[message.sid].update(message as UpdateMessage); } - // The base mission class only checks for completion messages. if (messageTypeGuard.isCompleteMessage(message)) { - const jobType = this.vehicleMapping[message.sid]; + const jobType = this.activeVehicleMapping[message.sid]; if ((this.waitingTasks.get(jobType) as Task[]).length > 0) { // Assigns the next task in that job to the vehicle. const newTask = this.waitingTasks.shift(jobType) as Task; this.vehicles[message.sid].assignTask(newTask); - this.activeTasks.set(message.sid, newTask); + this.activeTasks.set(`${message.sid}`, newTask); } else { // All tasks in that jobType are complete, so this vehicle will go back to waiting vehicles. this.waitingVehicles.push(jobType, this.vehicles[message.sid]); - this.activeTasks.delete(message.sid); + this.activeTasks.delete(`${message.sid}`); } /* @@ -353,59 +387,90 @@ export default abstract class Mission { if (this.activeTasks.size === 0 && this.waitingTasks.size() === 0) { ipcRenderer.send('post', 'completeMission', this.missionName); this.completionCallback(this.generateTerminatedData()); - this.stop(); - this.statusEventHandler.event('status', 'ready'); + this.stop(true); } else { // This should never happen. There is a big issue in the system if this somehow happens. ipcRenderer.send('post', 'updateMessages', { type: 'failure', message: `Mission completion failed in ${this.missionName}. Stopping mission`, }); - this.stop(); - this.statusEventHandler.event('status', 'ready'); + this.stop(false); } } } /** - * Adds a new task to the mission. Should only be called on its subclasses (probably through - * an overriden update() function). + * Adds a new task to the mission. Should only be called on its subclasses to add a task. + * For example, add tasks as certain information is found while the mission happens, or + * when user needs a vehicle to manually perform a task before another. + * + * @param jobType The job to add the task to. + * @param task The task object itself. + * @param addToFront True if the task should be added to the front of the queue + * instead of the back. */ - protected addTask(jobType: string, task: Task): void { + protected addTask(jobType: string, task: Task, addToFront?: boolean): void { // Fails to initialize if mission is not in the "ready" state. if (this.status !== 'running') { ipcRenderer.send('post', 'updateMessages', { type: 'failure', message: `Tried to add ${task.taskType} task while ${this.missionName} is not running`, }); - this.stop(); - this.statusEventHandler.event('status', 'ready'); + this.stop(false); return; } - /* - * Assign the task right away if there is a vehicle waiting for a task for that - * specific jobType. Otherwise just store it in waiting tasks so that when a vehicle - * is available, it can perform that task. - */ if (this.waitingVehicles.get(jobType) && (this.waitingVehicles.get(jobType) as Vehicle[]).length > 0 ) { + /* + * Assign the task right away if there is a vehicle waiting for a task for that + * specific jobType. + */ const vehicle = this.waitingVehicles.shift(jobType) as Vehicle; vehicle.assignTask(task); - this.activeTasks.set(vehicle.getVehicleId(), task); + this.activeTasks.set(`${vehicle.getVehicleId()}`, task); + } else if (addToFront) { + // Add to front of waitingTasks if requested. + this.waitingTasks.unshift(jobType, task); + } else if (this.addTaskCompare[jobType] && this.waitingTasks.get(jobType)) { + /* + * Add to proper spot in task (these tasks in this job are ordered) if there + * is a compare function. + */ + const index = searchIndex( + this.waitingTasks.get(jobType) as Task[], + task, + this.addTaskCompare[jobType], + ); + this.waitingTasks.insert(jobType, index, task); } else { + // Simply add the task to the back of the list. this.waitingTasks.push(jobType, task); } } /** * Stops the mission (either means mission was stopped manually or mission was completed). + * Will print out terminated data (this mission's parameters) if the mission was + * stopped through an error (or manually stopped). + * + * @param success True if the mission was completed, false if the mission was stopped + * manually/through error. */ - private stop(): void { - Object.keys(this.vehicleMapping).forEach((vehicleId): void => { + private stop(success: boolean): void { + Object.keys(this.activeVehicleMapping).forEach((vehicleId): void => { this.vehicles[vehicleId].stop(); }); + + this.statusEventHandler.event('status', 'ready'); + + if (!success) { + ipcRenderer.send('post', 'updateMessage', { + type: 'success', + message: `Terminated data: ${JSON.stringify(this.parameters)}`, + }); + } /* * TODO: Print out the output data for this mission, so that we do not have to start over from * step 1. @@ -417,27 +482,26 @@ export default abstract class Mission { * 1. All job types provided cover the required job types for this mission. * 2. There is a vehicle assigned to each job type. * - * @param vehicleMapping User provided map of vehicle to their job type, function will check + * @param activeVehicleMapping User provided map of vehicle to their job type, function will check * if this mapping is valid. */ private checkJobTypesAndActiveVehicles(): boolean { - const vehicleIds = Object.keys(this.vehicleMapping); + const vehicleIds = Object.keys(this.activeVehicleMapping); - // Create a set of the job types provided by vehicleMapping - const activeJobTypes = new Set(); - vehicleIds.forEach((vehicleId): void => { - activeJobTypes.add(this.vehicleMapping[vehicleId]); - }); - - // Check #1: ensure the jobs in the vehicleMapping cover the required jobs for this mission. - if (!Array.from(this.jobTypes).every((jobType): boolean => activeJobTypes.has(jobType))) { + /* + * Check #1: ensure the jobs in the activeVehicleMapping cover the required jobs for this + * mission. + */ + if (!Array.from(Object.keys(this.activeVehicleMapping)).every( + (vehicleId): boolean => this.jobTypes.has(this.activeVehicleMapping[vehicleId]), + )) { return false; } /* - * Check #2: if all jobs in the vehicleMapping cover the required jobs for the mission, then - * there is a vehicle mapped for every job already. We need to check that all these vehicles - * are in the "ready" state too. + * Check #2: if all jobs in the activeVehicleMapping cover the required jobs for the mission, + * then there is a vehicle mapped for every job already. We need to check that all these + * vehicles are in the "ready" state too. */ if (!vehicleIds.every((vehicleId): boolean => this.vehicles[vehicleId].getStatus() === 'ready')) { return false; @@ -447,29 +511,83 @@ export default abstract class Mission { } /** - * Handles a vehicle in an error state. We will give the vehicle a few seconds to get - * out of error state before ending the mission and log that user should take manual - * control of the vehicle as soon as possible. + * Handles a vehicle that is assigned a job when it goes to an error state. Tries to assign + * another vehicle that can perform the same task and remove that vehicle. If not possible, + * simply stops the mission. + * + * If no tasks is being executed, stop the mission. + * + * This never gets executed while mission's status is "ready", since no vehicles are assigned + * then. */ private handleUnresponsiveVehicle(vehicleId: string): void { - ipcRenderer.send('post', 'updateMessages', { - type: 'progress', - message: `Will see if the vehicle will exit the error state within ${config.vehicleAllowedErrorTime} seconds`, - }); - - this.status = 'error'; + /* + * In the case that a vehicle that is not performing a job is in an error state + * and somehow this function gets called (which it shouldn't), let the vehicle + * be. + * + * Again, this should never be called for a vehicle that is not performing a job. + * All vehicles in the "ready" state are assumed to be stable (whether they're airborne, + * underwater, or on land). + */ + if (!this.activeVehicleMapping[vehicleId]) return; - this.statusEventHandler.addHandler('vehicleStatus', - (status: VehicleStatus): boolean => status !== 'error', - { - callback: (): void => { - ipcRenderer.send('post', 'updateMessages', { - type: 'progress', - message: `${(vehicleConfig.vehicleInfos[vehicleId] as VehicleInfo).name} failed to exit the error state on time. Stopping mission. User, take manual control of vehicle as soon as possible`, - }); - }, - time: config.vehicleDisconnectionTime * 1000, + if (this.status === 'waiting') { + // Simply exit the mission if we are in the "waiting" stage. + ipcRenderer.send('post', 'updateMessages', { + type: 'failure', + message: 'Exiting mission due to vehicle in error state, take manual control over vehicle', + }); + this.stop(false); + } else if (this.status === 'initializing') { + /* + * Stop sending all extra start messages and exit the mission. + * See assignJobs for similar logic. + */ + ipcRenderer.send('post', 'updateMessages', { + type: 'failure', + message: 'Exiting mission due to vehicle in error state, take manual control over vehicle', }); + ipcRenderer.send('post', 'stopSendingMessages'); + this.stop(false); + } else { + const jobType = this.activeVehicleMapping[vehicleId]; + + // Delete the error vehicle from vehicle mapping. + delete this.activeVehicleMapping[vehicleId]; + + /* + * Delete the task related to error vehicle and insert it to front of waitingTasks. + * It is possible that there is no tasks related to the vehicle and that the + * vehicle has errored out while waiting to be assigned a task (vehicle status was + * "waiting"). + */ + if (this.activeTasks.has(vehicleId)) { + const task = this.activeTasks.get(vehicleId) as Task; + this.activeTasks.delete(vehicleId); + + this.addTask(jobType, task, true); + } + + /* + * Try to reassign that task (if there was one) to a waiting vehicle that can + * perform that job. + */ + const newVehicle = this.waitingVehicles.shift(jobType); + const task = this.waitingTasks.shift(jobType); + if (newVehicle && task) { + this.activeVehicleMapping[`${newVehicle.getVehicleId()}`] = jobType; + + newVehicle.assignTask(task); + this.activeTasks.set(`${newVehicle.getVehicleId()}`, task); + } else { + ipcRenderer.send('post', 'updateMessages', { + type: 'failure', + message: 'Exiting mission due to vehicle in error state, take manual control over vehicle', + }); + this.stop(false); + } + } } /** diff --git a/src/util/util.ts b/src/util/util.ts index 7c79f0df..e235c69d 100644 --- a/src/util/util.ts +++ b/src/util/util.ts @@ -37,3 +37,30 @@ export default { isJSON, updateVehicles, }; + +/** + * Performs binary search for an element. Will return the index in the array + * the compareFunction goes through (can be the index of the value). + */ +export function searchIndex( + array: T[], + value: T, + compareFunction: (a: T, b: T) => number, +): number { + let first = 0; + let last = array.length - 1; + + while (first <= last) { + const middle = Math.floor((first + last) / 2); + const comparison = compareFunction(value, array[middle]); + + if (comparison > 0) { + first = middle + 1; + } else if (comparison < 0) { + last = middle - 1; + } else { + return middle; + } + } + return -(first + 1); +} From 94a8aaee653eea31f7516db9019ba86190e81f68 Mon Sep 17 00:00:00 2001 From: Paul Matthew Barrameda Date: Sat, 13 Apr 2019 22:34:38 -0700 Subject: [PATCH 078/152] Small changes, no change in logic Will read through the code one more time before starting to implement other missions + Orchestrator --- src/common/struct/Mission.ts | 15 +++++---------- src/types/messages.ts | 4 ++-- 2 files changed, 7 insertions(+), 12 deletions(-) diff --git a/src/common/struct/Mission.ts b/src/common/struct/Mission.ts index e1af5f20..d98f03cc 100644 --- a/src/common/struct/Mission.ts +++ b/src/common/struct/Mission.ts @@ -122,7 +122,8 @@ export default abstract class Mission { /* * TODO: Allow the user to select more than one vehicle for a job. The vehicles can be used - * as backup in the case that the first vehicle goes on an error state. + * as backup in the case that the first vehicle goes on an error state. However this data + * should not be activeVehicleMapping. activeVehicleMapping will always be vehicleId => jobType */ /* @@ -327,7 +328,7 @@ export default abstract class Mission { * The mission is done when there are no more tasks inside activeTasks, as well as no more * tasks in waitingTasks. */ - this.waitingVehicles.keys().forEach((jobType): void => { + Object.values(this.activeVehicleMapping).forEach((jobType): void => { while ( (this.waitingTasks.get(jobType) as Task[]).length > 0 && (this.waitingVehicles.get(jobType) as Vehicle[]).length > 0 @@ -420,14 +421,12 @@ export default abstract class Mission { return; } - if (this.waitingVehicles.get(jobType) - && (this.waitingVehicles.get(jobType) as Vehicle[]).length > 0 - ) { + const vehicle = this.waitingVehicles.shift(jobType); + if (vehicle) { /* * Assign the task right away if there is a vehicle waiting for a task for that * specific jobType. */ - const vehicle = this.waitingVehicles.shift(jobType) as Vehicle; vehicle.assignTask(task); this.activeTasks.set(`${vehicle.getVehicleId()}`, task); } else if (addToFront) { @@ -471,10 +470,6 @@ export default abstract class Mission { message: `Terminated data: ${JSON.stringify(this.parameters)}`, }); } - /* - * TODO: Print out the output data for this mission, so that we do not have to start over from - * step 1. - */ } /** diff --git a/src/types/messages.ts b/src/types/messages.ts index 0ab817eb..b53fcaee 100644 --- a/src/types/messages.ts +++ b/src/types/messages.ts @@ -373,8 +373,8 @@ export interface UGVRetreiveMissionParameters { // UUVRetrieveMission does not need parameters, as its one task does not need any. -export type MissionParameters = ISRSearchMissionParameters | VTOLSearchMissionParameters | PayloadDropTaskParameters -| UGVRetreiveMissionParameters | {}; +export type MissionParameters = ISRSearchMissionParameters | VTOLSearchMissionParameters +| PayloadDropTaskParameters | UGVRetreiveMissionParameters | {}; /** * Options for the mission. All variables are optional so the UI should From bdbb4ee55d6c9ecaa89e9e78d6f0c6bd40f45f12 Mon Sep 17 00:00:00 2001 From: Paul Matthew Barrameda Date: Sun, 14 Apr 2019 03:31:15 -0700 Subject: [PATCH 079/152] Added ipcRenderer typing support through util/ipc.ts Small logic changes here and there too, nothing significant. Also converted all files to TypeScript/TypeScript React files. --- src/common/MessageHandler.ts | 74 +++-- src/common/Orchestrator.ts | 2 +- src/common/Xbee.ts | 15 +- src/common/struct/Mission.ts | 89 +++--- src/common/struct/Vehicle.ts | 13 +- src/main/index.ts | 66 ++-- src/renderer/App.tsx | 55 ++++ src/renderer/{index.css => app.css} | 2 +- src/renderer/fixtures/completeJobFixture.ts | 13 - src/renderer/fixtures/index.ts | 11 +- .../fixtures/updateMessagesFixtures.ts | 8 +- .../fixtures/updateVehiclesFixtures.ts | 6 +- src/renderer/index.js | 80 ----- src/renderer/index.ts | 36 +++ src/renderer/mainWindow/log/LogContainer.tsx | 48 +-- src/renderer/mainWindow/map/MapContainer.tsx | 4 +- .../map/control/GeolocationControl.tsx | 5 +- .../mainWindow/map/control/ThemeControl.tsx | 5 +- .../mainWindow/missionWindow/mission.css | 0 .../mainWindow/vehicle/VehicleTable.tsx | 5 +- src/renderer/missionWindow/MissionWindow.tsx | 18 +- .../missionParameterLayouts/ISRSearch.tsx | 9 - .../missionParameterLayouts/PayloadDrop.tsx | 9 - .../UGVRetrieveTarget.tsx | 9 - .../UUVRetrieveTarget.tsx | 9 - .../missionWindow/parameters/ISRSearch.tsx | 9 + .../missionWindow/parameters/PayloadDrop.tsx | 9 + .../parameters/UGVRetrieveTarget.tsx | 9 + .../parameters/UUVRetrieveTarget.tsx | 9 + .../VTOLSearch.tsx | 4 +- src/static/config.json | 5 +- src/static/index.ts | 18 +- src/types/messages.ts | 12 +- src/types/types.ts | 22 ++ src/util/ipc.ts | 295 ++++++++++++++++++ src/util/util.ts | 29 +- tsconfig.json | 2 +- 37 files changed, 669 insertions(+), 345 deletions(-) create mode 100644 src/renderer/App.tsx rename src/renderer/{index.css => app.css} (97%) delete mode 100644 src/renderer/fixtures/completeJobFixture.ts delete mode 100644 src/renderer/index.js create mode 100644 src/renderer/index.ts delete mode 100644 src/renderer/mainWindow/missionWindow/mission.css delete mode 100644 src/renderer/missionWindow/missionParameterLayouts/ISRSearch.tsx delete mode 100644 src/renderer/missionWindow/missionParameterLayouts/PayloadDrop.tsx delete mode 100644 src/renderer/missionWindow/missionParameterLayouts/UGVRetrieveTarget.tsx delete mode 100644 src/renderer/missionWindow/missionParameterLayouts/UUVRetrieveTarget.tsx create mode 100644 src/renderer/missionWindow/parameters/ISRSearch.tsx create mode 100644 src/renderer/missionWindow/parameters/PayloadDrop.tsx create mode 100644 src/renderer/missionWindow/parameters/UGVRetrieveTarget.tsx create mode 100644 src/renderer/missionWindow/parameters/UUVRetrieveTarget.tsx rename src/renderer/missionWindow/{missionParameterLayouts => parameters}/VTOLSearch.tsx (51%) create mode 100644 src/util/ipc.ts diff --git a/src/common/MessageHandler.ts b/src/common/MessageHandler.ts index 11bb36fb..c4c81ffa 100644 --- a/src/common/MessageHandler.ts +++ b/src/common/MessageHandler.ts @@ -1,14 +1,17 @@ -// Do not interact with this file outside the Orchestrator class. +// Do not import this file except in the Orchestrator class (common/Orchestrator). import { Event, ipcRenderer } from 'electron'; -import { config } from '../static/index'; +import { config, vehicleConfig } from '../static/index'; + import { AcknowledgementMessage, messageTypeGuard, JSONMessage, Message, } from '../types/messages'; + +import ipc from '../util/ipc'; import { isJSON } from '../util/util'; import DictionaryList from './struct/DictionaryList'; @@ -45,9 +48,7 @@ export default class MessageHandler { public constructor() { ipcRenderer.on('sendMessage', (_: Event, vehicleId: number, message: Message): void => this.sendMessage(vehicleId, message)); - ipcRenderer.on('receiveMessage', (_: Event, text: string): void => this.receiveMessage(text)); - ipcRenderer.on('stopSendingMessages', this.stopSendingMessages); } @@ -122,7 +123,7 @@ export default class MessageHandler { time: config.vehicleDisconnectionTime * 1000, callback: (): void => { this.removeMessage('outbox', jsonMessage.id); - ipcRenderer.send('post', 'deactivateVehicle', vehicleId); + ipc.postDeactivateVehicle(vehicleId); }, }); } @@ -149,7 +150,7 @@ export default class MessageHandler { private receiveMessage(text: string): void { // Filter out text that are not valid json. if (!isJSON(text)) { - ipcRenderer.send('post', 'updateMessages', { + ipc.postLogMessages({ message: `Received text from Xbee that is not a JSON, could not send bad message to sender ${text}`, }); return; @@ -158,17 +159,21 @@ export default class MessageHandler { // Filter out json that are not valid messages. const json = JSON.parse(text); if (!messageTypeGuard.isJSONMessage(json)) { - if (json.sid) { - this.sendBadMessage(json, 'Invalid message, does not meet requirements for a message'); + if (!Number.isNaN(json.sid) && vehicleConfig.isValidVehicleId(json.sid as number)) { + this.sendBadMessage(json as JSONMessage, 'Invalid message, does not meet requirements for a message'); } else { - ipcRenderer.send('post', 'updateMessages', { + ipc.postLogMessages({ message: `Received JSON from Xbee that is not a valid message, could not send bad message to sender: ${text}`, }); } + return; } const jsonMessage = json as JSONMessage; + // Ignore messages from unrecognized vehicles. + if (!vehicleConfig.isValidVehicleId(jsonMessage.sid)) return; + /* * Update the last message id received from the vehicle or discard message completely. * This is to be able to properly acknowledge a message. See the note on the following: @@ -189,51 +194,36 @@ export default class MessageHandler { return; } - /* - * Handles acknowledgement messages from any source (even vehicles not connected). - * DO NOT ACKNOWLEDGE. - * Stops sending the message that the ack message has ascknowledged. - */ - if (messageTypeGuard.isAcknowledgementMessage(jsonMessage)) { - const { ackid } = jsonMessage as AcknowledgementMessage; - - const hash = `${jsonMessage.sid}#${ackid}`; - this.updateHandler.event(hash, true); - return; - } - /* * All messages that get here will be forwarded to the Orchestrator and will - * be acknowledged there, because the MessageHandler does not know if the message - * comes from a connected vehicle or not. If the vehicle is not connected, the - * Orchestrator will simply ignore the message. + * be handled there (eg. acknowledgement, updating vehicle connection). */ + if (messageTypeGuard.isConnectMessage(jsonMessage)) { - ipcRenderer.send('post', 'handleConnectMessage', jsonMessage); + ipc.postHandleConnectMessage(jsonMessage); return; } if (messageTypeGuard.isCompleteMessage(jsonMessage)) { - ipcRenderer.send('post', 'handleCompleteMessage', jsonMessage); + ipc.postHandleCompleteMessage(jsonMessage); return; } if (messageTypeGuard.isPOIMessage(jsonMessage)) { - ipcRenderer.send('post', 'handlePOIMessage', jsonMessage); + ipc.postHandlePOIMessage(jsonMessage); return; } if (messageTypeGuard.isUpdateMessage(jsonMessage)) { - ipcRenderer.send('post', 'handleUpdateMessage', jsonMessage); + ipc.postHandleUpdateMessage(jsonMessage); return; } - // Exception is this message. DO NOT ACKNOWLEDGE. - if (messageTypeGuard.isBadMessageMessage(jsonMessage)) { - ipcRenderer.send('post', 'handleBadMessage', jsonMessage); - + // DO NOT ACKNOWLEDGE. + if (messageTypeGuard.isBadMessage(jsonMessage)) { + ipc.postHandleBadMessage(jsonMessage); /* - * ipcRenderer.send('post', 'updateMessages', { + * ipc.postLogMessages({ * type: 'failure', * message: `Received bad message from ${name}: ${error || 'No error message * was specified'}`, @@ -241,6 +231,22 @@ export default class MessageHandler { */ return; } + + /* + * Stops sending the message that the ack message has acknowledged. + * + * DO NOT ACKNOWLEDGE. + */ + if (messageTypeGuard.isAcknowledgementMessage(jsonMessage)) { + const { ackid } = jsonMessage as AcknowledgementMessage; + + const hash = `${jsonMessage.sid}#${ackid}`; + this.updateHandler.event(hash, true); + + ipc.postHandleAcknowledgementMessage(jsonMessage); + return; + } + /* * Any message that reaches here is a message (has a valid type, id, sid, tid, and time) * but does not have valid properties, or is a message GCS should not receive from vehicles diff --git a/src/common/Orchestrator.ts b/src/common/Orchestrator.ts index 3d89ce39..931fbe4f 100644 --- a/src/common/Orchestrator.ts +++ b/src/common/Orchestrator.ts @@ -1,3 +1,3 @@ -// Do not import this class outside src/renderer/index +// Do not import this class except in the index renderer file (renderer/index). import './MessageHandler'; diff --git a/src/common/Xbee.ts b/src/common/Xbee.ts index 74b70a3e..46d65461 100644 --- a/src/common/Xbee.ts +++ b/src/common/Xbee.ts @@ -1,6 +1,5 @@ // Do not interact with this file outside the MissionHandler class. -import { ipcRenderer } from 'electron'; import SerialPort from 'serialport'; import { constants as C, Frame, XBeeAPI } from 'xbee-api'; @@ -8,6 +7,8 @@ import { VehicleInfo, vehicleConfig } from '../static/index'; import { JSONMessage } from '../types/messages'; +import ipc from '../util/ipc'; + /* * TODO: Add a feature with Vehicle container to change this dynamically and reconnect. * With that feature, no need to have this at all in config. @@ -16,7 +17,7 @@ const port: string | undefined = 'COM5'; const serialport = new SerialPort(port, { baudRate: 57600 }, (err): void => { if (err) { - ipcRenderer.send('post', 'updateMessages', { + ipc.postLogMessages({ type: 'failure', message: `Failed to initialize Xbee: ${err.message}`, }); @@ -42,7 +43,7 @@ function sendMessage(message: JSONMessage): void { */ const vehicleInfoObject = vehicleConfig.vehicleInfos[message.tid]; if (!vehicleInfoObject) { - ipcRenderer.send('post', 'updateMessages', { + ipc.postLogMessages({ type: 'failure', message: `Failed to send message to vehicle with id ${message.tid}`, }); @@ -74,21 +75,21 @@ serialport.pipe(xbeeAPI.parser); xbeeAPI.builder.pipe(serialport as NodeJS.WritableStream); serialport.on('open', (): void => { - ipcRenderer.send('post', 'updateMessages', { + ipc.postLogMessages({ type: 'success', message: 'Xbee connection has opened', }); }); serialport.on('error', (error: Error): void => { - ipcRenderer.send('post', 'updateMessages', { + ipc.postLogMessages({ type: 'failure', message: `An error has occured with the Xbee connection: ${error.message}`, }); }); serialport.on('close', (): void => { - ipcRenderer.send('post', 'updateMessages', { + ipc.postLogMessages({ type: 'failure', message: 'Xbee connection has closed', }); @@ -98,7 +99,7 @@ xbeeAPI.parser.on('data', (frame: Frame): void => { if (frame.type !== C.FRAME_TYPE.ZIGBEE_RECEIVE_PACKET || !frame.data) return; // Sends notification to process the message (either a valid JSON string or not). - ipcRenderer.send('post', 'receiveMessage', frame.data.toString()); + ipc.postReceiveMessage(frame.data.toString()); }); export default { diff --git a/src/common/struct/Mission.ts b/src/common/struct/Mission.ts index d98f03cc..b9486d6a 100644 --- a/src/common/struct/Mission.ts +++ b/src/common/struct/Mission.ts @@ -1,5 +1,3 @@ -import { ipcRenderer } from 'electron'; - import { JobType, vehicleConfig, @@ -16,6 +14,7 @@ import { UpdateMessage, } from '../../types/messages'; +import ipc from '../../util/ipc'; import { searchIndex } from '../../util/util'; import DictionaryList from './DictionaryList'; @@ -42,7 +41,7 @@ export default abstract class Mission { /** * All connected vehicles (at least when this class was created). */ - private vehicles: { [vehicleId: string]: Vehicle }; + private vehicles: { [vehicleId: number]: Vehicle }; /** * Current status of mission. @@ -71,7 +70,7 @@ export default abstract class Mission { * The values put together as an array must equal all of the mission's job types. * There will be no job types in this mapping that are irrelevant to the mission. */ - private activeVehicleMapping: { [vehicleId: string]: JobType } = {}; + private activeVehicleMapping: { [vehicleId: number]: JobType } = {}; /** * Comparison to how tasks are added to the waiting task list. If there is no @@ -83,7 +82,7 @@ export default abstract class Mission { /** * Map of the id of the vehicle to the task it is performing. */ - private activeTasks = new Map(); + private activeTasks = new Map(); /** * All tasks waiting to be executed, mapped by their job type. @@ -111,9 +110,9 @@ export default abstract class Mission { public constructor( completionCallback: (information: MissionInformation) => void, - vehicles: { [vehicleId: string]: Vehicle }, + vehicles: { [vehicleId: number]: Vehicle }, information: MissionInformation, - activeVehicleMapping: { [vehicleId: string]: JobType }, + activeVehicleMapping: { [vehicleId: number]: JobType }, ) { this.completionCallback = completionCallback; this.vehicles = vehicles; @@ -130,7 +129,8 @@ export default abstract class Mission { * Filters out any irrelevant jobs when adding to activeVehicleMapping in the case that * extra jobs were included in the mapping. */ - Object.keys(activeVehicleMapping).forEach((vehicleId): void => { + Object.keys(activeVehicleMapping).forEach((vehicleIdString): void => { + const vehicleId = parseInt(vehicleIdString, 10); const jobType = activeVehicleMapping[vehicleId]; if (this.jobTypes.has(jobType)) { this.activeVehicleMapping[vehicleId] = jobType; @@ -167,7 +167,7 @@ export default abstract class Mission { private initialize(): void { // Stops mission if mission is not in the "ready" state. if (this.status !== 'ready') { - ipcRenderer.send('post', 'updateMessages', { + ipc.postLogMessages({ type: 'failure', message: `Something wrong happened in ${this.missionName}`, }); @@ -177,7 +177,7 @@ export default abstract class Mission { // Fails to initialize if provided job types do not match up to this mission's job types. if (!this.checkJobTypesAndActiveVehicles()) { - ipcRenderer.send('post', 'updateMessages', { + ipc.postLogMessages({ type: 'failure', message: `Provided vehicles for ${this.missionName} is incomplete. Stopping mission`, }); @@ -198,7 +198,7 @@ export default abstract class Mission { private assignJobs(): void { // Fails to assign jobs if mission is not in the "initializing" state. if (this.status !== 'initializing') { - ipcRenderer.send('post', 'updateMessages', { + ipc.postLogMessages({ type: 'failure', message: `Something wrong happened in ${this.missionName}`, }); @@ -206,8 +206,12 @@ export default abstract class Mission { return; } - let pendingVehicleIds = Object.keys(this.activeVehicleMapping); - const assignedVehicleIds: string[] = []; + let pendingVehicleIds: number[] = []; + const assignedVehicleIds: number[] = []; + Object.keys(this.activeVehicleMapping).forEach((vehicleIdString): void => { + const vehicleId = parseInt(vehicleIdString, 10); + pendingVehicleIds.push(vehicleId); + }); /* * The three functions below are callbacks to when we assign the vehicle its job. @@ -217,7 +221,7 @@ export default abstract class Mission { /** * Triggers when vehicle successfully receives the job assignment. */ - const onSuccess = (vehicleId: string): void => { + const onSuccess = (vehicleId: number): void => { // Remove pendingId when vehicle is assigned the job successfully. pendingVehicleIds = pendingVehicleIds.filter((v): boolean => v !== vehicleId); assignedVehicleIds.push(vehicleId); @@ -228,7 +232,7 @@ export default abstract class Mission { * This will trigger this.start(). */ if (pendingVehicleIds.length === 0) { - ipcRenderer.send('post', 'updateMessages', { + ipc.postLogMessages({ type: 'success', message: `Assigned jobs to all vehicles for ${this.missionName}`, }); @@ -245,15 +249,15 @@ export default abstract class Mission { * Stop mission if any vehicle failed to get the job. Also stops sending messages * to any other vehicle if there is somehow a "start" message on queue. */ - ipcRenderer.send('post', 'stopSendingMessages'); + ipc.postStopSendingMessages(); this.stop(false); }; /** * Triggers when vehicle performing job enters an error state. */ - const onError = (vehicleId: string, message?: string): void => { - ipcRenderer.send('post', 'updateMessages', { + const onError = (vehicleId: number, message?: string): void => { + ipc.postLogMessages({ type: 'success', message: `${(vehicleConfig.vehicleInfos[vehicleId] as VehicleInfo).name} has entered an error state in ${this.missionName}: ${message || 'No error message specified'}`, }); @@ -278,7 +282,7 @@ export default abstract class Mission { private start(): void { // Fails to initialize if mission is not in the "ready" state. if (this.status !== 'waiting') { - ipcRenderer.send('post', 'updateMessages', { + ipc.postLogMessages({ type: 'failure', message: `Something wrong happened in ${this.missionName}`, }); @@ -289,7 +293,7 @@ export default abstract class Mission { // Adds all the tasks to the waitingTasks, if the task's jobType relates to this mission. const jobTasks = this.generateTasks(); Object.keys(jobTasks).forEach((jobType): void => { - if (vehicleConfig.isJobType(jobType) && this.jobTypes.has(jobType as JobType)) { + if (vehicleConfig.isValidJobType(jobType) && this.jobTypes.has(jobType as JobType)) { if (this.addTaskCompare[jobType]) { // Will happen if there is a compare for this job type. (jobTasks.get(jobType) as Task[]).forEach((task): void => { @@ -313,7 +317,8 @@ export default abstract class Mission { * Using waitingVehicles to assign vehicles tasks is faster than using activeVehicleMapping, * hence we need waitingVehicles. It is also more easy to understand. */ - Object.keys(this.activeVehicleMapping).forEach((vehicleId): void => { + Object.keys(this.activeVehicleMapping).forEach((vehicleIdString): void => { + const vehicleId = parseInt(vehicleIdString, 10); const jobType = this.activeVehicleMapping[vehicleId]; const vehicle = this.vehicles[vehicleId]; @@ -337,7 +342,7 @@ export default abstract class Mission { const vehicle = this.waitingVehicles.shift(jobType) as Vehicle; vehicle.assignTask(task); - this.activeTasks.set(`${vehicle.getVehicleId()}`, task); + this.activeTasks.set(vehicle.getVehicleId(), task); } }); @@ -372,11 +377,11 @@ export default abstract class Mission { const newTask = this.waitingTasks.shift(jobType) as Task; this.vehicles[message.sid].assignTask(newTask); - this.activeTasks.set(`${message.sid}`, newTask); + this.activeTasks.set(message.sid, newTask); } else { // All tasks in that jobType are complete, so this vehicle will go back to waiting vehicles. this.waitingVehicles.push(jobType, this.vehicles[message.sid]); - this.activeTasks.delete(`${message.sid}`); + this.activeTasks.delete(message.sid); } /* @@ -386,12 +391,12 @@ export default abstract class Mission { * throughout the mission. */ if (this.activeTasks.size === 0 && this.waitingTasks.size() === 0) { - ipcRenderer.send('post', 'completeMission', this.missionName); + ipc.postCompleteMission(this.missionName); this.completionCallback(this.generateTerminatedData()); this.stop(true); } else { // This should never happen. There is a big issue in the system if this somehow happens. - ipcRenderer.send('post', 'updateMessages', { + ipc.postLogMessages({ type: 'failure', message: `Mission completion failed in ${this.missionName}. Stopping mission`, }); @@ -413,7 +418,7 @@ export default abstract class Mission { protected addTask(jobType: string, task: Task, addToFront?: boolean): void { // Fails to initialize if mission is not in the "ready" state. if (this.status !== 'running') { - ipcRenderer.send('post', 'updateMessages', { + ipc.postLogMessages({ type: 'failure', message: `Tried to add ${task.taskType} task while ${this.missionName} is not running`, }); @@ -428,7 +433,7 @@ export default abstract class Mission { * specific jobType. */ vehicle.assignTask(task); - this.activeTasks.set(`${vehicle.getVehicleId()}`, task); + this.activeTasks.set(vehicle.getVehicleId(), task); } else if (addToFront) { // Add to front of waitingTasks if requested. this.waitingTasks.unshift(jobType, task); @@ -458,14 +463,15 @@ export default abstract class Mission { * manually/through error. */ private stop(success: boolean): void { - Object.keys(this.activeVehicleMapping).forEach((vehicleId): void => { + Object.keys(this.activeVehicleMapping).forEach((vehicleIdString): void => { + const vehicleId = parseInt(vehicleIdString, 10); this.vehicles[vehicleId].stop(); }); this.statusEventHandler.event('status', 'ready'); if (!success) { - ipcRenderer.send('post', 'updateMessage', { + ipc.postLogMessages({ type: 'success', message: `Terminated data: ${JSON.stringify(this.parameters)}`, }); @@ -487,9 +493,10 @@ export default abstract class Mission { * Check #1: ensure the jobs in the activeVehicleMapping cover the required jobs for this * mission. */ - if (!Array.from(Object.keys(this.activeVehicleMapping)).every( - (vehicleId): boolean => this.jobTypes.has(this.activeVehicleMapping[vehicleId]), - )) { + if (!Array.from(Object.keys(this.activeVehicleMapping)).every((vehicleIdString): boolean => { + const vehicleId = parseInt(vehicleIdString, 10); + return this.jobTypes.has(this.activeVehicleMapping[vehicleId]); + })) { return false; } @@ -498,7 +505,7 @@ export default abstract class Mission { * then there is a vehicle mapped for every job already. We need to check that all these * vehicles are in the "ready" state too. */ - if (!vehicleIds.every((vehicleId): boolean => this.vehicles[vehicleId].getStatus() === 'ready')) { + if (!vehicleIds.every((vehicleId): boolean => this.vehicles[parseInt(vehicleId, 10)].getStatus() === 'ready')) { return false; } @@ -515,7 +522,7 @@ export default abstract class Mission { * This never gets executed while mission's status is "ready", since no vehicles are assigned * then. */ - private handleUnresponsiveVehicle(vehicleId: string): void { + private handleUnresponsiveVehicle(vehicleId: number): void { /* * In the case that a vehicle that is not performing a job is in an error state * and somehow this function gets called (which it shouldn't), let the vehicle @@ -529,7 +536,7 @@ export default abstract class Mission { if (this.status === 'waiting') { // Simply exit the mission if we are in the "waiting" stage. - ipcRenderer.send('post', 'updateMessages', { + ipc.postLogMessages({ type: 'failure', message: 'Exiting mission due to vehicle in error state, take manual control over vehicle', }); @@ -539,11 +546,11 @@ export default abstract class Mission { * Stop sending all extra start messages and exit the mission. * See assignJobs for similar logic. */ - ipcRenderer.send('post', 'updateMessages', { + ipc.postLogMessages({ type: 'failure', message: 'Exiting mission due to vehicle in error state, take manual control over vehicle', }); - ipcRenderer.send('post', 'stopSendingMessages'); + ipc.postStopSendingMessages(); this.stop(false); } else { const jobType = this.activeVehicleMapping[vehicleId]; @@ -571,12 +578,12 @@ export default abstract class Mission { const newVehicle = this.waitingVehicles.shift(jobType); const task = this.waitingTasks.shift(jobType); if (newVehicle && task) { - this.activeVehicleMapping[`${newVehicle.getVehicleId()}`] = jobType; + this.activeVehicleMapping[newVehicle.getVehicleId()] = jobType; newVehicle.assignTask(task); - this.activeTasks.set(`${newVehicle.getVehicleId()}`, task); + this.activeTasks.set(newVehicle.getVehicleId(), task); } else { - ipcRenderer.send('post', 'updateMessages', { + ipc.postLogMessages({ type: 'failure', message: 'Exiting mission due to vehicle in error state, take manual control over vehicle', }); diff --git a/src/common/struct/Vehicle.ts b/src/common/struct/Vehicle.ts index 663e11f0..add56a5c 100644 --- a/src/common/struct/Vehicle.ts +++ b/src/common/struct/Vehicle.ts @@ -1,15 +1,14 @@ -import { ipcRenderer } from 'electron'; - import { JobType, vehicleConfig } from '../../static/index'; -import { VehicleObject, VehicleStatus } from '../../types/types'; - import { Message, StartMessage, Task, UpdateMessage, } from '../../types/messages'; +import { VehicleObject, VehicleStatus } from '../../types/types'; + +import ipc from '../../util/ipc'; import UpdateHandler from './UpdateHandler'; @@ -129,7 +128,7 @@ export default class Vehicle { if (battery > 1 || battery < 0) { const vehicleInfo = message && message.sid && vehicleConfig.vehicleInfos[message.sid]; - ipcRenderer.send('post', 'updateMessages', { + ipc.postLogMessages({ type: 'failure', message: `Received an invalid battery status (${battery * 100}%) from ${(vehicleInfo && vehicleInfo.name) || 'an unknown vehicle'}`, }); @@ -206,7 +205,7 @@ export default class Vehicle { * @param message Message to send. */ private sendMessage(message: Message): void { - ipcRenderer.send('post', 'sendMessage', this.vehicleId, message); + ipc.postSendMessage(this.vehicleId, message); } /** @@ -279,7 +278,7 @@ export default class Vehicle { * is not compatible with its job. * Vehicle's status is "waiting" if it has been assigned a mission but is awaiting a task. */ - if (this.status !== 'waiting' || !this.assignedJob || !vehicleConfig.isTaskTypeForJob(task.taskType, this.assignedJob)) { + if (this.status !== 'waiting' || !this.assignedJob || !vehicleConfig.isValidTaskTypeForJob(task.taskType, this.assignedJob)) { return false; } diff --git a/src/main/index.ts b/src/main/index.ts index 34790260..9b7bf6ea 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -15,6 +15,10 @@ import moment from 'moment'; import { imageConfig, locationConfig } from '../static/index'; +import { FileSaveOptions, LatLngZoom } from '../types/types'; + +import ipc from '../util/ipc'; + /** * This key is required to enable geolocation in the application. * Others cannot use this key outside of geolocation access so no need to hide it. @@ -102,7 +106,7 @@ const darwinMenu: MenuItemConstructorOptions = { * Runs when the user wants to save a configuration of the GCS. * The configuration currently includes the map location loaded. */ -function saveConfig(): void { +function postSaveConfig(): void { if (!mainWindow) return; const fileName = moment().format('[GCS Configuration] YYYY-MM-DD [at] h.mm.ss A'); @@ -118,25 +122,25 @@ function saveConfig(): void { if (!filePath) return; // Loads data up with information returned from main and mission windows. - const data = {}; - mainWindow.webContents.send('saveConfig', { + const saveOptions: FileSaveOptions = { filePath, - data, - }); + data: { + map: { + lat: 0, + lng: 0, + zoom: 18, + }, + }, + }; - if (missionWindow) { - missionWindow.webContents.send('saveConfig', { - filePath, - data, - }); - } + ipc.postSaveConfig(saveOptions, mainWindow, missionWindow); } /** * Runs when the user wants to load a configuration of the GCS. * The configuration currently includes the map location loaded. */ -function loadConfig(): void { +function postLoadConfig(): void { if (!mainWindow) return; // Loads a window that allows the user to choose the filePath of the file to be loaded. @@ -154,18 +158,13 @@ function loadConfig(): void { if (!data) return; - // Loads parsed data into main and mission windows. - mainWindow.webContents.send('loadConfig', data); - if (missionWindow) { - missionWindow.webContents.send('loadConfig', data); - } -}/** + ipc.postLoadConfig(data, mainWindow, missionWindow); +} + +/** * Hides the mission window. */ function hideMissionWindow(): void { - if (mainWindow) { - mainWindow.webContents.send('setSelectedMission', -1); - } if (missionWindow) { missionWindow.hide(); } @@ -285,7 +284,7 @@ const menu: MenuItemConstructorOptions[] = [ { label: 'Open File...', accelerator: 'CommandOrControl+O', - click: (): void => { loadConfig(); }, + click: (): void => { postLoadConfig(); }, }, { type: 'separator' }, { role: 'close' }, @@ -293,7 +292,7 @@ const menu: MenuItemConstructorOptions[] = [ { label: 'Save As...', accelerator: 'CommandOrControl+S', - click: (): void => { saveConfig(); }, + click: (): void => { postSaveConfig(); }, }, ], }, @@ -327,7 +326,7 @@ const menu: MenuItemConstructorOptions[] = [ label: 'My Location', click: (): void => { if (mainWindow) { - mainWindow.webContents.send('setMapToUserLocation'); + ipc.postSetMapToUserLocation(mainWindow, missionWindow); } }, }, @@ -385,9 +384,11 @@ function setLocationMenu(): void { locationMenu.push({ label, click: (menuItem): void => { - if (mainWindow) { - mainWindow.webContents.send('updateMapLocation', locationConfig.locations[menuItem.label]); - } + ipc.postUpdateMapLocation( + locationConfig.locations[menuItem.label] as LatLngZoom, + mainWindow, + missionWindow, + ); }, }); }); @@ -456,14 +457,17 @@ app.on('before-quit', (): void => { ipcMain.on('post', (_: Event, notification: string, ...data: any[]): void => { // eslint-disable-line @typescript-eslint/no-explicit-any if (notification === 'showMissionWindow') { showMissionWindow(); - } else if (notification === 'hideMissionWindow') { + return; + } + + if (notification === 'hideMissionWindow') { hideMissionWindow(); + return; } /* - * We must forward ipc notifications to both windows or else - * some notifications will not be picked up. Of course we can filter out which - * notifications go for which window, but its simpler to have it forwarded to both. + * We must forward notifications to both windows or else some notifications will not + * be picked up. */ if (mainWindow) { mainWindow.webContents.send(notification, ...data); diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx new file mode 100644 index 00000000..f985f0b6 --- /dev/null +++ b/src/renderer/App.tsx @@ -0,0 +1,55 @@ +import { ipcRenderer } from 'electron'; +import React, { Component, ReactNode } from 'react'; + +import { ThemeProps } from '../types/types'; + +import MainWindow from './mainWindow/MainWindow'; +import MissionWindow from './missionWindow/MissionWindow'; + +import 'leaflet/dist/leaflet.css'; +import 'react-virtualized/styles.css'; + +import './app.css'; + +const windows: { [hash: string]: ReactNode } = { + '#main': MainWindow, + '#mission': MissionWindow, +}; + +type State = ThemeProps; + +export default class App extends Component<{}, State> { + public constructor(props: {}) { + super(props); + + this.state = { + theme: 'light', + }; + + this.toggleTheme = this.toggleTheme.bind(this); + } + + public componentDidMount(): void { + ipcRenderer.on('toggleTheme', this.toggleTheme); + } + + public toggleTheme(): void { + const { theme } = this.state; + + this.setState({ theme: theme === 'light' ? 'dark' : 'light' }); + } + + public render(): ReactNode { + const { theme } = this.state; + + // In the case another hash was somehow loaded. + if (window.location.hash !== '#main' && window.location.hash !== '#mission') { + return
; + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const Window = windows[window.location.hash] as any; + + return ; + } +} diff --git a/src/renderer/index.css b/src/renderer/app.css similarity index 97% rename from src/renderer/index.css rename to src/renderer/app.css index 1ad36cf1..ef34f39b 100644 --- a/src/renderer/index.css +++ b/src/renderer/app.css @@ -1,4 +1,4 @@ -/* Defined styles for the whole application */ +/* Styles for the whole application */ html, body, diff --git a/src/renderer/fixtures/completeJobFixture.ts b/src/renderer/fixtures/completeJobFixture.ts deleted file mode 100644 index 3a710920..00000000 --- a/src/renderer/fixtures/completeJobFixture.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { ipcRenderer } from 'electron'; - -/** - * Sends a completeJob notification 5 seconds after the startJob notification is received. - */ -export default function completeJob(): void { - // Sends completeJob notification after receiving startJob notification. - ipcRenderer.on('startJob', (): void => { - setTimeout((): void => { - ipcRenderer.send('post', 'completeJob'); - }, 5000); - }); -} diff --git a/src/renderer/fixtures/index.ts b/src/renderer/fixtures/index.ts index 4a92e8df..334d26d3 100644 --- a/src/renderer/fixtures/index.ts +++ b/src/renderer/fixtures/index.ts @@ -1,17 +1,8 @@ -/* - * Import all fixtures in here. Feel free to enable/disable fixtures by commenting them out. - * - * Note we do not use fs in here since building the app will not properly link the items. - * We shouldn't have fixtures enabled when building in the first place, but we are preventing - * errors from happening in the first place. - */ +// Import all fixtures in here. Feel free to enable/disable fixtures by commenting them out. -import completeJob from './completeJobFixture'; import updateMessages from './updateMessagesFixtures'; import updateVehicles from './updateVehiclesFixtures'; -completeJob(); - setInterval((): void => { updateMessages(); updateVehicles(); diff --git a/src/renderer/fixtures/updateMessagesFixtures.ts b/src/renderer/fixtures/updateMessagesFixtures.ts index bf6003c6..def9cd22 100644 --- a/src/renderer/fixtures/updateMessagesFixtures.ts +++ b/src/renderer/fixtures/updateMessagesFixtures.ts @@ -1,8 +1,8 @@ -import { ipcRenderer } from 'electron'; +import { LogMessage } from '../../types/types'; -import { MessageType } from '../../types/types'; +import ipc from '../../util/ipc'; -const fixtures: { type?: MessageType; message: string }[] = [ +const fixtures: LogMessage[] = [ { type: 'failure', message: 'Test failure message', @@ -25,5 +25,5 @@ const fixtures: { type?: MessageType; message: string }[] = [ */ export default function updateMessages(): void { const fixture = fixtures[Math.floor(Math.random() * fixtures.length)]; - ipcRenderer.send('post', 'updateMessages', fixture); + ipc.postLogMessages(fixture); } diff --git a/src/renderer/fixtures/updateVehiclesFixtures.ts b/src/renderer/fixtures/updateVehiclesFixtures.ts index e8e65746..f47b52c0 100644 --- a/src/renderer/fixtures/updateVehiclesFixtures.ts +++ b/src/renderer/fixtures/updateVehiclesFixtures.ts @@ -1,11 +1,11 @@ -import { ipcRenderer } from 'electron'; - import Vehicle, { VehicleOptions } from '../../common/struct/Vehicle'; import { locationConfig, vehicleConfig } from '../../static/index'; import { VehicleObject, VehicleStatus } from '../../types/types'; +import ipc from '../../util/ipc'; + const fixtureOptions: VehicleOptions[] = [ { sid: 100, @@ -63,5 +63,5 @@ export default function updateVehicles(): void { fixtures[i] = vehicles[i].toPlainObject(); } - ipcRenderer.send('post', 'updateVehicles', ...fixtures); + ipc.postUpdateVehicles(...fixtures); } diff --git a/src/renderer/index.js b/src/renderer/index.js deleted file mode 100644 index 7b22c788..00000000 --- a/src/renderer/index.js +++ /dev/null @@ -1,80 +0,0 @@ -/* eslint-disable react/jsx-filename-extension */ - -import { ipcRenderer } from 'electron'; -import React, { Component } from 'react'; -import ReactDOM from 'react-dom'; - -import statics from '../static/index'; - -import MainWindow from './mainWindow/MainWindow'; -import MissionWindow from './missionWindow/MissionWindow'; - -import 'leaflet/dist/leaflet.css'; -import 'react-virtualized/styles.css'; - -import './index.css'; - -const isDevelopment = process.env.NODE_ENV !== 'production'; - -const windows = { - '#main': MainWindow, - '#mission': MissionWindow, -}; - -/** - * Function runs only once, when it is loaded through the main window. - * Running this function more than once (through both main and mission windows) causes - * the information below to be run twice, and we do not want that to happen. - */ -function runOnce() { - /* eslint-disable global-require */ - - if (window.location.hash !== '#main') return; - - // Set up Orchestrator. - require('../common/Orchestrator'); - - // Set up geolocation if geolocation is enabled in config. - if (statics.config.geolocation) { - ipcRenderer.send('post', 'setMapToUserLocation'); - } - - // Sets up fixtures if in development and fixtures are enabled in config. - if (isDevelopment && statics.config.fixtures) { - require('./fixtures/index'); - } - - /* eslint-enable global-require */ -} - -class Index extends Component { - constructor(props) { - super(props); - - this.state = { - theme: 'light', - }; - - this.toggleTheme = this.toggleTheme.bind(this); - } - - componentDidMount() { - ipcRenderer.on('toggleTheme', this.toggleTheme); - } - - toggleTheme() { - const { theme } = this.state; - - this.setState({ theme: theme === 'light' ? 'dark' : 'light' }); - } - - render() { - const { theme } = this.state; - - const Window = windows[window.location.hash]; - - return ; - } -} - -ReactDOM.render(, document.getElementById('app'), runOnce); diff --git a/src/renderer/index.ts b/src/renderer/index.ts new file mode 100644 index 00000000..80dd644b --- /dev/null +++ b/src/renderer/index.ts @@ -0,0 +1,36 @@ +/* eslint-disable global-require */ + +import React from 'react'; +import ReactDOM from 'react-dom'; + +import statics from '../static/index'; + +import ipc from '../util/ipc'; + +import App from './App'; + +const isDevelopment = process.env.NODE_ENV !== 'production'; + +/** + * Function runs only once, when it is loaded through the main window. + * Running this function more than once (through both main and mission windows) causes + * the information below to be run twice, and we do not want that to happen. + */ +function runOnce(): void { + if (window.location.hash !== '#main') return; + + // Set up Orchestrator. + require('../common/Orchestrator'); + + // Set up geolocation if geolocation is enabled in config. + if (statics.config.geolocation) { + ipc.postSetMapToUserLocation(); + } + + // Sets up fixtures if in development and fixtures are enabled in config. + if (isDevelopment && statics.config.fixtures) { + require('./fixtures/index'); + } +} + +ReactDOM.render(React.createElement(App), document.getElementById('app'), runOnce); diff --git a/src/renderer/mainWindow/log/LogContainer.tsx b/src/renderer/mainWindow/log/LogContainer.tsx index fb6afd4b..dce63cf9 100644 --- a/src/renderer/mainWindow/log/LogContainer.tsx +++ b/src/renderer/mainWindow/log/LogContainer.tsx @@ -9,33 +9,15 @@ import { ListRowProps, } from 'react-virtualized'; -import { isMessageType, MessageType, ThemeProps } from '../../../types/types'; +import { + isMessageType, + LogMessage, + MessageType, + ThemeProps, +} from '../../../types/types'; import './log.css'; -/** - * Interface for a message in the log container. - */ -interface Message { - /** - * The type of the message. Defines the color the message will be printed in. - */ - type?: MessageType; - - /** - * The content of the message. - */ - message: string; - - /** - * The time was received and logged. - */ - time: Moment; -} - -/** - * State of the log container. - */ interface State { /** * The current filter being applied to messages. If the filter is not "", then only messages @@ -47,7 +29,7 @@ interface State { * All messages that have been logged. This includes message that are being hidden * if a filter is being applied. */ - messages: Message[]; + messages: LogMessage[]; /** * All messages that are being shown. If there's no filter, then this is the same @@ -55,7 +37,7 @@ interface State { * message every time the component is re-rendered) for a space (a duplicate array * of messages). */ - filteredMessages: Message[]; + filteredMessages: LogMessage[]; } /** @@ -79,11 +61,11 @@ export default class LogContainer extends Component { this.rowRenderer = this.rowRenderer.bind(this); this.clearMessages = this.clearMessages.bind(this); this.updateFilter = this.updateFilter.bind(this); - this.updateMessages = this.updateMessages.bind(this); + this.logMessages = this.logMessages.bind(this); } public componentDidMount(): void { - ipcRenderer.on('updateMessages', (_: Event, ...messages: Message[]): void => this.updateMessages(...messages)); + ipcRenderer.on('logMessages', (_: Event, ...messages: LogMessage[]): void => this.logMessages(...messages)); } /** @@ -110,7 +92,7 @@ export default class LogContainer extends Component { parent={parent} >
-
{message.time.format('HH:mm:ss.SSS')}
+
{(message.time as Moment).format('HH:mm:ss.SSS')}
{message.message}
@@ -151,16 +133,16 @@ export default class LogContainer extends Component { /** * Updates the messages in the log. Will update filtered messages accordingly. */ - private updateMessages(...messages: Message[]): void { + private logMessages(...messages: LogMessage[]): void { const { filteredMessages, messages: thisMessages, filter } = this.state; const currentMessages = thisMessages; const currentFilteredMessages = filteredMessages; messages.forEach((message): void => { - const msg: Message = { + const msg: LogMessage = { type: message.type || '', - ...message, - time: moment(), + message: message.message, + time: message.time || moment(), }; if (filter === '' || msg.type === filter) { diff --git a/src/renderer/mainWindow/map/MapContainer.tsx b/src/renderer/mainWindow/map/MapContainer.tsx index a4a48cb2..7a949e53 100644 --- a/src/renderer/mainWindow/map/MapContainer.tsx +++ b/src/renderer/mainWindow/map/MapContainer.tsx @@ -82,8 +82,8 @@ export default class MapContainer extends Component { } public componentDidMount(): void { - ipcRenderer.on('loadConfig', (_: Event, data: FileLoadOptions): void => this.loadConfig(data)); - ipcRenderer.on('saveConfig', (_: Event, data: FileSaveOptions): void => this.saveConfig(data)); + ipcRenderer.on('loadConfig', (_: Event, loadOptions: FileLoadOptions): void => this.loadConfig(loadOptions)); + ipcRenderer.on('saveConfig', (_: Event, saveOptions: FileSaveOptions): void => this.saveConfig(saveOptions)); ipcRenderer.on('centerMapToVehicle', (_: Event, vehicle: VehicleObject): void => this.centerMapToVehicle(vehicle)); ipcRenderer.on('setMapToUserLocation', this.setMapToUserLocation); diff --git a/src/renderer/mainWindow/map/control/GeolocationControl.tsx b/src/renderer/mainWindow/map/control/GeolocationControl.tsx index d647e9f8..7084b563 100644 --- a/src/renderer/mainWindow/map/control/GeolocationControl.tsx +++ b/src/renderer/mainWindow/map/control/GeolocationControl.tsx @@ -1,6 +1,7 @@ -import { ipcRenderer } from 'electron'; import React, { Component, ReactNode } from 'react'; +import ipc from '../../../../util/ipc'; + import Control from './Control'; /** @@ -8,7 +9,7 @@ import Control from './Control'; */ export default class GeolocationControl extends Component { private static onClick(): void{ - ipcRenderer.send('post', 'setMapToUserLocation'); + ipc.postSetMapToUserLocation(); } public render(): ReactNode { diff --git a/src/renderer/mainWindow/map/control/ThemeControl.tsx b/src/renderer/mainWindow/map/control/ThemeControl.tsx index 8aafa435..dec9a36e 100644 --- a/src/renderer/mainWindow/map/control/ThemeControl.tsx +++ b/src/renderer/mainWindow/map/control/ThemeControl.tsx @@ -1,8 +1,9 @@ -import { ipcRenderer } from 'electron'; import React, { Component, ReactNode } from 'react'; import { ThemeProps } from '../../../../types/types'; +import ipc from '../../../../util/ipc'; + import Control from './Control'; /** @@ -10,7 +11,7 @@ import Control from './Control'; */ export default class ThemeControl extends Component { private static onClick(): void { - ipcRenderer.send('post', 'toggleTheme'); + ipc.postToggleTheme(); } public render(): ReactNode { diff --git a/src/renderer/mainWindow/missionWindow/mission.css b/src/renderer/mainWindow/missionWindow/mission.css deleted file mode 100644 index e69de29b..00000000 diff --git a/src/renderer/mainWindow/vehicle/VehicleTable.tsx b/src/renderer/mainWindow/vehicle/VehicleTable.tsx index b9d2ab2d..aaaeb904 100644 --- a/src/renderer/mainWindow/vehicle/VehicleTable.tsx +++ b/src/renderer/mainWindow/vehicle/VehicleTable.tsx @@ -1,4 +1,3 @@ -import { ipcRenderer } from 'electron'; import React, { Component, ReactNode } from 'react'; import { AutoSizer, @@ -17,6 +16,8 @@ import { import { ThemeProps, VehicleObject } from '../../../types/types'; +import ipc from '../../../util/ipc'; + interface WidthSignature { [column: string]: number; } @@ -77,7 +78,7 @@ export default class VehicleTable extends Component { */ private onRowClick(info: RowMouseEventHandlerParams): void { const { vehicles } = this.props; - ipcRenderer.send('post', 'centerMapToVehicle', vehicles[info.index]); + ipc.postCenterMapToVehicle(vehicles[info.index]); } /** diff --git a/src/renderer/missionWindow/MissionWindow.tsx b/src/renderer/missionWindow/MissionWindow.tsx index db0c0e5d..9ca3c997 100644 --- a/src/renderer/missionWindow/MissionWindow.tsx +++ b/src/renderer/missionWindow/MissionWindow.tsx @@ -1,4 +1,3 @@ -// import { ipcRenderer } from 'electron'; import React, { Component, ReactNode } from 'react'; import { @@ -8,14 +7,18 @@ import { } from '../../types/messages'; import { ThemeProps } from '../../types/types'; -import ISRSearch from './missionParameterLayouts/ISRSearch'; -import PayLoadDrop from './missionParameterLayouts/PayloadDrop'; -import UGVRetrieveTarget from './missionParameterLayouts/UGVRetrieveTarget'; +import ISRSearch from './parameters/ISRSearch'; +import PayLoadDrop from './parameters/PayloadDrop'; +import UGVRetrieveTarget from './parameters/UGVRetrieveTarget'; +import UUVRetreiveTarget from './parameters/UUVRetrieveTarget'; +import VTOLSearch from './parameters/VTOLSearch'; -const layouts: { [missionName: string]: () => JSX.Element } = { +const layouts: { [missionName: string]: () => ReactNode } = { payloadDrop: PayLoadDrop, isrSearch: ISRSearch, ugvRetrieve: UGVRetrieveTarget, + uuvRetrieve: UUVRetreiveTarget, + vtolSearch: VTOLSearch, }; interface Parameters { @@ -32,7 +35,7 @@ interface State { } /** - * + * Mission window component. */ export default class MissionWindow extends Component { public constructor(props: ThemeProps) { @@ -47,7 +50,8 @@ export default class MissionWindow extends Component { const { theme } = this.props; const { firstMission } = this.state; - const Layout = layouts[firstMission]; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const Layout = layouts[firstMission] as any; return (
diff --git a/src/renderer/missionWindow/missionParameterLayouts/ISRSearch.tsx b/src/renderer/missionWindow/missionParameterLayouts/ISRSearch.tsx deleted file mode 100644 index 68e2970c..00000000 --- a/src/renderer/missionWindow/missionParameterLayouts/ISRSearch.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import React from 'react'; - -export const jobs = ['isrSearch']; - -export default function ISRSearch(): JSX.Element { - return ( -
ISRSearch
- ); -} diff --git a/src/renderer/missionWindow/missionParameterLayouts/PayloadDrop.tsx b/src/renderer/missionWindow/missionParameterLayouts/PayloadDrop.tsx deleted file mode 100644 index 9b165e68..00000000 --- a/src/renderer/missionWindow/missionParameterLayouts/PayloadDrop.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import React from 'react'; - -export const jobs = ['payloadDrop']; - -export default function ISRSearch(): JSX.Element { - return ( -
Payload Drop
- ); -} diff --git a/src/renderer/missionWindow/missionParameterLayouts/UGVRetrieveTarget.tsx b/src/renderer/missionWindow/missionParameterLayouts/UGVRetrieveTarget.tsx deleted file mode 100644 index 46192c8b..00000000 --- a/src/renderer/missionWindow/missionParameterLayouts/UGVRetrieveTarget.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import React from 'react'; - -export const jobs = ['ugvRetrieve']; - -export default function ISRSearch(): JSX.Element { - return ( -
ISRSearch
- ); -} diff --git a/src/renderer/missionWindow/missionParameterLayouts/UUVRetrieveTarget.tsx b/src/renderer/missionWindow/missionParameterLayouts/UUVRetrieveTarget.tsx deleted file mode 100644 index 88be2536..00000000 --- a/src/renderer/missionWindow/missionParameterLayouts/UUVRetrieveTarget.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import React from 'react'; - -export const jobs = ['uuvRetrieve']; - -export default function ISRSearch(): JSX.Element { - return ( -
ISRSearch
- ); -} diff --git a/src/renderer/missionWindow/parameters/ISRSearch.tsx b/src/renderer/missionWindow/parameters/ISRSearch.tsx new file mode 100644 index 00000000..acd43fdb --- /dev/null +++ b/src/renderer/missionWindow/parameters/ISRSearch.tsx @@ -0,0 +1,9 @@ +import React, { ReactNode } from 'react'; + +export const jobs = ['isrSearch']; + +export default function ISRSearch(): ReactNode { + return ( +
ISRSearch
+ ); +} diff --git a/src/renderer/missionWindow/parameters/PayloadDrop.tsx b/src/renderer/missionWindow/parameters/PayloadDrop.tsx new file mode 100644 index 00000000..e12654aa --- /dev/null +++ b/src/renderer/missionWindow/parameters/PayloadDrop.tsx @@ -0,0 +1,9 @@ +import React, { ReactNode } from 'react'; + +export const jobs = ['payloadDrop']; + +export default function ISRSearch(): ReactNode { + return ( +
Payload Drop
+ ); +} diff --git a/src/renderer/missionWindow/parameters/UGVRetrieveTarget.tsx b/src/renderer/missionWindow/parameters/UGVRetrieveTarget.tsx new file mode 100644 index 00000000..e22e1c97 --- /dev/null +++ b/src/renderer/missionWindow/parameters/UGVRetrieveTarget.tsx @@ -0,0 +1,9 @@ +import React, { ReactNode } from 'react'; + +export const jobs = ['ugvRetrieve']; + +export default function ISRSearch(): ReactNode { + return ( +
ISRSearch
+ ); +} diff --git a/src/renderer/missionWindow/parameters/UUVRetrieveTarget.tsx b/src/renderer/missionWindow/parameters/UUVRetrieveTarget.tsx new file mode 100644 index 00000000..7023ae0d --- /dev/null +++ b/src/renderer/missionWindow/parameters/UUVRetrieveTarget.tsx @@ -0,0 +1,9 @@ +import React, { ReactNode } from 'react'; + +export const jobs = ['uuvRetrieve']; + +export default function ISRSearch(): ReactNode { + return ( +
ISRSearch
+ ); +} diff --git a/src/renderer/missionWindow/missionParameterLayouts/VTOLSearch.tsx b/src/renderer/missionWindow/parameters/VTOLSearch.tsx similarity index 51% rename from src/renderer/missionWindow/missionParameterLayouts/VTOLSearch.tsx rename to src/renderer/missionWindow/parameters/VTOLSearch.tsx index 6e09e9d5..c7c5a903 100644 --- a/src/renderer/missionWindow/missionParameterLayouts/VTOLSearch.tsx +++ b/src/renderer/missionWindow/parameters/VTOLSearch.tsx @@ -1,8 +1,8 @@ -import React from 'react'; +import React, { ReactNode } from 'react'; export const jobs = ['quickScan', 'detailedSearch']; -export default function ISRSearch(): JSX.Element { +export default function ISRSearch(): ReactNode { return (
ISRSearch
); diff --git a/src/static/config.json b/src/static/config.json index c1fb6bfd..51f9f420 100644 --- a/src/static/config.json +++ b/src/static/config.json @@ -1,8 +1,7 @@ { - "fixtures": false, + "fixtures": true, "geolocation": true, "vehicleDisconnectionTime": 20, - "messageSendRate": 10, - "vehicleAllowedErrorTime": 5 + "messageSendRate": 10 } diff --git a/src/static/index.ts b/src/static/index.ts index dc35d337..71e9ad87 100644 --- a/src/static/index.ts +++ b/src/static/index.ts @@ -59,7 +59,7 @@ export type JobType = 'isrSearch' | 'payloadDrop' | 'ugvRetrieve' | 'uuvRetrieve | 'quickScan' | 'detailedSearch'; const vehicleInfos: { - [id: string]: VehicleInfo | undefined; + [vehicleId: number]: VehicleInfo | undefined; } = vehicleInfosObject; const vehicleJobs: { @@ -86,10 +86,17 @@ const startLocation: LatLngZoom = startLocationString && locations[startLocation zoom: 18, }; +/** + * Checks if a number is a valid vehicle id. + */ +function isValidVehicleId(vehicleId: number): boolean { + return vehicleInfos[vehicleId] !== undefined; +} + /** * Checks if a string is a valid job type. */ -function isJobType(jobType: string): boolean { +function isValidJobType(jobType: string): boolean { return jobType === 'isrSearch' || jobType === 'payloadDrop' || jobType === 'ugvRetrieve' @@ -101,7 +108,7 @@ function isJobType(jobType: string): boolean { /** * Checks if a task is a valid task type for that job. */ -function isTaskTypeForJob(taskType: string, jobType: JobType): boolean { +function isValidTaskTypeForJob(taskType: string, jobType: JobType): boolean { return (vehicleJobs[jobType] as string[]).includes(taskType); } @@ -111,8 +118,9 @@ export const locationConfig = { }; export const vehicleConfig = { - isJobType, - isTaskTypeForJob, + isValidJobType, + isValidTaskTypeForJob, + isValidVehicleId, vehicleIds, vehicleInfos, vehicleStatuses, diff --git a/src/types/messages.ts b/src/types/messages.ts index b53fcaee..8fb0f9ab 100644 --- a/src/types/messages.ts +++ b/src/types/messages.ts @@ -451,7 +451,7 @@ export interface StartMessage extends MessageBase { function isStartMessage(message: Message): boolean { return message.type === 'start' - && vehicleConfig.isJobType(message.jobType); + && vehicleConfig.isValidJobType(message.jobType); } export interface AddMissionMessage extends MessageBase { @@ -602,7 +602,7 @@ export function isConnectMessage(message: Message): boolean { return message.type === 'connect' // Check if jobsAvailable is a string array of valid job types. - && message.jobsAvailable.every(vehicleConfig.isJobType); + && message.jobsAvailable.every(vehicleConfig.isValidJobType); } // Definitions for all other message types. @@ -621,7 +621,7 @@ export function isAcknowledgementMessage(message: Message): boolean { && !Number.isNaN(message.ackid); } -export interface BadMessageMessage extends MessageBase { +export interface BadMessage extends MessageBase { type: 'badMessage'; /** @@ -633,13 +633,13 @@ export interface BadMessageMessage extends MessageBase { error?: string; } -export function isBadMessageMessage(message: Message): boolean { +export function isBadMessage(message: Message): boolean { return message.type === 'badMessage'; } export type Message = StartMessage | AddMissionMessage | PauseMessage | ResumeMessage | StopMessage | ConnectionAckMessage | UpdateMessage | POIMessage | CompleteMessage | ConnectMessage -| AcknowledgementMessage | BadMessageMessage; +| AcknowledgementMessage | BadMessage; /** * Logic for this function is slightly different from isTask since the GCS does not need to know @@ -710,7 +710,7 @@ export const messageTypeGuard = { isCompleteMessage, isConnectMessage, isAcknowledgementMessage, - isBadMessageMessage, + isBadMessage, isMessage, isJSONMessage, }; diff --git a/src/types/types.ts b/src/types/types.ts index 82ec218e..9e00b2d9 100644 --- a/src/types/types.ts +++ b/src/types/types.ts @@ -1,3 +1,5 @@ +import { Moment } from 'moment'; + /** * Status of the vehicle, lets us know of where the vehicle is in its mission. */ @@ -120,3 +122,23 @@ export interface FileSaveOptions { */ data: FileLoadOptions; } + +/** + * Structure for a message displayed in log container. + */ +export interface LogMessage { + /** + * The type of the message. Defines the color the message will be printed in. + */ + type?: MessageType; + + /** + * The content of the message. + */ + message: string; + + /** + * The time was received and logged. + */ + time?: Moment; +} diff --git a/src/util/ipc.ts b/src/util/ipc.ts new file mode 100644 index 00000000..f5131e5d --- /dev/null +++ b/src/util/ipc.ts @@ -0,0 +1,295 @@ +/* + * Write all ipcRenderer post functions here to ensure that all developers know the type + * of what are included into these messages. When writing the function, write where + * these notifications are received. Write the functions in ALPHABETICAL ORDER (of notification). + * A simple note on which file (assume going from src directory) will help developers out a lot. + * Do not forget to include the function in the default export! + * + * When writing the receiving side of ipcRenderer, the function called by the callback + * function must be the EXACT SAME NAME as the notification message and have parameters with + * SAME EXACT NAMES as the parameters of the ipc function below. + * + * Look at all code for examples. Do not forget that the first parameter of the callback function + * of the receiving side of ipcRenderer will have an Event object passed to it. Name this object "_" + * unless you plan on using the object, then name it "event". + */ + +import { BrowserWindow, ipcRenderer } from 'electron'; + +import { JSONMessage, Message } from '../types/messages'; +import { + FileLoadOptions, + FileSaveOptions, + LatLngZoom, + LogMessage, + VehicleObject, +} from '../types/types'; + +/** + * Post "centerMapToVehicle" notification. + * + * Files that take this notification: + * - renderer/mainWindow/map/MapContainer + */ +function postCenterMapToVehicle(vehicle: VehicleObject): void { + ipcRenderer.send('post', 'centerMapToVehicle', vehicle); +} + +/** + * Post "competeMission" notification. + * + * Files that take this notification: + */ +function postCompleteMission(missionName: string): void { + ipcRenderer.send('post', 'completeMission', missionName); +} + +/** + * Post "deactivateVehicle" notification. + * + * Files that take this notification: + * - common/Orchestrator + */ +function postDeactivateVehicle(vehicleId: number): void { + ipcRenderer.send('post', 'deactivateVehicle', vehicleId); +} + +/** + * Post "handleAcknowledgementMessage" notification. + * + * Files that take this notification: + * - common/Orchestrator + */ +function postHandleAcknowledgementMessage(jsonMessage: JSONMessage): void { + ipcRenderer.send('post', 'handleAcknowledgementMessage', jsonMessage); +} + +/** + * Post "handleBadMessage" notification. + * + * Files that take this notification: + * - common/Orchestrator + */ +function postHandleBadMessage(jsonMessage: JSONMessage): void { + ipcRenderer.send('post', 'handleBadMessage', jsonMessage); +} + +/** + * Post "handleCompleteMessage" notification. + * + * Files that take this notification: + * - common/Orchestrator + */ +function postHandleCompleteMessage(jsonMessage: JSONMessage): void { + ipcRenderer.send('post', 'handleCompleteMessage', jsonMessage); +} + +/** + * Post "handleConnectMessage" notification. + * + * Files that take this notification: + * - common/Orchestrator + */ +function postHandleConnectMessage(jsonMessage: JSONMessage): void { + ipcRenderer.send('post', 'handleConnectMessage', jsonMessage); +} + +/** + * Post "handlePOIMessage" notification. + * + * Files that take this notification: + * - common/Orchestrator + */ +function postHandlePOIMessage(jsonMessage: JSONMessage): void { + ipcRenderer.send('post', 'handlePOIMessage', jsonMessage); +} + +/** + * Post "handleUpdateMessage" notification. + * + * Files that take this notification: + * - common/Orchestrator + */ +function postHandleUpdateMessage(jsonMessage: JSONMessage): void { + ipcRenderer.send('post', 'handleUpdateMessage', jsonMessage); +} + +/** + * Post "hideMissionWindow" notification. + * + * Files that take this notification: + * - main/index + */ +function postHideMissionWindow(): void { + ipcRenderer.send('post', 'hideMissionWindow'); +} + +/** + * Post "loadConfig" notification. + * + * Files that take this notification: + * - renderer/mainWindow/map/MapContainer + */ +function postLoadConfig( + loadOptions: FileLoadOptions, + mainWindow?: BrowserWindow | null, + missionWindow?: BrowserWindow | null, +): void { + if (mainWindow !== undefined) { + if (mainWindow) mainWindow.webContents.send('post', 'loadConfig', loadOptions); + if (missionWindow) missionWindow.webContents.send('post', 'loadConfig', loadOptions); + } else { + ipcRenderer.send('post', 'loadConfig', loadOptions); + } +} + +/** + * Post "logMessages" notification. + * + * Files that take this notification: + * - renderer/mainWindow/log/LogContainer + */ +function postLogMessages(...messages: LogMessage[]): void { + ipcRenderer.send('post', 'logMessages', ...messages); +} + +/** + * Post "receiveMessage" notification. + * + * Files that take this notification: + * - common/MessageHandler + */ +function postReceiveMessage(text: string): void { + ipcRenderer.send('post', 'receiveMessage', text); +} + +/** + * Post "saveConfig" notification. + * + * Files that take this notification: + * - renderer/mainWindow/map/MapContainer + */ +function postSaveConfig( + saveOptions: FileSaveOptions, + mainWindow?: BrowserWindow | null, + missionWindow?: BrowserWindow | null, +): void { + if (mainWindow !== undefined) { + if (mainWindow) mainWindow.webContents.send('post', 'saveConfig', saveOptions); + if (missionWindow) missionWindow.webContents.send('post', 'saveConfig', saveOptions); + } else { + ipcRenderer.send('post', 'saveConfig', saveOptions); + } +} + +/** + * Post "sendMessage" notification. + * + * Files that take this notification: + * - common/MessageHandler + */ +function postSendMessage(vehicleId: number, message: Message): void { + ipcRenderer.send('post', 'sendMessage', vehicleId, message); +} + +/** + * Post "setMapToUserLocation" notification. + * + * Files that take this notification: + * - renderer/mainWindow/map/MapContainer + */ +function postSetMapToUserLocation( + mainWindow?: BrowserWindow | null, + missionWindow?: BrowserWindow | null, +): void { + if (mainWindow !== undefined) { + if (mainWindow) mainWindow.webContents.send('setMapToUserLocation'); + if (missionWindow) missionWindow.webContents.send('setMapToUserLocation'); + } else { + ipcRenderer.send('post', 'setMapToUserLocation'); + } +} + +/** + * Post "showMissionWindow" notification. + * + * Files that take this notification: + * - main/index + */ +function postShowMissionWindow(): void { + ipcRenderer.send('post', 'showMissionWindow'); +} + +/** + * Post "stopSendingMessages" notification. + * + * Files that take this notification: + * - common/MessageHandler + */ +function postStopSendingMessages(): void { + ipcRenderer.send('post', 'stopSendingMessages'); +} + +/** + * Post "toggleTheme" notification. + * + * Files that take this notification: + * - renderer/index + */ +function postToggleTheme(): void { + ipcRenderer.send('post', 'toggleTheme'); +} + +/** + * Post "updateMapLocation" notification. + * + * Files that take this notification: + * - renderer/mainWindow/map/MapContainer + */ +function postUpdateMapLocation( + location: LatLngZoom, + mainWindow?: BrowserWindow | null, + missionWindow?: BrowserWindow | null, +): void { + if (mainWindow !== undefined) { + if (mainWindow) mainWindow.webContents.send('updateMapLocation', location); + if (missionWindow) missionWindow.webContents.send('updateMapLocation', location); + } else { + ipcRenderer.send('post', 'updateMapLocation', location); + } +} + +/** + * Post "updateVehicles" notification. + * + * Files that take this notification: + * - renderer/mainWindow/map/MapContainer + * - renderer/mainWindow/vehicle/VehicleContainer + */ +function postUpdateVehicles(...vehicles: VehicleObject[]): void { + ipcRenderer.send('post', 'updateVehicles', ...vehicles); +} + +export default { + postCenterMapToVehicle, + postCompleteMission, + postHandleAcknowledgementMessage, + postHandleBadMessage, + postHandleCompleteMessage, + postHandleConnectMessage, + postHandlePOIMessage, + postHandleUpdateMessage, + postHideMissionWindow, + postDeactivateVehicle, + postLoadConfig, + postLogMessages, + postReceiveMessage, + postSaveConfig, + postSendMessage, + postSetMapToUserLocation, + postShowMissionWindow, + postStopSendingMessages, + postToggleTheme, + postUpdateMapLocation, + postUpdateVehicles, +}; diff --git a/src/util/util.ts b/src/util/util.ts index e235c69d..3ade0bbe 100644 --- a/src/util/util.ts +++ b/src/util/util.ts @@ -39,28 +39,33 @@ export default { }; /** - * Performs binary search for an element. Will return the index in the array - * the compareFunction goes through (can be the index of the value). + * Performs binary search for an element. The index that will be returned will either be: + * - index of value provided. + * - index where the value would fit in the array, if it were spliced in. + * ex: searchIndex([1, 2, 4, 5], 3, (a, b) => a - b) will return index = 2. */ export function searchIndex( array: T[], value: T, compareFunction: (a: T, b: T) => number, ): number { - let first = 0; - let last = array.length - 1; + let left = 0; + let right = array.length - 1; - while (first <= last) { - const middle = Math.floor((first + last) / 2); + while (left <= right) { + const middle = Math.floor((left + right) / 2); const comparison = compareFunction(value, array[middle]); - if (comparison > 0) { - first = middle + 1; - } else if (comparison < 0) { - last = middle - 1; - } else { + if (comparison === 0) { return middle; } + + if (comparison < 0) { + left = middle + 1; + } else { + right = middle - 1; + } } - return -(first + 1); + + return left; } diff --git a/tsconfig.json b/tsconfig.json index 7d14a3aa..eabc865f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "./node_modules/electron-webpack/tsconfig-base.json", + "extends": "electron-webpack/tsconfig-base.json", "compilerOptions": { "target": "es6", "module": "commonjs", From 0d10de29903c47b4d08b77f79f7ba2364bc37aa5 Mon Sep 17 00:00:00 2001 From: Paul Matthew Barrameda Date: Sun, 14 Apr 2019 22:56:20 -0700 Subject: [PATCH 080/152] Updated logic Made MessageHandler ignore whether or not the message type field is capitalized or not. Made completionCallback of Mission class return a MissionParameter, not MissionInformation. Modified stop() function of Mission class. Will need to modify naming of functions/variables in this class even more, its very unreadable. Added "stopMission" support to ipc.ts. Renamed locations.json to location.json. Added disconnect() support to Vehicle. It is not possible to disconnect the vehicle from the Orchestrator through vehicle.update(). --- src/common/MessageHandler.ts | 17 ++- src/common/missions/index.ts | 3 + src/common/struct/Mission.ts | 124 +++++++++---------- src/common/struct/Vehicle.ts | 9 ++ src/main/index.ts | 2 +- src/static/index.ts | 2 +- src/static/{locations.json => location.json} | 0 src/types/messages.ts | 4 +- src/types/tasks.ts | 0 src/util/ipc.ts | 20 ++- 10 files changed, 106 insertions(+), 75 deletions(-) create mode 100644 src/common/missions/index.ts rename src/static/{locations.json => location.json} (100%) create mode 100644 src/types/tasks.ts diff --git a/src/common/MessageHandler.ts b/src/common/MessageHandler.ts index c4c81ffa..debfce40 100644 --- a/src/common/MessageHandler.ts +++ b/src/common/MessageHandler.ts @@ -19,7 +19,7 @@ import UpdateHandler from './struct/UpdateHandler'; import xbee from './Xbee'; -export default class MessageHandler { +class MessageHandler { /** * Dictionary for all messages that are being sent, sent and acknowledged, and received. * @@ -169,6 +169,13 @@ export default class MessageHandler { return; } + /* + * The message typeguard check will let us know that there is a type field. It does not check if + * the type field is lowercased, so we will manually make it lowercased here, as all of our + * message type fields are required to be in lowercase. + */ + json.type = (json.type as string).toLowerCase(); + const jsonMessage = json as JSONMessage; // Ignore messages from unrecognized vehicles. @@ -234,7 +241,6 @@ export default class MessageHandler { /* * Stops sending the message that the ack message has acknowledged. - * * DO NOT ACKNOWLEDGE. */ if (messageTypeGuard.isAcknowledgementMessage(jsonMessage)) { @@ -263,3 +269,10 @@ export default class MessageHandler { this.updateHandler.clearHandlers(); } } + +/* + * This allows only one instance of the MessageHandler to be used. + * The Orchestrator does not need to call any functions of the MessageHandler. + * All requests are passed through an ipcRenderer notification. + */ +export default new MessageHandler(); diff --git a/src/common/missions/index.ts b/src/common/missions/index.ts new file mode 100644 index 00000000..6d916df1 --- /dev/null +++ b/src/common/missions/index.ts @@ -0,0 +1,3 @@ +export default { + +}; diff --git a/src/common/struct/Mission.ts b/src/common/struct/Mission.ts index b9486d6a..aeecc62b 100644 --- a/src/common/struct/Mission.ts +++ b/src/common/struct/Mission.ts @@ -21,7 +21,7 @@ import DictionaryList from './DictionaryList'; import Vehicle from './Vehicle'; import UpdateHandler from './UpdateHandler'; -type MissionStatus = 'ready' | 'initializing' | 'waiting' | 'running'; +export type MissionStatus = 'ready' | 'initializing' | 'waiting' | 'running'; /** * Mission backend for the GCS. Automatically creates and assigns tasks. No provided value should @@ -99,9 +99,9 @@ export default abstract class Mission { private waitingVehicles = new DictionaryList(); /** - * Callback to occur when + * Callback to occur when mission has completed. */ - private completionCallback: (information: MissionInformation) => void; + private completionCallback: (information: MissionParameters) => void; /** * Handles different states of the vehicle. @@ -109,7 +109,7 @@ export default abstract class Mission { private statusEventHandler = new UpdateHandler(); public constructor( - completionCallback: (information: MissionInformation) => void, + completionCallback: (information: MissionParameters) => void, vehicles: { [vehicleId: number]: Vehicle }, information: MissionInformation, activeVehicleMapping: { [vehicleId: number]: JobType }, @@ -167,11 +167,7 @@ export default abstract class Mission { private initialize(): void { // Stops mission if mission is not in the "ready" state. if (this.status !== 'ready') { - ipc.postLogMessages({ - type: 'failure', - message: `Something wrong happened in ${this.missionName}`, - }); - this.stop(false); + this.stop(false, `Something wrong happend in ${this.missionName}`); return; } @@ -179,7 +175,7 @@ export default abstract class Mission { if (!this.checkJobTypesAndActiveVehicles()) { ipc.postLogMessages({ type: 'failure', - message: `Provided vehicles for ${this.missionName} is incomplete. Stopping mission`, + message: `Cannot start mission: Provided vehicles for ${this.missionName} is incomplete`, }); this.statusEventHandler.event('status', 'ready'); return; @@ -198,11 +194,7 @@ export default abstract class Mission { private assignJobs(): void { // Fails to assign jobs if mission is not in the "initializing" state. if (this.status !== 'initializing') { - ipc.postLogMessages({ - type: 'failure', - message: `Something wrong happened in ${this.missionName}`, - }); - this.stop(false); + this.stop(false, `Something wrong happened in ${this.missionName}`); return; } @@ -244,13 +236,13 @@ export default abstract class Mission { /** * Triggers when vehicle fails to accept the job assignment. */ - const onDisconnect = (): void => { + const onDisconnect = (vehicleId: number): void => { /* * Stop mission if any vehicle failed to get the job. Also stops sending messages * to any other vehicle if there is somehow a "start" message on queue. */ ipc.postStopSendingMessages(); - this.stop(false); + this.stop(false, `Failed to assign job to ${(vehicleConfig.vehicleInfos[vehicleId] as VehicleInfo).name}, as it has disconnected`); }; /** @@ -258,7 +250,7 @@ export default abstract class Mission { */ const onError = (vehicleId: number, message?: string): void => { ipc.postLogMessages({ - type: 'success', + type: 'failure', message: `${(vehicleConfig.vehicleInfos[vehicleId] as VehicleInfo).name} has entered an error state in ${this.missionName}: ${message || 'No error message specified'}`, }); @@ -269,10 +261,16 @@ export default abstract class Mission { pendingVehicleIds.forEach((vehicleId): void => { const jobType = this.activeVehicleMapping[vehicleId]; - this.vehicles[vehicleId].assignJob(jobType, + const assigned = this.vehicles[vehicleId].assignJob(jobType, (): void => onSuccess(vehicleId), - (): void => onDisconnect(), + (): void => onDisconnect(vehicleId), (message): void => onError(vehicleId, message)); + + // If vehicle was not in the "ready" state when assigned. + if (!assigned) { + ipc.postStopSendingMessages(); + this.stop(false, `Failed to assign job to ${(vehicleConfig.vehicleInfos[vehicleId] as VehicleInfo).name} as it was not in a ready state`); + } }); } @@ -282,11 +280,7 @@ export default abstract class Mission { private start(): void { // Fails to initialize if mission is not in the "ready" state. if (this.status !== 'waiting') { - ipc.postLogMessages({ - type: 'failure', - message: `Something wrong happened in ${this.missionName}`, - }); - this.stop(false); + this.stop(false, `Something wrong happened in ${this.missionName}`); return; } @@ -341,7 +335,7 @@ export default abstract class Mission { const task = this.waitingTasks.shift(jobType) as Task; const vehicle = this.waitingVehicles.shift(jobType) as Vehicle; - vehicle.assignTask(task); + if (!this.assignTask(vehicle, task)) return; this.activeTasks.set(vehicle.getVehicleId(), task); } }); @@ -350,6 +344,17 @@ export default abstract class Mission { this.statusEventHandler.event('status', 'running'); } + private assignTask(vehicle: Vehicle, task: Task): boolean { + const assigned = vehicle.assignTask(task); + + if (!assigned) { + ipc.postStopSendingMessages(); + this.stop(false, `Failed to assign task to ${(vehicleConfig.vehicleInfos[vehicle.getVehicleId()] as VehicleInfo).name} as it was not in a waiting state`); + } + + return assigned; + } + /** * This will be called from the Orchestrator. The Orchestrator should make sure that the * vehicleId being provided is a valid vehicleId of a vehicle that was connected when the @@ -376,7 +381,7 @@ export default abstract class Mission { // Assigns the next task in that job to the vehicle. const newTask = this.waitingTasks.shift(jobType) as Task; - this.vehicles[message.sid].assignTask(newTask); + if (!this.assignTask(this.vehicles[message.sid], newTask)) return; this.activeTasks.set(message.sid, newTask); } else { // All tasks in that jobType are complete, so this vehicle will go back to waiting vehicles. @@ -391,16 +396,11 @@ export default abstract class Mission { * throughout the mission. */ if (this.activeTasks.size === 0 && this.waitingTasks.size() === 0) { - ipc.postCompleteMission(this.missionName); this.completionCallback(this.generateTerminatedData()); this.stop(true); } else { // This should never happen. There is a big issue in the system if this somehow happens. - ipc.postLogMessages({ - type: 'failure', - message: `Mission completion failed in ${this.missionName}. Stopping mission`, - }); - this.stop(false); + this.stop(false, `Failed to complete ${this.missionName}`); } } } @@ -418,11 +418,7 @@ export default abstract class Mission { protected addTask(jobType: string, task: Task, addToFront?: boolean): void { // Fails to initialize if mission is not in the "ready" state. if (this.status !== 'running') { - ipc.postLogMessages({ - type: 'failure', - message: `Tried to add ${task.taskType} task while ${this.missionName} is not running`, - }); - this.stop(false); + this.stop(false, `Tried to add ${task.taskType} task while ${this.missionName} is not running`); return; } @@ -432,7 +428,7 @@ export default abstract class Mission { * Assign the task right away if there is a vehicle waiting for a task for that * specific jobType. */ - vehicle.assignTask(task); + if (!this.assignTask(vehicle, task)) return; this.activeTasks.set(vehicle.getVehicleId(), task); } else if (addToFront) { // Add to front of waitingTasks if requested. @@ -461,8 +457,9 @@ export default abstract class Mission { * * @param success True if the mission was completed, false if the mission was stopped * manually/through error. + * @param error The error message (if success === false). */ - private stop(success: boolean): void { + private stop(success: boolean, error?: string): void { Object.keys(this.activeVehicleMapping).forEach((vehicleIdString): void => { const vehicleId = parseInt(vehicleIdString, 10); this.vehicles[vehicleId].stop(); @@ -470,9 +467,20 @@ export default abstract class Mission { this.statusEventHandler.event('status', 'ready'); - if (!success) { + if (success) { + ipc.postCompleteMission(this.missionName, this.generateTerminatedData()); + } else { + ipc.postStopMission(); ipc.postLogMessages({ - type: 'success', + type: 'failure', + message: `Stopped mission: ${error}`, + }, { + /* + * Put terminated data on mission stop (on fail) so that the user can start the mission + * over again with the terminated data (assume this was no the first mission that ran, + * the user should not have to start from first mission since he/she did not know the + * data for this mission). + */ message: `Terminated data: ${JSON.stringify(this.parameters)}`, }); } @@ -536,22 +544,11 @@ export default abstract class Mission { if (this.status === 'waiting') { // Simply exit the mission if we are in the "waiting" stage. - ipc.postLogMessages({ - type: 'failure', - message: 'Exiting mission due to vehicle in error state, take manual control over vehicle', - }); - this.stop(false); + this.stop(false, 'Take manual control over vehicle'); } else if (this.status === 'initializing') { - /* - * Stop sending all extra start messages and exit the mission. - * See assignJobs for similar logic. - */ - ipc.postLogMessages({ - type: 'failure', - message: 'Exiting mission due to vehicle in error state, take manual control over vehicle', - }); + // Stop sending all extra start messages and exit the mission. ipc.postStopSendingMessages(); - this.stop(false); + this.stop(false, 'Take manual control over vehicle'); } else { const jobType = this.activeVehicleMapping[vehicleId]; @@ -580,14 +577,10 @@ export default abstract class Mission { if (newVehicle && task) { this.activeVehicleMapping[newVehicle.getVehicleId()] = jobType; - newVehicle.assignTask(task); + if (!this.assignTask(newVehicle, task)) return; this.activeTasks.set(newVehicle.getVehicleId(), task); } else { - ipc.postLogMessages({ - type: 'failure', - message: 'Exiting mission due to vehicle in error state, take manual control over vehicle', - }); - this.stop(false); + this.stop(false, 'Failed to reassign job, take manual control over vehicle'); } } } @@ -598,8 +591,9 @@ export default abstract class Mission { protected abstract generateTasks(): DictionaryList; /** - * Generates data that comes out of the specific mission, either to be given to the user - * or to the next mission. The data should be formatted to support the next mission. + * Generates data after the mission has completed, either to be given to the user + * or to the next mission. For example, data produced here for an ISR Search mission should + * be data for the VTOL Search mission (of type VTOLSearchMissionParameters). */ - protected abstract generateTerminatedData(): MissionInformation; + protected abstract generateTerminatedData(): MissionParameters; } diff --git a/src/common/struct/Vehicle.ts b/src/common/struct/Vehicle.ts index add56a5c..d4251a33 100644 --- a/src/common/struct/Vehicle.ts +++ b/src/common/struct/Vehicle.ts @@ -191,6 +191,15 @@ export default class Vehicle { this.updateEventHandler.events(message); } + /** + * Sets the vehicle as disconnected by changing it status to "disconnected". + * The Orchestrator is in charge of preventing any more updates to the vehicle + * if it is disconnected. + */ + public disconnect(): void { + this.status = 'disconnected'; + } + /** * Sets lastConnectionTime to current time when function is called. Called * by the Orchestrator whenever GCS receives a message from the vehicle. diff --git a/src/main/index.ts b/src/main/index.ts index 9b7bf6ea..7b1a5142 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -360,7 +360,7 @@ const menu: MenuItemConstructorOptions[] = [ /** * Adds a list of locations on the menu to allow user to pan to specific location in the map. - * The list of locations comes from ../../resources/locations.json. + * The list of locations comes from static/location.json. */ function setLocationMenu(): void { const location = menu.find((m): boolean => m.label === 'Locations'); diff --git a/src/static/index.ts b/src/static/index.ts index 71e9ad87..c52d2636 100644 --- a/src/static/index.ts +++ b/src/static/index.ts @@ -26,7 +26,7 @@ import icon from './images/icon.png'; import pin from './images/pin.png'; import config from './config.json'; -import { startLocation as startLocationString, locations as locationsObject } from './locations.json'; +import { startLocation as startLocationString, locations as locationsObject } from './location.json'; import { vehicleIds as vehicleIdsObject, vehicleInfos as vehicleInfosObject, diff --git a/src/static/locations.json b/src/static/location.json similarity index 100% rename from src/static/locations.json rename to src/static/location.json diff --git a/src/types/messages.ts b/src/types/messages.ts index 8fb0f9ab..72dd5c71 100644 --- a/src/types/messages.ts +++ b/src/types/messages.ts @@ -657,7 +657,7 @@ export type Message = StartMessage | AddMissionMessage | PauseMessage | ResumeMe */ // eslint-disable-next-line @typescript-eslint/no-explicit-any export function isMessage(message: { [key: string]: any }): boolean { - return message.type && [ + return message.type && typeof message.type === 'string' && [ 'start', 'addMission', 'pause', @@ -670,7 +670,7 @@ export function isMessage(message: { [key: string]: any }): boolean { 'connect', 'ack', 'badMessage', - ].includes(message.type); + ].includes(message.type.toLowerCase()); } export type JSONMessage = Message & { diff --git a/src/types/tasks.ts b/src/types/tasks.ts new file mode 100644 index 00000000..e69de29b diff --git a/src/util/ipc.ts b/src/util/ipc.ts index f5131e5d..bf832756 100644 --- a/src/util/ipc.ts +++ b/src/util/ipc.ts @@ -16,7 +16,7 @@ import { BrowserWindow, ipcRenderer } from 'electron'; -import { JSONMessage, Message } from '../types/messages'; +import { JSONMessage, Message, MissionParameters } from '../types/messages'; import { FileLoadOptions, FileSaveOptions, @@ -39,9 +39,10 @@ function postCenterMapToVehicle(vehicle: VehicleObject): void { * Post "competeMission" notification. * * Files that take this notification: + * - common/Orchestrator */ -function postCompleteMission(missionName: string): void { - ipcRenderer.send('post', 'completeMission', missionName); +function postCompleteMission(missionName: string, data: MissionParameters): void { + ipcRenderer.send('post', 'completeMission', missionName, data); } /** @@ -144,7 +145,7 @@ function postLoadConfig( } /** - * Post "logMessages" notification. + * Post "logMessages" notification. Please never end the messages to log with a period. * * Files that take this notification: * - renderer/mainWindow/log/LogContainer @@ -220,6 +221,16 @@ function postShowMissionWindow(): void { ipcRenderer.send('post', 'showMissionWindow'); } +/** + * Post "stopMission" notification. + * + * Files that take this notification: + * - common/Orchestrator + */ +function postStopMission(): void { + ipcRenderer.send('post', 'stopMission'); +} + /** * Post "stopSendingMessages" notification. * @@ -288,6 +299,7 @@ export default { postSendMessage, postSetMapToUserLocation, postShowMissionWindow, + postStopMission, postStopSendingMessages, postToggleTheme, postUpdateMapLocation, From ed24d786ac10d13f84ae331a435a43ec88fabee8 Mon Sep 17 00:00:00 2001 From: Paul Matthew Barrameda Date: Mon, 15 Apr 2019 03:03:26 -0700 Subject: [PATCH 081/152] Added Orchestrator and fixed logic Fixed logic for acknowledgement messages in MessageHandler. Fixed logic for stopping sending messages in MessageHandler. Simplified logic in Mission to log parameters whenever a mission completes. Changed lastConnectionTime to update to the time field in the messages. Added more typings/functions. --- src/common/MessageHandler.ts | 61 +++++++++++++----------- src/common/Xbee.ts | 2 - src/common/missions/index.ts | 4 +- src/common/struct/Mission.ts | 53 +++++++++++++-------- src/common/struct/UpdateHandler.ts | 7 +++ src/common/struct/Vehicle.ts | 20 ++++---- src/types/messages.ts | 7 +++ src/util/ipc.ts | 75 +++++++++++++++++++++++------- 8 files changed, 149 insertions(+), 80 deletions(-) diff --git a/src/common/MessageHandler.ts b/src/common/MessageHandler.ts index debfce40..b410e435 100644 --- a/src/common/MessageHandler.ts +++ b/src/common/MessageHandler.ts @@ -1,5 +1,3 @@ -// Do not import this file except in the Orchestrator class (common/Orchestrator). - import { Event, ipcRenderer } from 'electron'; import { config, vehicleConfig } from '../static/index'; @@ -85,7 +83,8 @@ class MessageHandler { */ this.messageDictionary.push('sent', jsonMessage); - if (jsonMessage.type === 'ack' || jsonMessage.type === 'badMessage') { + if (messageTypeGuard.isAcknowledgementMessage(message) + || messageTypeGuard.isBadMessage(message)) { /* * Will only send these types of messages once, but everything else will be sent * continuously until acknowleged. @@ -102,7 +101,8 @@ class MessageHandler { * the interval). */ const expiry = setInterval( - (): void => xbee.sendMessage(jsonMessage), config.messageSendRate * 1000, + (): void => xbee.sendMessage(jsonMessage), + config.messageSendRate * 1000, ); /* @@ -122,8 +122,9 @@ class MessageHandler { }, { time: config.vehicleDisconnectionTime * 1000, callback: (): void => { + clearInterval(expiry); this.removeMessage('outbox', jsonMessage.id); - ipc.postDeactivateVehicle(vehicleId); + ipc.postDisconnectFromVehicle(vehicleId); }, }); } @@ -181,6 +182,22 @@ class MessageHandler { // Ignore messages from unrecognized vehicles. if (!vehicleConfig.isValidVehicleId(jsonMessage.sid)) return; + /* + * Stops sending the message that the ack message has acknowledged. + * Put this before the if statement below since want to acknowledge + * every message of the same ID afterwards, but for all other messages + * we only want to handle it once. + * DO NOT ACKNOWLEDGE. + */ + if (messageTypeGuard.isAcknowledgementMessage(jsonMessage)) { + const { ackid } = jsonMessage as AcknowledgementMessage; + + const hash = `${jsonMessage.sid}#${ackid}`; + this.updateHandler.event(hash, true); + + ipc.postHandleAcknowledgementMessage(jsonMessage); + } + /* * Update the last message id received from the vehicle or discard message completely. * This is to be able to properly acknowledge a message. See the note on the following: @@ -207,7 +224,7 @@ class MessageHandler { */ if (messageTypeGuard.isConnectMessage(jsonMessage)) { - ipc.postHandleConnectMessage(jsonMessage); + ipc.postConnectToVehicle(jsonMessage); return; } @@ -229,44 +246,32 @@ class MessageHandler { // DO NOT ACKNOWLEDGE. if (messageTypeGuard.isBadMessage(jsonMessage)) { ipc.postHandleBadMessage(jsonMessage); - /* - * ipc.postLogMessages({ - * type: 'failure', - * message: `Received bad message from ${name}: ${error || 'No error message - * was specified'}`, - * }); - */ return; } /* - * Stops sending the message that the ack message has acknowledged. - * DO NOT ACKNOWLEDGE. + * The first acknowledgement gets passed through all other if statements so + * we stop it here */ - if (messageTypeGuard.isAcknowledgementMessage(jsonMessage)) { - const { ackid } = jsonMessage as AcknowledgementMessage; - - const hash = `${jsonMessage.sid}#${ackid}`; - this.updateHandler.event(hash, true); - - ipc.postHandleAcknowledgementMessage(jsonMessage); - return; - } + if (messageTypeGuard.isAcknowledgementMessage(jsonMessage)) return; /* * Any message that reaches here is a message (has a valid type, id, sid, tid, and time) * but does not have valid properties, or is a message GCS should not receive from vehicles * (e.g. GCS should not receive a "start" message from a vehicle). */ - this.sendBadMessage(jsonMessage, `Message of type ${jsonMessage.type} is invalid`); + this.sendBadMessage(jsonMessage, `Message of type ${jsonMessage.type} is invalid or is not acceptable by GCS`); } /** - * Stops sending all messages (literally discards all messages that are being sent, but they - * will still be in the "sent" dictionary). + * Stops sending all messages (literally discards all messages from the "outbox" list, but they + * will still be in the "sent" list). */ private stopSendingMessages(): void { - this.updateHandler.clearHandlers(); + // Run the clearInterval() of each handler. + this.updateHandler.getEvents().forEach((hash): void => { + this.updateHandler.event(hash, true); + }); } } diff --git a/src/common/Xbee.ts b/src/common/Xbee.ts index 46d65461..c6a32d69 100644 --- a/src/common/Xbee.ts +++ b/src/common/Xbee.ts @@ -1,5 +1,3 @@ -// Do not interact with this file outside the MissionHandler class. - import SerialPort from 'serialport'; import { constants as C, Frame, XBeeAPI } from 'xbee-api'; diff --git a/src/common/missions/index.ts b/src/common/missions/index.ts index 6d916df1..ff8b4c56 100644 --- a/src/common/missions/index.ts +++ b/src/common/missions/index.ts @@ -1,3 +1 @@ -export default { - -}; +export default {}; diff --git a/src/common/struct/Mission.ts b/src/common/struct/Mission.ts index aeecc62b..6ff976b4 100644 --- a/src/common/struct/Mission.ts +++ b/src/common/struct/Mission.ts @@ -11,7 +11,6 @@ import { MissionOptions, MissionParameters, Task, - UpdateMessage, } from '../../types/messages'; import ipc from '../../util/ipc'; @@ -101,7 +100,7 @@ export default abstract class Mission { /** * Callback to occur when mission has completed. */ - private completionCallback: (information: MissionParameters) => void; + private completionCallback: (parameters: MissionParameters) => void; /** * Handles different states of the vehicle. @@ -109,7 +108,7 @@ export default abstract class Mission { private statusEventHandler = new UpdateHandler(); public constructor( - completionCallback: (information: MissionParameters) => void, + completionCallback: (parameters: MissionParameters) => void, vehicles: { [vehicleId: number]: Vehicle }, information: MissionInformation, activeVehicleMapping: { [vehicleId: number]: JobType }, @@ -160,6 +159,14 @@ export default abstract class Mission { this.initialize(); } + public getStatus(): MissionStatus { + return this.status; + } + + public getVehicles(): { [vehicleId: number]: Vehicle } { + return this.vehicles; + } + /** * Initializes the mission. This is run first, and will run assignJobs if run successfully. * Will set status to "initlializing" after running successfully. @@ -362,7 +369,7 @@ export default abstract class Mission { * * Subclasses can override this to check for even more types of messages. */ - public update(message: JSONMessage): void { + public update(jsonMessage: JSONMessage): void { /* * The base mission class only checks for update and completion messages. The base class will * either reassign the vehicle the task, put the vehicle to wait for further tasks, @@ -370,23 +377,23 @@ export default abstract class Mission { * is to simply keep its track of vehicles up to date. */ - if (messageTypeGuard.isUpdateMessage(message)) { - this.vehicles[message.sid].update(message as UpdateMessage); + if (messageTypeGuard.isUpdateMessage(jsonMessage)) { + this.vehicles[jsonMessage.sid].update(jsonMessage); } - if (messageTypeGuard.isCompleteMessage(message)) { - const jobType = this.activeVehicleMapping[message.sid]; + if (messageTypeGuard.isCompleteMessage(jsonMessage)) { + const jobType = this.activeVehicleMapping[jsonMessage.sid]; if ((this.waitingTasks.get(jobType) as Task[]).length > 0) { // Assigns the next task in that job to the vehicle. const newTask = this.waitingTasks.shift(jobType) as Task; - if (!this.assignTask(this.vehicles[message.sid], newTask)) return; - this.activeTasks.set(message.sid, newTask); + if (!this.assignTask(this.vehicles[jsonMessage.sid], newTask)) return; + this.activeTasks.set(jsonMessage.sid, newTask); } else { // All tasks in that jobType are complete, so this vehicle will go back to waiting vehicles. - this.waitingVehicles.push(jobType, this.vehicles[message.sid]); - this.activeTasks.delete(message.sid); + this.waitingVehicles.push(jobType, this.vehicles[jsonMessage.sid]); + this.activeTasks.delete(jsonMessage.sid); } /* @@ -396,7 +403,7 @@ export default abstract class Mission { * throughout the mission. */ if (this.activeTasks.size === 0 && this.waitingTasks.size() === 0) { - this.completionCallback(this.generateTerminatedData()); + this.completionCallback(this.generateCompletionParameters()); this.stop(true); } else { // This should never happen. There is a big issue in the system if this somehow happens. @@ -468,7 +475,12 @@ export default abstract class Mission { this.statusEventHandler.event('status', 'ready'); if (success) { - ipc.postCompleteMission(this.missionName, this.generateTerminatedData()); + const completionParameters = this.generateCompletionParameters(); + ipc.postCompleteMission(this.missionName, completionParameters); + ipc.postLogMessages({ + type: 'success', + message: `Completion parameters for ${this.missionName}: ${JSON.stringify(completionParameters)}`, + }); } else { ipc.postStopMission(); ipc.postLogMessages({ @@ -476,12 +488,13 @@ export default abstract class Mission { message: `Stopped mission: ${error}`, }, { /* - * Put terminated data on mission stop (on fail) so that the user can start the mission + * Put terminated parameters on stop (on fail) so that the user can start the mission * over again with the terminated data (assume this was no the first mission that ran, * the user should not have to start from first mission since he/she did not know the - * data for this mission). + * parameters for this mission). This can also apply to the first mission, cause the user + * might not have written the parameters in paper. */ - message: `Terminated data: ${JSON.stringify(this.parameters)}`, + message: `Terminated parameters for ${this.missionName}: ${JSON.stringify(this.parameters)}`, }); } } @@ -591,9 +604,9 @@ export default abstract class Mission { protected abstract generateTasks(): DictionaryList; /** - * Generates data after the mission has completed, either to be given to the user - * or to the next mission. For example, data produced here for an ISR Search mission should + * Generates new mission parameters after the mission has completed, either to be given to the + * user or to the next mission. For example, data produced here for an ISR Search mission should * be data for the VTOL Search mission (of type VTOLSearchMissionParameters). */ - protected abstract generateTerminatedData(): MissionParameters; + protected abstract generateCompletionParameters(): MissionParameters; } diff --git a/src/common/struct/UpdateHandler.ts b/src/common/struct/UpdateHandler.ts index c6cd4226..3a61e468 100644 --- a/src/common/struct/UpdateHandler.ts +++ b/src/common/struct/UpdateHandler.ts @@ -46,6 +46,13 @@ interface Handler { export default class UpdateHandler { private eventDictionary: { [key: string]: Handler[] | undefined } = {}; + /** + * Gets list of events in the update handler. + */ + public getEvents(): string[] { + return Object.keys(this.eventDictionary); + } + /** * Adds a new handler. * diff --git a/src/common/struct/Vehicle.ts b/src/common/struct/Vehicle.ts index d4251a33..e942f7df 100644 --- a/src/common/struct/Vehicle.ts +++ b/src/common/struct/Vehicle.ts @@ -1,6 +1,7 @@ import { JobType, vehicleConfig } from '../../static/index'; import { + JSONMessage, Message, StartMessage, Task, @@ -20,7 +21,6 @@ type ErrorCallback = (message?: string) => void; export interface VehicleOptions { sid: number; jobs: JobType[]; - status?: VehicleStatus; } /** @@ -39,7 +39,7 @@ export default class Vehicle { /** * Current assigned job. */ - private assignedJob: JobType | undefined; + private assignedJob: JobType | '' = ''; /** * Current status of the vehicle. @@ -96,8 +96,6 @@ export default class Vehicle { this.vehicleId = options.sid; this.jobs = options.jobs; - if (options.status) this.status = options.status; - // We never want these handlers to disappear so always return false on their callback functions. this.updateEventHandler.addHandler('status', (status, message): boolean => { @@ -184,11 +182,15 @@ export default class Vehicle { /** * Updates all variables in this vehicle to the variables in the message. Called * by the Orchestrator when the GCS receives an update message from the vehicle. + * Also updates the lastConnectionTime accordingly. * * @param message The message from the vehicle itself. */ - public update(message: UpdateMessage): void { - this.updateEventHandler.events(message); + public update(jsonMessage: JSONMessage): void { + const updateMessage = jsonMessage as UpdateMessage; + + this.lastConnectionTime = jsonMessage.time; + this.updateEventHandler.events(updateMessage); } /** @@ -204,8 +206,8 @@ export default class Vehicle { * Sets lastConnectionTime to current time when function is called. Called * by the Orchestrator whenever GCS receives a message from the vehicle. */ - public updateLastConnectionTime(): void { - this.lastConnectionTime = Date.now(); + public updateLastConnectionTime(jsonMessage: JSONMessage): void { + this.lastConnectionTime = jsonMessage.time; } /** @@ -307,6 +309,6 @@ export default class Vehicle { type: 'stop', }); - this.assignedJob = undefined; + this.assignedJob = ''; } } diff --git a/src/types/messages.ts b/src/types/messages.ts index 72dd5c71..e0b55775 100644 --- a/src/types/messages.ts +++ b/src/types/messages.ts @@ -421,6 +421,13 @@ export interface MissionInformation { options: MissionOptions; } +export type MissionName = 'isrSearch' | 'vtolSearch' | 'payloadDrop' | 'ugvRetrieve' | 'uuvRetrieve'; + +export interface MissionDescription { + missionName: MissionName; + information: MissionInformation; +} + interface MessageBase { /** * Type of message. diff --git a/src/util/ipc.ts b/src/util/ipc.ts index bf832756..6ec773c9 100644 --- a/src/util/ipc.ts +++ b/src/util/ipc.ts @@ -16,7 +16,12 @@ import { BrowserWindow, ipcRenderer } from 'electron'; -import { JSONMessage, Message, MissionParameters } from '../types/messages'; +import { + JSONMessage, + Message, + MissionDescription, + MissionParameters, +} from '../types/messages'; import { FileLoadOptions, FileSaveOptions, @@ -41,18 +46,47 @@ function postCenterMapToVehicle(vehicle: VehicleObject): void { * Files that take this notification: * - common/Orchestrator */ -function postCompleteMission(missionName: string, data: MissionParameters): void { - ipcRenderer.send('post', 'completeMission', missionName, data); +function postCompleteMission(missionName: string, completionParameters: MissionParameters): void { + ipcRenderer.send('post', 'completeMission', missionName, completionParameters); +} + +/** + * Post "confirmCompleteMission" notification. + * + * Files that take this notification: + */ +function postConfirmCompleteMission(): void { + ipcRenderer.send('post', 'confirmCompleteMission'); +} + +/** + * Post "connectToVehicle" notification. + * + * Files that take this notification: + * - common/Orchestrator + */ +function postConnectToVehicle(jsonMessage: JSONMessage): void { + ipcRenderer.send('post', 'connectToVehicle', jsonMessage); } /** - * Post "deactivateVehicle" notification. + * Post "disconnectFromVehicle" notification. * * Files that take this notification: * - common/Orchestrator */ -function postDeactivateVehicle(vehicleId: number): void { - ipcRenderer.send('post', 'deactivateVehicle', vehicleId); +function postDisconnectFromVehicle(vehicleId: number): void { + ipcRenderer.send('post', 'disconnectFromVehicle', vehicleId); +} + +/** + * Post "finishMissions" notification. This is different from the "completeMission" + * notification in that this is sent once all missions to run are completed. + * + * Files that take this notification: + */ +function postFinishMissions(completionParameters: MissionParameters): void { + ipcRenderer.send('post', 'finishMissions', completionParameters); } /** @@ -85,16 +119,6 @@ function postHandleCompleteMessage(jsonMessage: JSONMessage): void { ipcRenderer.send('post', 'handleCompleteMessage', jsonMessage); } -/** - * Post "handleConnectMessage" notification. - * - * Files that take this notification: - * - common/Orchestrator - */ -function postHandleConnectMessage(jsonMessage: JSONMessage): void { - ipcRenderer.send('post', 'handleConnectMessage', jsonMessage); -} - /** * Post "handlePOIMessage" notification. * @@ -221,6 +245,18 @@ function postShowMissionWindow(): void { ipcRenderer.send('post', 'showMissionWindow'); } +/** + * Post "startMissions" notification. + * + * Files that take this notification: + * - common/Orchestrator + */ +function postStartMissions( + missions: MissionDescription[], +): void { + ipcRenderer.send('post', 'startMissions', missions); +} + /** * Post "stopMission" notification. * @@ -284,14 +320,16 @@ function postUpdateVehicles(...vehicles: VehicleObject[]): void { export default { postCenterMapToVehicle, postCompleteMission, + postConfirmCompleteMission, + postConnectToVehicle, + postDisconnectFromVehicle, + postFinishMissions, postHandleAcknowledgementMessage, postHandleBadMessage, postHandleCompleteMessage, - postHandleConnectMessage, postHandlePOIMessage, postHandleUpdateMessage, postHideMissionWindow, - postDeactivateVehicle, postLoadConfig, postLogMessages, postReceiveMessage, @@ -299,6 +337,7 @@ export default { postSendMessage, postSetMapToUserLocation, postShowMissionWindow, + postStartMissions, postStopMission, postStopSendingMessages, postToggleTheme, From 73f21a0c6ba3258756bd34bbcfcd1447c301362c Mon Sep 17 00:00:00 2001 From: Paul Matthew Barrameda Date: Mon, 15 Apr 2019 03:05:40 -0700 Subject: [PATCH 082/152] Forgot to include Orchestrator into last commit Will add the original authors to this commit Co-Authored-By: Maxence Weyrich Co-Authored-By: Kedwin Chen --- src/common/Orchestrator.ts | 263 ++++++++++++++++++++++++++++++++++++- 1 file changed, 262 insertions(+), 1 deletion(-) diff --git a/src/common/Orchestrator.ts b/src/common/Orchestrator.ts index 931fbe4f..439ca57d 100644 --- a/src/common/Orchestrator.ts +++ b/src/common/Orchestrator.ts @@ -1,3 +1,264 @@ -// Do not import this class except in the index renderer file (renderer/index). +import { ipcRenderer, Event } from 'electron'; + +import { config, vehicleConfig, VehicleInfo } from '../static/index'; + +import { + BadMessage, + ConnectMessage, + JSONMessage, + MissionDescription, + MissionParameters, +} from '../types/messages'; + +import ipc from '../util/ipc'; + +import Mission from './struct/Mission'; +import UpdateHandler from './struct/UpdateHandler'; +import Vehicle from './struct/Vehicle'; import './MessageHandler'; + +class Orchestrator { + /** + * Acknowledges a message. All messages are passed here through the MessageHandler. + * Only messages that are no acknowledged are bad messages and acknowledgements. + */ + private static acknowledge(jsonMessage: JSONMessage): void { + ipc.postSendMessage(jsonMessage.sid, { + type: 'ack', + ackid: jsonMessage.id, + }); + } + + /** + * List of mission names and information for each mission. + */ + private missions: MissionDescription[] = []; + + /** + * Current index of missions that mission is performing. + * Value is -1 if no mission is being performed. + */ + private currentMissionIndex = -1; + + /** + * True if a mission is running, false otherwise. + */ + private running = false; + + /** + * True if user wants confirmation between missions, false otherwise. + */ + private requireConfirmation = false; + + /** + * Current Mission being performed. Value is null + * if no mission is being performed. + */ + private currentMission: Mission | null = null; + + private vehicles: { [vehicleId: number]: Vehicle } = {}; + + /** + * Keeps track of the vehicle and ensures it is connected. + */ + private vehiclePinger = new UpdateHandler(); + + public constructor() { + ipcRenderer.on('connectToVehicle', (_: Event, jsonMessage: JSONMessage): void => this.connectToVehicle(jsonMessage)); + ipcRenderer.on('disconnectFromVehicle', (_: Event, vehicleId: number): void => this.disconnectFromVehicle(vehicleId)); + + ipcRenderer.on('handleAcknowledgementMessage', (_: Event, jsonMessage: JSONMessage): void => this.handleAcknowledgementMessage(jsonMessage)); + ipcRenderer.on('handleBadMessage', (_: Event, jsonMessage: JSONMessage): void => this.handleBadMessage(jsonMessage)); + ipcRenderer.on('handleUpdateMessage', (_: Event, jsonMessage: JSONMessage): void => this.handleUpdateMessage(jsonMessage)); + ipcRenderer.on('handlePOIMessage', (_: Event, jsonMessage: JSONMessage): void => this.handlePOIMessage(jsonMessage)); + ipcRenderer.on('handleCompleteMessage', (_: Event, jsonMessage: JSONMessage): void => this.handleCompleteMessage(jsonMessage)); + + ipcRenderer.on('startMissions', (_: Event, missions: MissionDescription[], requireConfirmation: boolean): void => this.startMissions(missions, requireConfirmation)); + ipcRenderer.on('completeMission', (_: Event, missionName: string, completionParameters: MissionParameters): void => this.completeMission(missionName, completionParameters)); + ipcRenderer.on('stopMission', this.stopMission); + } + + /** + * Connects to vehicle specified by the connect message. + * @param jsonMessage The connect message. + */ + private connectToVehicle(jsonMessage: JSONMessage): void { + const connectMessage = jsonMessage as ConnectMessage; + + this.vehicles[jsonMessage.sid] = new Vehicle({ + sid: jsonMessage.sid, + jobs: connectMessage.jobsAvailable, + }); + } + + /** + * Checks if the vehicle is still connected. Does not send a message to the vehicle, + * simply checks the last time the vehicle has connected with the GCS and uses that. + * @param vehicle Vehicle to "ping". + */ + private ping(vehicle: Vehicle): void { + const delta = Date.now() - vehicle.getLastConnectionTime(); + if (delta >= 0 && delta <= config.vehicleDisconnectionTime) { + // Handler that expires and creates itself everytime it "pings" the vehicle. + this.vehiclePinger.addHandler(`${vehicle.getVehicleId()}`, (): boolean => false, { + callback: (): void => this.ping(vehicle), + time: delta, + }); + } else { + this.disconnectFromVehicle(vehicle.getVehicleId()); + } + } + + /** + * Disconnects from a vehicle (if it does not communicate recently enough). + * @param vehicleId ID of vehicle to disconnect from. + */ + private disconnectFromVehicle(vehicleId: number): void { + // Just in case, the statement should never pass. + if (!this.vehicles[vehicleId]) return; + this.vehicles[vehicleId].disconnect(); + + ipc.postUpdateVehicles(this.vehicles[vehicleId].toPlainObject()); + } + + private handleAcknowledgementMessage(jsonMessage: JSONMessage): void { + if (!this.vehicles[jsonMessage.sid] || this.vehicles[jsonMessage.sid].getStatus() === 'disconnected') return; + this.vehicles[jsonMessage.sid].updateLastConnectionTime(jsonMessage); + } + + private handleBadMessage(jsonMessage: JSONMessage): void { + if (!this.vehicles[jsonMessage.sid] || this.vehicles[jsonMessage.sid].getStatus() === 'disconnected') return; + this.vehicles[jsonMessage.sid].updateLastConnectionTime(jsonMessage); + + const badMessage = jsonMessage as BadMessage; + ipc.postLogMessages({ + type: 'failure', + message: `Received bad message from ${(vehicleConfig.vehicleInfos[jsonMessage.sid] as VehicleInfo).name}: ${badMessage.error || 'No error message specified'}}`, + }); + } + + private handleUpdateMessage(jsonMessage: JSONMessage): void { + if (!this.vehicles[jsonMessage.sid] || this.vehicles[jsonMessage.sid].getStatus() === 'disconnected') return; + + this.vehicles[jsonMessage.sid].update(jsonMessage); + if (this.currentMission) this.currentMission.update(jsonMessage); + Orchestrator.acknowledge(jsonMessage); + + ipc.postUpdateVehicles(this.vehicles[jsonMessage.sid].toPlainObject()); + } + + private handlePOIMessage(jsonMessage: JSONMessage): void { + if (!this.vehicles[jsonMessage.sid] || this.vehicles[jsonMessage.sid].getStatus() === 'disconnected') return; + + if (this.currentMission) { + this.currentMission.update(jsonMessage); + } + Orchestrator.acknowledge(jsonMessage); + } + + private handleCompleteMessage(jsonMessage: JSONMessage): void { + if (!this.vehicles[jsonMessage.sid] || this.vehicles[jsonMessage.sid].getStatus() === 'disconnected') return; + + if (!this.currentMission) { + ipc.postLogMessages({ + type: 'failure', + message: `Received complete message from ${vehicleConfig.vehicleInfos[jsonMessage.sid]} while no mission was running`, + }); + return; + } + + this.currentMission.update(jsonMessage); + Orchestrator.acknowledge(jsonMessage); + } + + /** + * Checks if all missionName fields in each mission is valid, then runs the FIRST mission of + * the provided missions. + * + * @param missions Description for all missions. Includes name of mission and information + * required for mission. + */ + private startMissions(missions: MissionDescription[], requireConfirmation: boolean): void { + if (this.running) { + ipc.postLogMessages({ + type: 'failure', + message: 'Cannot start new missions while missions are already running', + }); + return; + } + + this.running = true; + this.missions = missions; + this.requireConfirmation = requireConfirmation; + + // TODO: Set current mission to start. No code yet since those missions arent implemented. + this.currentMissionIndex = 0; + } + + /** + * Happens when a mission completes. Either starts the next mission automatically, + * ask user for confirmation before starting next mission, or ends missions. + * + * @param missionName Name of mission that has completed. Used for ensuring missions are + * being kept track of. + * @param completionParameters Paramters for next mission. + */ + private completeMission(missionName: string, completionParameters: MissionParameters): void { + /* + * The following should not happen. Only happens if the currentMissionIndex somehow + * does not match up with the mission that just finished, or if no mission was running for + * some reason. + */ + if (!this.running || (this.currentMissionIndex >= 0 + && this.missions[this.currentMissionIndex].missionName !== missionName)) { + ipc.postLogMessages({ + type: 'failure', + message: 'Received invalid completion for a mission', + }); + return; + } + + if (this.currentMissionIndex === this.missions.length - 1) { + this.stopMission(); + ipc.postFinishMissions(completionParameters); + } else if (this.requireConfirmation) { + ipc.postConfirmCompleteMission(); + } else { + this.startNextMission(); + } + } + + /** + * Starts next mission. + */ + private startNextMission(): void { + if (!this.running) { + ipc.postLogMessages({ + type: 'failure', + message: 'Cannot start next mission while no missions are running', + }); + return; + } + + this.currentMissionIndex += 1; + // TODO: Set next mission to start. No code yet since those missions arent implemented. + } + + /** + * Clears all missions in Orchestrator. + */ + private stopMission(): void { + this.currentMissionIndex = -1; + this.currentMission = null; + this.missions = []; + this.running = false; + this.requireConfirmation = false; + } +} + +/* + * This allows only one instance of the Orchestrator to be used. + * All requests are passed through an ipcRenderer notification. + */ +export default new Orchestrator(); From 694bdd90d5de097b7b6fde95f59e6e0280ecb72d Mon Sep 17 00:00:00 2001 From: Paul Matthew Barrameda Date: Mon, 15 Apr 2019 04:15:22 -0700 Subject: [PATCH 083/152] Added ISRSearch mission Really need to divide the message.ts file to different files. Will add the rest of the files tomorrow. Co-Authored-By: Maxence Weyrich Co-Authored-By: Kedwin Chen --- src/common/missions/ISRSearch.ts | 136 +++++++++++++++++++++++++++++++ src/common/struct/Mission.ts | 25 ++++-- src/types/messages.ts | 28 +++++++ 3 files changed, 181 insertions(+), 8 deletions(-) create mode 100644 src/common/missions/ISRSearch.ts diff --git a/src/common/missions/ISRSearch.ts b/src/common/missions/ISRSearch.ts new file mode 100644 index 00000000..b870c183 --- /dev/null +++ b/src/common/missions/ISRSearch.ts @@ -0,0 +1,136 @@ +import DictionaryList from '../struct/DictionaryList'; +import Mission from '../struct/Mission'; + +import { JobType } from '../../static/index'; + +import { + ISRSearchMissionParameters, + JSONMessage, + messageTypeGuard, + missionParametersTypeGuard, + MissionName, + POIMessage, + Task, + toFloat, + toHexFloat, + VTOLSearchMissionParameters, +} from '../../types/messages'; +import { LatLngZoom } from '../../types/types'; + +import ipc from '../../util/ipc'; + +export const missionName: MissionName = 'isrSearch'; + +export const jobTypes: JobType[] = ['isrSearch']; + +class ISRSearch extends Mission { + protected missionName = missionName; + + protected jobTypes = new Set(jobTypes); + + protected addTaskCompare = {}; + + /** + * List of point of interests to get parameters for next mission (VTOL Search). + */ + private missionData: LatLngZoom[] = []; + + protected generateTasks(): DictionaryList { + // Check if options and parameters line up. + if (!this.options.isrSearch + || missionParametersTypeGuard.isISRSearchMissionParameters(this.parameters)) { + throw new Error('Failed to generate tasks as options/parameters were invalid'); + } + const missionParameters = this.parameters as ISRSearchMissionParameters; + + const tasks = new DictionaryList(); + + // Add takeoff to our tasks. + if (!this.options.isrSearch.noTakeoff) { + tasks.push('isrSearch', { + taskType: 'takeoff', + ...missionParameters.takeoff, + }); + } + + tasks.push('isrSearch', { + taskType: 'isrSearch', + ...missionParameters.isrSearch, + }); + + if (!this.options.isrSearch.noLand) { + tasks.push('isrSearch', { + taskType: 'land', + ...missionParameters.land, + }); + } + + return tasks; + } + + protected generateCompletionParameters(): VTOLSearchMissionParameters | undefined { + // Will be changed depending on what VTOL wants from UAV. + if (this.missionData.length === 0) { + ipc.postLogMessages({ + type: 'failure', + message: 'No points of interests were found in the mission', + }); + return undefined; + } + + let bottom: number = Number.MIN_VALUE; + let top: number = Number.MAX_VALUE; + + let left: number = Number.MIN_VALUE; + let right: number = Number.MAX_VALUE; + + this.missionData.forEach((data): void => { + if (!bottom || data.lat < bottom) bottom = data.lat; + if (!top || data.lat > top) top = data.lat; + + if (!left || data.lng < left) left = data.lng; + if (!right || data.lng > right) right = data.lng; + }); + + return { + quickScan: { + waypoints: [ + { + lat: toHexFloat(top), + lng: toHexFloat(left), + }, + { + lat: toHexFloat(top), + lng: toHexFloat(right), + }, + { + lat: toHexFloat(bottom), + lng: toHexFloat(left), + }, + { + lat: toHexFloat(bottom), + lng: toHexFloat(right), + }, + ], + }, + }; + } + + public update(jsonMessage: JSONMessage): void { + super.update(jsonMessage); + + if (messageTypeGuard.isPOIMessage(jsonMessage)) { + const poiMessage = jsonMessage as POIMessage; + this.missionData.push({ + lat: toFloat(poiMessage.lat), + lng: toFloat(poiMessage.lng), + }); + } + } +} + +export default { + missionName, + jobTypes, + Mission: ISRSearch, +}; diff --git a/src/common/struct/Mission.ts b/src/common/struct/Mission.ts index 6ff976b4..c680f228 100644 --- a/src/common/struct/Mission.ts +++ b/src/common/struct/Mission.ts @@ -8,6 +8,7 @@ import { JSONMessage, messageTypeGuard, MissionInformation, + MissionName, MissionOptions, MissionParameters, Task, @@ -22,6 +23,8 @@ import UpdateHandler from './UpdateHandler'; export type MissionStatus = 'ready' | 'initializing' | 'waiting' | 'running'; +export type CompareTaskFunction = (a: Task, b: Task) => number; + /** * Mission backend for the GCS. Automatically creates and assigns tasks. No provided value should * be undefined (specifically the vehicle mapping). @@ -30,7 +33,7 @@ export default abstract class Mission { /** * Name of mission. */ - protected abstract missionName: string; + protected abstract missionName: MissionName; /** * Related job types to the mission. @@ -76,7 +79,8 @@ export default abstract class Mission { * comparison function specified for the jobType, then jobs will be added and taken * on a FILO order (queue). */ - protected abstract addTaskCompare: { [jobType: string]: (a: Task, b: Task) => number }; + protected abstract addTaskCompare: + { [jobType: string]: CompareTaskFunction | undefined }; /** * Map of the id of the vehicle to the task it is performing. @@ -100,7 +104,7 @@ export default abstract class Mission { /** * Callback to occur when mission has completed. */ - private completionCallback: (parameters: MissionParameters) => void; + private completionCallback: (parameters: MissionParameters | undefined) => void; /** * Handles different states of the vehicle. @@ -108,7 +112,7 @@ export default abstract class Mission { private statusEventHandler = new UpdateHandler(); public constructor( - completionCallback: (parameters: MissionParameters) => void, + completionCallback: (parameters: MissionParameters | undefined) => void, vehicles: { [vehicleId: number]: Vehicle }, information: MissionInformation, activeVehicleMapping: { [vehicleId: number]: JobType }, @@ -301,7 +305,7 @@ export default abstract class Mission { const index = searchIndex( this.waitingTasks.get(jobType) || [], task, - this.addTaskCompare[jobType], + this.addTaskCompare[jobType] as CompareTaskFunction, ); this.waitingTasks.insert(jobType, index, task); }); @@ -448,7 +452,7 @@ export default abstract class Mission { const index = searchIndex( this.waitingTasks.get(jobType) as Task[], task, - this.addTaskCompare[jobType], + this.addTaskCompare[jobType] as CompareTaskFunction, ); this.waitingTasks.insert(jobType, index, task); } else { @@ -476,7 +480,12 @@ export default abstract class Mission { if (success) { const completionParameters = this.generateCompletionParameters(); - ipc.postCompleteMission(this.missionName, completionParameters); + if (!completionParameters) { + this.stop(false, 'No parameters were generated from this mission'); + return; + } + + ipc.postCompleteMission(this.missionName, completionParameters as MissionParameters); ipc.postLogMessages({ type: 'success', message: `Completion parameters for ${this.missionName}: ${JSON.stringify(completionParameters)}`, @@ -608,5 +617,5 @@ export default abstract class Mission { * user or to the next mission. For example, data produced here for an ISR Search mission should * be data for the VTOL Search mission (of type VTOLSearchMissionParameters). */ - protected abstract generateCompletionParameters(): MissionParameters; + protected abstract generateCompletionParameters(): MissionParameters | undefined; } diff --git a/src/types/messages.ts b/src/types/messages.ts index e0b55775..1becf086 100644 --- a/src/types/messages.ts +++ b/src/types/messages.ts @@ -356,26 +356,54 @@ export interface ISRSearchMissionParameters { land: LandTaskParameters; } +export function isISRSearchMissionParameters(parameters: MissionParameters): boolean { + return 'takeoff' in parameters && 'isrSearch' in parameters && 'land' in parameters; +} + export interface VTOLSearchMissionParameters { quickScan: QuickScanTaskParameters; } +export function isVTOLSearchMissionParameters(parameters: MissionParameters): boolean { + return 'quickScan' in parameters; +} + export interface PayloadDropMissionParameters { takeoff: TakeoffTaskParameters; payloadDrop: PayloadDropTaskParameters; land: LandTaskParameters; } +export function isPayloadDropMissionParameters(parameters: MissionParameters): boolean { + return 'takeoff' in parameters && 'payloadDrop' in parameters && 'land' in parameters; +} + export interface UGVRetreiveMissionParameters { retrieveTarget: UGVRetreiveMissionParameters; deliverTarget: DeliverTargetTaskParameters; } +export function isUGVRetreiveMissionParameters(parameters: MissionParameters): boolean { + return 'retrieveTarget' in parameters && 'deliverTarget' in parameters; +} + // UUVRetrieveMission does not need parameters, as its one task does not need any. +export function isUUVRetrieveMissionParameters(parameters: MissionParameters): boolean { + return Object.keys(parameters).length === 0; +} + export type MissionParameters = ISRSearchMissionParameters | VTOLSearchMissionParameters | PayloadDropTaskParameters | UGVRetreiveMissionParameters | {}; +export const missionParametersTypeGuard = { + isISRSearchMissionParameters, + isVTOLSearchMissionParameters, + isPayloadDropMissionParameters, + isUGVRetreiveMissionParameters, + isUUVRetrieveMissionParameters, +}; + /** * Options for the mission. All variables are optional so the UI should * be able to detect which of the variables are required or not. From 7ef85de29ab97f29c5e2d700842166f428987c5d Mon Sep 17 00:00:00 2001 From: Paul Matthew Barrameda Date: Mon, 15 Apr 2019 05:16:21 -0700 Subject: [PATCH 084/152] Added support to start Mission Again will need to clean code. Should work --- src/common/Orchestrator.ts | 61 +++++++++++++++++++++++++++++--- src/common/missions/ISRSearch.ts | 2 +- src/common/missions/index.ts | 39 +++++++++++++++++++- src/common/struct/Mission.ts | 8 ----- src/types/messages.ts | 1 + src/util/ipc.ts | 3 +- 6 files changed, 98 insertions(+), 16 deletions(-) diff --git a/src/common/Orchestrator.ts b/src/common/Orchestrator.ts index 439ca57d..6426a614 100644 --- a/src/common/Orchestrator.ts +++ b/src/common/Orchestrator.ts @@ -1,6 +1,10 @@ import { ipcRenderer, Event } from 'electron'; -import { config, vehicleConfig, VehicleInfo } from '../static/index'; +import { + config, + vehicleConfig, + VehicleInfo, +} from '../static/index'; import { BadMessage, @@ -12,6 +16,8 @@ import { import ipc from '../util/ipc'; +import missionObject, { MissionObject } from './missions/index'; + import Mission from './struct/Mission'; import UpdateHandler from './struct/UpdateHandler'; import Vehicle from './struct/Vehicle'; @@ -74,7 +80,13 @@ class Orchestrator { ipcRenderer.on('handlePOIMessage', (_: Event, jsonMessage: JSONMessage): void => this.handlePOIMessage(jsonMessage)); ipcRenderer.on('handleCompleteMessage', (_: Event, jsonMessage: JSONMessage): void => this.handleCompleteMessage(jsonMessage)); - ipcRenderer.on('startMissions', (_: Event, missions: MissionDescription[], requireConfirmation: boolean): void => this.startMissions(missions, requireConfirmation)); + ipcRenderer.on('startMissions', ( + _: Event, + missions: MissionDescription[], + requireConfirmation: boolean, + ): void => this.startMissions(missions, requireConfirmation)); + + ipcRenderer.on('completeMission', (_: Event, missionName: string, completionParameters: MissionParameters): void => this.completeMission(missionName, completionParameters)); ipcRenderer.on('stopMission', this.stopMission); } @@ -179,7 +191,10 @@ class Orchestrator { * @param missions Description for all missions. Includes name of mission and information * required for mission. */ - private startMissions(missions: MissionDescription[], requireConfirmation: boolean): void { + private startMissions( + missions: MissionDescription[], + requireConfirmation: boolean, + ): void { if (this.running) { ipc.postLogMessages({ type: 'failure', @@ -188,12 +203,35 @@ class Orchestrator { return; } + if (missions.length === 0) { + ipc.postLogMessages({ + type: 'failure', + message: 'Cannot start new mission with no specified mission', + }); + return; + } + this.running = true; this.missions = missions; this.requireConfirmation = requireConfirmation; - // TODO: Set current mission to start. No code yet since those missions arent implemented. this.currentMissionIndex = 0; + const missionObj = missionObject[this.missions[this.currentMissionIndex].missionName]; + if (!missionObj) { + ipc.postLogMessages({ + type: 'failure', + message: 'Cannot start mission with unknown mission name', + }); + } + + const mission = missionObj as MissionObject; + + this.currentMissionIndex += 1; + this.currentMission = new mission.constructor( + this.vehicles, + this.missions[this.currentMissionIndex].information, + this.missions[this.currentMissionIndex].activeVehicleMapping, + ); } /** @@ -240,9 +278,22 @@ class Orchestrator { }); return; } + const missionObj = missionObject[this.missions[this.currentMissionIndex].missionName]; + if (!missionObj) { + ipc.postLogMessages({ + type: 'failure', + message: 'Cannot start mission with unknown mission name', + }); + } + + const mission = missionObj as MissionObject; this.currentMissionIndex += 1; - // TODO: Set next mission to start. No code yet since those missions arent implemented. + this.currentMission = new mission.constructor( + this.vehicles, + this.missions[this.currentMissionIndex].information, + this.missions[this.currentMissionIndex].activeVehicleMapping, + ); } /** diff --git a/src/common/missions/ISRSearch.ts b/src/common/missions/ISRSearch.ts index b870c183..f1836bbc 100644 --- a/src/common/missions/ISRSearch.ts +++ b/src/common/missions/ISRSearch.ts @@ -132,5 +132,5 @@ class ISRSearch extends Mission { export default { missionName, jobTypes, - Mission: ISRSearch, + constructor: ISRSearch, }; diff --git a/src/common/missions/index.ts b/src/common/missions/index.ts index ff8b4c56..70fbff00 100644 --- a/src/common/missions/index.ts +++ b/src/common/missions/index.ts @@ -1 +1,38 @@ -export default {}; +import { JobType } from '../../static/index'; + +import { MissionName } from '../../types/messages'; + +import Mission from '../struct/Mission'; + +import ISRSearch from './ISRSearch'; + +type MissionType = typeof Mission; + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +interface MissionWrapper extends MissionType { } + +/** + * Representation of a Mission subclass in this directory. + */ +export interface MissionObject { + /** + * Name of mission. + */ + missionName: MissionName; + + /** + * Jobs related to mission. + */ + jobTypes: JobType[]; + + /** + * Constructor of the mission. + */ + constructor: MissionWrapper; +} + +const missionObject: { [missionName: string]: MissionObject | undefined } = { + isrSearch: ISRSearch, +}; + +export default missionObject; diff --git a/src/common/struct/Mission.ts b/src/common/struct/Mission.ts index c680f228..95dd6749 100644 --- a/src/common/struct/Mission.ts +++ b/src/common/struct/Mission.ts @@ -101,23 +101,16 @@ export default abstract class Mission { */ private waitingVehicles = new DictionaryList(); - /** - * Callback to occur when mission has completed. - */ - private completionCallback: (parameters: MissionParameters | undefined) => void; - /** * Handles different states of the vehicle. */ private statusEventHandler = new UpdateHandler(); public constructor( - completionCallback: (parameters: MissionParameters | undefined) => void, vehicles: { [vehicleId: number]: Vehicle }, information: MissionInformation, activeVehicleMapping: { [vehicleId: number]: JobType }, ) { - this.completionCallback = completionCallback; this.vehicles = vehicles; this.parameters = information.parameters; this.options = information.options; @@ -407,7 +400,6 @@ export default abstract class Mission { * throughout the mission. */ if (this.activeTasks.size === 0 && this.waitingTasks.size() === 0) { - this.completionCallback(this.generateCompletionParameters()); this.stop(true); } else { // This should never happen. There is a big issue in the system if this somehow happens. diff --git a/src/types/messages.ts b/src/types/messages.ts index 1becf086..4d88c9e9 100644 --- a/src/types/messages.ts +++ b/src/types/messages.ts @@ -453,6 +453,7 @@ export type MissionName = 'isrSearch' | 'vtolSearch' | 'payloadDrop' | 'ugvRetri export interface MissionDescription { missionName: MissionName; + activeVehicleMapping: { [vehicleId: number]: JobType }; information: MissionInformation; } diff --git a/src/util/ipc.ts b/src/util/ipc.ts index 6ec773c9..88d9be4a 100644 --- a/src/util/ipc.ts +++ b/src/util/ipc.ts @@ -253,8 +253,9 @@ function postShowMissionWindow(): void { */ function postStartMissions( missions: MissionDescription[], + requireConfirmation: boolean, ): void { - ipcRenderer.send('post', 'startMissions', missions); + ipcRenderer.send('post', 'startMissions', missions, requireConfirmation); } /** From 12bcb3bfd2e80e1a9102a0a466ca799d2a61ed30 Mon Sep 17 00:00:00 2001 From: Paul Matthew Barrameda Date: Mon, 15 Apr 2019 14:55:02 -0700 Subject: [PATCH 085/152] Added "startNextMission" to Orchestrator --- src/common/Orchestrator.ts | 3 ++- src/util/ipc.ts | 11 +++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/common/Orchestrator.ts b/src/common/Orchestrator.ts index 6426a614..d589d5e0 100644 --- a/src/common/Orchestrator.ts +++ b/src/common/Orchestrator.ts @@ -87,6 +87,7 @@ class Orchestrator { ): void => this.startMissions(missions, requireConfirmation)); + ipcRenderer.on('startNextMission', this.startNextMission); ipcRenderer.on('completeMission', (_: Event, missionName: string, completionParameters: MissionParameters): void => this.completeMission(missionName, completionParameters)); ipcRenderer.on('stopMission', this.stopMission); } @@ -261,7 +262,7 @@ class Orchestrator { this.stopMission(); ipc.postFinishMissions(completionParameters); } else if (this.requireConfirmation) { - ipc.postConfirmCompleteMission(); + ipc.postConfirmCompleteMission(); // Start next mission on "startNextMission" notification. } else { this.startNextMission(); } diff --git a/src/util/ipc.ts b/src/util/ipc.ts index 88d9be4a..06cb84e1 100644 --- a/src/util/ipc.ts +++ b/src/util/ipc.ts @@ -258,6 +258,16 @@ function postStartMissions( ipcRenderer.send('post', 'startMissions', missions, requireConfirmation); } +/** + * Post "startNextMission" notification. + * + * Files that take this notification: + * - common/Orchestrator + */ +function postStartNextMission(): void { + ipcRenderer.send('post', 'startNextMission'); +} + /** * Post "stopMission" notification. * @@ -339,6 +349,7 @@ export default { postSetMapToUserLocation, postShowMissionWindow, postStartMissions, + postStartNextMission, postStopMission, postStopSendingMessages, postToggleTheme, From 1d193d5a7b8be10ad886b5ead033f6459e541515 Mon Sep 17 00:00:00 2001 From: Paul Matthew Barrameda Date: Mon, 15 Apr 2019 15:20:07 -0700 Subject: [PATCH 086/152] Fixed comments in Orchestrator + logic Noticed indexing was wrong in startNextMission so I fixed that. --- src/common/Orchestrator.ts | 137 ++++++++++++++++++++--------------- src/common/struct/Mission.ts | 10 +-- 2 files changed, 84 insertions(+), 63 deletions(-) diff --git a/src/common/Orchestrator.ts b/src/common/Orchestrator.ts index d589d5e0..da66fe97 100644 --- a/src/common/Orchestrator.ts +++ b/src/common/Orchestrator.ts @@ -36,6 +36,16 @@ class Orchestrator { }); } + /** + * Posts error message when error happens in Orchestrator. + */ + private static postOrchestratorError(error: string): void { + ipc.postLogMessages({ + type: 'failure', + message: `Something wrong happened in Orchestrator: ${error}`, + }); + } + /** * List of mission names and information for each mission. */ @@ -124,24 +134,41 @@ class Orchestrator { } /** - * Disconnects from a vehicle (if it does not communicate recently enough). + * Disconnects from a vehicle. * @param vehicleId ID of vehicle to disconnect from. */ private disconnectFromVehicle(vehicleId: number): void { - // Just in case, the statement should never pass. - if (!this.vehicles[vehicleId]) return; - this.vehicles[vehicleId].disconnect(); + if (!this.vehicles[vehicleId]) { + Orchestrator.postOrchestratorError(`Tried to disconnect from nonexisting vehicle id ${vehicleId}`); + return; + } + this.vehicles[vehicleId].disconnect(); ipc.postUpdateVehicles(this.vehicles[vehicleId].toPlainObject()); } + /** + * Handles acknowledgement messages. + * @param jsonMessage Message from vehicle. + */ private handleAcknowledgementMessage(jsonMessage: JSONMessage): void { - if (!this.vehicles[jsonMessage.sid] || this.vehicles[jsonMessage.sid].getStatus() === 'disconnected') return; + if (!this.vehicles[jsonMessage.sid] || this.vehicles[jsonMessage.sid].getStatus() === 'disconnected') { + Orchestrator.postOrchestratorError(`Received acknowledgement message from disconnected vehicle id ${jsonMessage.sid}`); + return; + } this.vehicles[jsonMessage.sid].updateLastConnectionTime(jsonMessage); } + /** + * Handles bad messages. + * @param jsonMessage Message from vehicle. + */ private handleBadMessage(jsonMessage: JSONMessage): void { - if (!this.vehicles[jsonMessage.sid] || this.vehicles[jsonMessage.sid].getStatus() === 'disconnected') return; + if (!this.vehicles[jsonMessage.sid] || this.vehicles[jsonMessage.sid].getStatus() === 'disconnected') { + Orchestrator.postOrchestratorError(`Received bad message from disconnected vehicle id ${jsonMessage.sid}`); + return; + } + this.vehicles[jsonMessage.sid].updateLastConnectionTime(jsonMessage); const badMessage = jsonMessage as BadMessage; @@ -151,8 +178,15 @@ class Orchestrator { }); } + /** + * Handles update messages. + * @param jsonMessage Message from vehicle. + */ private handleUpdateMessage(jsonMessage: JSONMessage): void { - if (!this.vehicles[jsonMessage.sid] || this.vehicles[jsonMessage.sid].getStatus() === 'disconnected') return; + if (!this.vehicles[jsonMessage.sid] || this.vehicles[jsonMessage.sid].getStatus() === 'disconnected') { + Orchestrator.postOrchestratorError(`Received update message from disconnected vehicle id ${jsonMessage.sid}`); + return; + } this.vehicles[jsonMessage.sid].update(jsonMessage); if (this.currentMission) this.currentMission.update(jsonMessage); @@ -161,23 +195,32 @@ class Orchestrator { ipc.postUpdateVehicles(this.vehicles[jsonMessage.sid].toPlainObject()); } + /** + * Handles point of interest messages. + * @param jsonMessage Message from vehicle. + */ private handlePOIMessage(jsonMessage: JSONMessage): void { - if (!this.vehicles[jsonMessage.sid] || this.vehicles[jsonMessage.sid].getStatus() === 'disconnected') return; - - if (this.currentMission) { - this.currentMission.update(jsonMessage); + if (!this.vehicles[jsonMessage.sid] || this.vehicles[jsonMessage.sid].getStatus() === 'disconnected') { + Orchestrator.postOrchestratorError(`Received point of interest message from disconnected vehicle id ${jsonMessage.sid}`); + return; } + + if (this.currentMission) this.currentMission.update(jsonMessage); Orchestrator.acknowledge(jsonMessage); } + /** + * Handles complete messages. + * @param jsonMessage Message from vehicle. + */ private handleCompleteMessage(jsonMessage: JSONMessage): void { - if (!this.vehicles[jsonMessage.sid] || this.vehicles[jsonMessage.sid].getStatus() === 'disconnected') return; + if (!this.vehicles[jsonMessage.sid] || this.vehicles[jsonMessage.sid].getStatus() === 'disconnected') { + Orchestrator.postOrchestratorError(`Received complete message from disconnected vehicle id ${jsonMessage.sid}`); + return; + } if (!this.currentMission) { - ipc.postLogMessages({ - type: 'failure', - message: `Received complete message from ${vehicleConfig.vehicleInfos[jsonMessage.sid]} while no mission was running`, - }); + Orchestrator.postOrchestratorError(`Received complete message from ${vehicleConfig.vehicleInfos[jsonMessage.sid]} while no mission was running`); return; } @@ -197,42 +240,21 @@ class Orchestrator { requireConfirmation: boolean, ): void { if (this.running) { - ipc.postLogMessages({ - type: 'failure', - message: 'Cannot start new missions while missions are already running', - }); + Orchestrator.postOrchestratorError('Cannot start new missions while missions are already running'); return; } if (missions.length === 0) { - ipc.postLogMessages({ - type: 'failure', - message: 'Cannot start new mission with no specified mission', - }); + Orchestrator.postOrchestratorError('Cannot start new missions with no missions provided'); return; } this.running = true; this.missions = missions; this.requireConfirmation = requireConfirmation; - this.currentMissionIndex = 0; - const missionObj = missionObject[this.missions[this.currentMissionIndex].missionName]; - if (!missionObj) { - ipc.postLogMessages({ - type: 'failure', - message: 'Cannot start mission with unknown mission name', - }); - } - const mission = missionObj as MissionObject; - - this.currentMissionIndex += 1; - this.currentMission = new mission.constructor( - this.vehicles, - this.missions[this.currentMissionIndex].information, - this.missions[this.currentMissionIndex].activeVehicleMapping, - ); + this.startMission(); } /** @@ -244,17 +266,9 @@ class Orchestrator { * @param completionParameters Paramters for next mission. */ private completeMission(missionName: string, completionParameters: MissionParameters): void { - /* - * The following should not happen. Only happens if the currentMissionIndex somehow - * does not match up with the mission that just finished, or if no mission was running for - * some reason. - */ if (!this.running || (this.currentMissionIndex >= 0 && this.missions[this.currentMissionIndex].missionName !== missionName)) { - ipc.postLogMessages({ - type: 'failure', - message: 'Received invalid completion for a mission', - }); + Orchestrator.postOrchestratorError('Invalid mission was completed'); return; } @@ -273,18 +287,24 @@ class Orchestrator { */ private startNextMission(): void { if (!this.running) { - ipc.postLogMessages({ - type: 'failure', - message: 'Cannot start next mission while no missions are running', - }); + Orchestrator.postOrchestratorError('Tried to start next mission while no missions are running'); return; } + + this.currentMissionIndex += 1; + this.startMission(); + } + + /** + * Support function to start a mission. Uses currentMissionIndex to determine which + * mission to start. Ensure to set the currentMissionIndex properly before calling + * this function. + */ + private startMission(): void { const missionObj = missionObject[this.missions[this.currentMissionIndex].missionName]; if (!missionObj) { - ipc.postLogMessages({ - type: 'failure', - message: 'Cannot start mission with unknown mission name', - }); + Orchestrator.postOrchestratorError('Tried to start mission with unknown mission name'); + return; } const mission = missionObj as MissionObject; @@ -298,7 +318,8 @@ class Orchestrator { } /** - * Clears all missions in Orchestrator. + * Clears all missions in Orchestrator. Run after all missions are completed or + * mission is stopped. */ private stopMission(): void { this.currentMissionIndex = -1; diff --git a/src/common/struct/Mission.ts b/src/common/struct/Mission.ts index 95dd6749..5027854c 100644 --- a/src/common/struct/Mission.ts +++ b/src/common/struct/Mission.ts @@ -171,7 +171,7 @@ export default abstract class Mission { private initialize(): void { // Stops mission if mission is not in the "ready" state. if (this.status !== 'ready') { - this.stop(false, `Something wrong happend in ${this.missionName}`); + this.stop(false, `Something wrong happend in ${this.missionName} mission`); return; } @@ -198,7 +198,7 @@ export default abstract class Mission { private assignJobs(): void { // Fails to assign jobs if mission is not in the "initializing" state. if (this.status !== 'initializing') { - this.stop(false, `Something wrong happened in ${this.missionName}`); + this.stop(false, `Something wrong happened in ${this.missionName} mission`); return; } @@ -230,7 +230,7 @@ export default abstract class Mission { if (pendingVehicleIds.length === 0) { ipc.postLogMessages({ type: 'success', - message: `Assigned jobs to all vehicles for ${this.missionName}`, + message: `Assigned jobs to all vehicles for ${this.missionName} mission`, }); this.statusEventHandler.event('status', 'waiting'); @@ -284,7 +284,7 @@ export default abstract class Mission { private start(): void { // Fails to initialize if mission is not in the "ready" state. if (this.status !== 'waiting') { - this.stop(false, `Something wrong happened in ${this.missionName}`); + this.stop(false, `Something wrong happened in ${this.missionName} mission`); return; } @@ -403,7 +403,7 @@ export default abstract class Mission { this.stop(true); } else { // This should never happen. There is a big issue in the system if this somehow happens. - this.stop(false, `Failed to complete ${this.missionName}`); + this.stop(false, `Failed to complete ${this.missionName} mission`); } } } From 54f25a85b33c5f7ddf878360d1db391bd1fc627f Mon Sep 17 00:00:00 2001 From: Paul Matthew Barrameda Date: Mon, 15 Apr 2019 16:33:23 -0700 Subject: [PATCH 087/152] Modified code structure + comments to improve readability Final fix to logic of MessageHandler's receivingMessage function. It should only forward the first message of a certain ID to the Orchestrator, but acknowledge all valid messages. --- src/common/MessageHandler.ts | 154 +++++------------- src/common/Orchestrator.ts | 11 +- src/common/Xbee.ts | 20 +-- src/common/struct/DictionaryList.ts | 9 +- src/common/struct/Mission.ts | 7 +- src/common/struct/UpdateHandler.ts | 18 -- src/common/struct/Vehicle.ts | 32 ++-- .../fixtures/updateVehiclesFixtures.ts | 2 +- src/util/ipc.ts | 11 ++ 9 files changed, 77 insertions(+), 187 deletions(-) diff --git a/src/common/MessageHandler.ts b/src/common/MessageHandler.ts index b410e435..f5c949b9 100644 --- a/src/common/MessageHandler.ts +++ b/src/common/MessageHandler.ts @@ -30,7 +30,7 @@ class MessageHandler { private messageDictionary = new DictionaryList(); /** - * Handler that listens for different update events for the message handler. + * Handler that listens for different update events. */ private updateHandler = new UpdateHandler(); @@ -76,49 +76,27 @@ class MessageHandler { this.id += 1; - /* - * Adds the json message to "sent" and "outbox" on messageDictionary. If it's an ack - * or badMessage message, it is only added to "sent", since that message does not need to be - * acknowledged. - */ this.messageDictionary.push('sent', jsonMessage); if (messageTypeGuard.isAcknowledgementMessage(message) || messageTypeGuard.isBadMessage(message)) { - /* - * Will only send these types of messages once, but everything else will be sent - * continuously until acknowleged. - */ xbee.sendMessage(jsonMessage); return; } this.messageDictionary.push('outbox', jsonMessage); - /* - * Keep sending the message in a certain rate (see messageSendRate). - * This will end when the message is acknowledged (the updateHandler will clear - * the interval). - */ const expiry = setInterval( (): void => xbee.sendMessage(jsonMessage), config.messageSendRate * 1000, ); - /* - * Removes respective message from "outbox" when an ack message is received - * and value passed is true. - * - * If no ack message is received within a certain amount of time (vehicleDisconnectionTime), - * then the GCS will disconnect itself from the vehicle. - */ - + // Keep track of message, to stop sending it once acknowledged. const hash = `${jsonMessage.tid}#${jsonMessage.id}`; - - this.updateHandler.addHandler(hash, (value: boolean): boolean => { + this.updateHandler.addHandler(hash, (): boolean => { clearInterval(expiry); this.removeMessage('outbox', jsonMessage.id); - return value; + return true; }, { time: config.vehicleDisconnectionTime * 1000, callback: (): void => { @@ -132,7 +110,7 @@ class MessageHandler { /** * Send a bad message. * - * @param jsonMessage The bad message (needs to have at least an sid field). + * @param jsonMessage The bad message. * @param error Error message. */ private sendBadMessage(jsonMessage: JSONMessage, error?: string): void { @@ -145,11 +123,10 @@ class MessageHandler { /** * Processes a message that was received. * - * @param string The raw string. Either can contain the message (a valid JSON) or some - * random stuff. + * @param text The raw string in the message. */ private receiveMessage(text: string): void { - // Filter out text that are not valid json. + // Filter out all invalid messages. if (!isJSON(text)) { ipc.postLogMessages({ message: `Received text from Xbee that is not a JSON, could not send bad message to sender ${text}`, @@ -157,7 +134,6 @@ class MessageHandler { return; } - // Filter out json that are not valid messages. const json = JSON.parse(text); if (!messageTypeGuard.isJSONMessage(json)) { if (!Number.isNaN(json.sid) && vehicleConfig.isValidVehicleId(json.sid as number)) { @@ -170,106 +146,57 @@ class MessageHandler { return; } - /* - * The message typeguard check will let us know that there is a type field. It does not check if - * the type field is lowercased, so we will manually make it lowercased here, as all of our - * message type fields are required to be in lowercase. - */ json.type = (json.type as string).toLowerCase(); - const jsonMessage = json as JSONMessage; // Ignore messages from unrecognized vehicles. if (!vehicleConfig.isValidVehicleId(jsonMessage.sid)) return; - /* - * Stops sending the message that the ack message has acknowledged. - * Put this before the if statement below since want to acknowledge - * every message of the same ID afterwards, but for all other messages - * we only want to handle it once. - * DO NOT ACKNOWLEDGE. - */ - if (messageTypeGuard.isAcknowledgementMessage(jsonMessage)) { - const { ackid } = jsonMessage as AcknowledgementMessage; + const newMessage = !this.receivedMessageId[jsonMessage.sid] + || this.receivedMessageId[jsonMessage.sid] as number < jsonMessage.id; - const hash = `${jsonMessage.sid}#${ackid}`; - this.updateHandler.event(hash, true); + if (messageTypeGuard.isConnectMessage(jsonMessage)) { + ipc.postAcknowledgeMessage(jsonMessage); + if (newMessage) ipc.postConnectToVehicle(jsonMessage); + } else if (messageTypeGuard.isCompleteMessage(jsonMessage)) { + ipc.postAcknowledgeMessage(jsonMessage); + if (newMessage) ipc.postHandleCompleteMessage(jsonMessage); + } else if (messageTypeGuard.isPOIMessage(jsonMessage)) { + ipc.postAcknowledgeMessage(jsonMessage); + if (newMessage) ipc.postHandlePOIMessage(jsonMessage); + } else if (messageTypeGuard.isUpdateMessage(jsonMessage)) { + ipc.postAcknowledgeMessage(jsonMessage); + if (newMessage) ipc.postHandleUpdateMessage(jsonMessage); + } else if (messageTypeGuard.isBadMessage(jsonMessage)) { + ipc.postHandleBadMessage(jsonMessage); + } else if (messageTypeGuard.isAcknowledgementMessage(jsonMessage)) { + const { ackid } = jsonMessage as AcknowledgementMessage; - ipc.postHandleAcknowledgementMessage(jsonMessage); + if (newMessage) { + const hash = `${jsonMessage.sid}#${ackid}`; + this.updateHandler.event(hash, true); // Stop sending message that this message acknowleges. + ipc.postHandleAcknowledgementMessage(jsonMessage); + } + } else { + this.sendBadMessage(jsonMessage, `Message of type ${jsonMessage.type} is invalid or is not acceptable by GCS`); } - /* - * Update the last message id received from the vehicle or discard message completely. - * This is to be able to properly acknowledge a message. See the note on the following: - * - * https://ground-control-station.readthedocs.io/en/latest/communications/other-messages.html#acknowledgement-message - */ - if (!this.receivedMessageId[jsonMessage.sid] - || this.receivedMessageId[jsonMessage.sid] as number < jsonMessage.id - ) { - /* - * If this statement passes, it means that GCS is recieving a completely new message - * from the vehicle. - */ + // Logic: https://ground-control-station.readthedocs.io/en/latest/communications/messages/other-messages.html#acknowledgement-message + if (newMessage) { this.receivedMessageId[jsonMessage.sid] = jsonMessage.id; this.messageDictionary.push('received', jsonMessage); - } else { - // If the statement fails, it means that the GCS is receiving an old message. Discard it. - return; - } - - /* - * All messages that get here will be forwarded to the Orchestrator and will - * be handled there (eg. acknowledgement, updating vehicle connection). - */ - - if (messageTypeGuard.isConnectMessage(jsonMessage)) { - ipc.postConnectToVehicle(jsonMessage); - return; } - - if (messageTypeGuard.isCompleteMessage(jsonMessage)) { - ipc.postHandleCompleteMessage(jsonMessage); - return; - } - - if (messageTypeGuard.isPOIMessage(jsonMessage)) { - ipc.postHandlePOIMessage(jsonMessage); - return; - } - - if (messageTypeGuard.isUpdateMessage(jsonMessage)) { - ipc.postHandleUpdateMessage(jsonMessage); - return; - } - - // DO NOT ACKNOWLEDGE. - if (messageTypeGuard.isBadMessage(jsonMessage)) { - ipc.postHandleBadMessage(jsonMessage); - return; - } - - /* - * The first acknowledgement gets passed through all other if statements so - * we stop it here - */ - if (messageTypeGuard.isAcknowledgementMessage(jsonMessage)) return; - - /* - * Any message that reaches here is a message (has a valid type, id, sid, tid, and time) - * but does not have valid properties, or is a message GCS should not receive from vehicles - * (e.g. GCS should not receive a "start" message from a vehicle). - */ - this.sendBadMessage(jsonMessage, `Message of type ${jsonMessage.type} is invalid or is not acceptable by GCS`); } /** - * Stops sending all messages (literally discards all messages from the "outbox" list, but they - * will still be in the "sent" list). + * Stops sending all messages. */ private stopSendingMessages(): void { - // Run the clearInterval() of each handler. - this.updateHandler.getEvents().forEach((hash): void => { + const outbox = this.messageDictionary.get('outbox'); + if (!outbox) return; + + outbox.forEach((jsonMessage): void => { + const hash = `${jsonMessage.tid}#${jsonMessage.id}`; this.updateHandler.event(hash, true); }); } @@ -277,7 +204,6 @@ class MessageHandler { /* * This allows only one instance of the MessageHandler to be used. - * The Orchestrator does not need to call any functions of the MessageHandler. * All requests are passed through an ipcRenderer notification. */ export default new MessageHandler(); diff --git a/src/common/Orchestrator.ts b/src/common/Orchestrator.ts index da66fe97..f5b0c7bd 100644 --- a/src/common/Orchestrator.ts +++ b/src/common/Orchestrator.ts @@ -29,7 +29,7 @@ class Orchestrator { * Acknowledges a message. All messages are passed here through the MessageHandler. * Only messages that are no acknowledged are bad messages and acknowledgements. */ - private static acknowledge(jsonMessage: JSONMessage): void { + private static acknowledgeMessage(jsonMessage: JSONMessage): void { ipc.postSendMessage(jsonMessage.sid, { type: 'ack', ackid: jsonMessage.id, @@ -84,6 +84,8 @@ class Orchestrator { ipcRenderer.on('connectToVehicle', (_: Event, jsonMessage: JSONMessage): void => this.connectToVehicle(jsonMessage)); ipcRenderer.on('disconnectFromVehicle', (_: Event, vehicleId: number): void => this.disconnectFromVehicle(vehicleId)); + ipcRenderer.on('acknowledgeMessage', (_: Event, jsonMessage: JSONMessage): void => Orchestrator.acknowledgeMessage(jsonMessage)); + ipcRenderer.on('handleAcknowledgementMessage', (_: Event, jsonMessage: JSONMessage): void => this.handleAcknowledgementMessage(jsonMessage)); ipcRenderer.on('handleBadMessage', (_: Event, jsonMessage: JSONMessage): void => this.handleBadMessage(jsonMessage)); ipcRenderer.on('handleUpdateMessage', (_: Event, jsonMessage: JSONMessage): void => this.handleUpdateMessage(jsonMessage)); @@ -144,7 +146,7 @@ class Orchestrator { } this.vehicles[vehicleId].disconnect(); - ipc.postUpdateVehicles(this.vehicles[vehicleId].toPlainObject()); + ipc.postUpdateVehicles(this.vehicles[vehicleId].toObject()); } /** @@ -190,9 +192,8 @@ class Orchestrator { this.vehicles[jsonMessage.sid].update(jsonMessage); if (this.currentMission) this.currentMission.update(jsonMessage); - Orchestrator.acknowledge(jsonMessage); - ipc.postUpdateVehicles(this.vehicles[jsonMessage.sid].toPlainObject()); + ipc.postUpdateVehicles(this.vehicles[jsonMessage.sid].toObject()); } /** @@ -206,7 +207,6 @@ class Orchestrator { } if (this.currentMission) this.currentMission.update(jsonMessage); - Orchestrator.acknowledge(jsonMessage); } /** @@ -225,7 +225,6 @@ class Orchestrator { } this.currentMission.update(jsonMessage); - Orchestrator.acknowledge(jsonMessage); } /** diff --git a/src/common/Xbee.ts b/src/common/Xbee.ts index c6a32d69..9ffae842 100644 --- a/src/common/Xbee.ts +++ b/src/common/Xbee.ts @@ -7,17 +7,14 @@ import { JSONMessage } from '../types/messages'; import ipc from '../util/ipc'; -/* - * TODO: Add a feature with Vehicle container to change this dynamically and reconnect. - * With that feature, no need to have this at all in config. - */ +// TODO: Add a feature with Vehicle container to change this dynamically and reconnect. const port: string | undefined = 'COM5'; -const serialport = new SerialPort(port, { baudRate: 57600 }, (err): void => { - if (err) { +const serialport = new SerialPort(port, { baudRate: 57600 }, (error): void => { + if (error) { ipc.postLogMessages({ type: 'failure', - message: `Failed to initialize Xbee: ${err.message}`, + message: `Failed to initialize Xbee: ${error.message}`, }); } }); @@ -32,13 +29,8 @@ const xbeeAPI = new XBeeAPI(); * @param message The message to send to the vehicle. */ function sendMessage(message: JSONMessage): void { - // Cannot send messages if port is not open. if (!serialport.isOpen) return; - /* - * Check to ensure the vehicle we are sending this to is valid. This is just in case - * MessageHandler somehow provides an invalid tid (id of the target vehicle). - */ const vehicleInfoObject = vehicleConfig.vehicleInfos[message.tid]; if (!vehicleInfoObject) { ipc.postLogMessages({ @@ -57,12 +49,14 @@ function sendMessage(message: JSONMessage): void { }); } +// TODO: Open connection from MessageHandler. function openConnection(): boolean { if (serialport.isOpen) return false; serialport.open(); return true; } +// TODO: Close connection from MessageHandler. function closeConnection(): boolean { if (!serialport.isOpen) return false; serialport.close(); @@ -95,8 +89,6 @@ serialport.on('close', (): void => { xbeeAPI.parser.on('data', (frame: Frame): void => { if (frame.type !== C.FRAME_TYPE.ZIGBEE_RECEIVE_PACKET || !frame.data) return; - - // Sends notification to process the message (either a valid JSON string or not). ipc.postReceiveMessage(frame.data.toString()); }); diff --git a/src/common/struct/DictionaryList.ts b/src/common/struct/DictionaryList.ts index b0ad1956..cfb1604c 100644 --- a/src/common/struct/DictionaryList.ts +++ b/src/common/struct/DictionaryList.ts @@ -4,10 +4,6 @@ export type CompareFunction = (a: T, b: T) => number; /** * Structure that can store lists in a dictionary. Lists can be obtained with a key string, * and can be modified with custom functions. - * - * The functions in this structure can be called from the list with a few lines of code. However - * it is possible that the value of a certain key can be undefined, which this class will do - * checks for. */ export default class DictionaryList { /** @@ -30,7 +26,6 @@ export default class DictionaryList { this.dictionary[key] = []; } - // Cast to T[] since this value cannot be undefined. const list = this.dictionary[key] as T[]; this.numItems += values.length; @@ -41,7 +36,7 @@ export default class DictionaryList { /** * Appends new elements to the front of the array, and returns the new size of - * the dictioanry. + * the dictionary. * * @param key The key to access the list in the dictionary. */ @@ -74,7 +69,7 @@ export default class DictionaryList { * @param list The list to change the dictionary's list to. */ public set(key: string, list: T[]): void { - const currentLength = this.dictionary[key] ? (this.dictionary[key] as T[]).length : 0; + const currentLength = this.dictionary[key] ? this.size(key) : 0; this.dictionary[key] = list; this.numItems += list.length - currentLength; diff --git a/src/common/struct/Mission.ts b/src/common/struct/Mission.ts index 5027854c..8ee71537 100644 --- a/src/common/struct/Mission.ts +++ b/src/common/struct/Mission.ts @@ -332,10 +332,7 @@ export default abstract class Mission { * tasks in waitingTasks. */ Object.values(this.activeVehicleMapping).forEach((jobType): void => { - while ( - (this.waitingTasks.get(jobType) as Task[]).length > 0 - && (this.waitingVehicles.get(jobType) as Vehicle[]).length > 0 - ) { + while (this.waitingTasks.size(jobType) > 0 && this.waitingVehicles.size(jobType) > 0) { const task = this.waitingTasks.shift(jobType) as Task; const vehicle = this.waitingVehicles.shift(jobType) as Vehicle; @@ -381,7 +378,7 @@ export default abstract class Mission { if (messageTypeGuard.isCompleteMessage(jsonMessage)) { const jobType = this.activeVehicleMapping[jsonMessage.sid]; - if ((this.waitingTasks.get(jobType) as Task[]).length > 0) { + if (this.waitingTasks.size(jobType) > 0) { // Assigns the next task in that job to the vehicle. const newTask = this.waitingTasks.shift(jobType) as Task; diff --git a/src/common/struct/UpdateHandler.ts b/src/common/struct/UpdateHandler.ts index 3a61e468..0b38b7d6 100644 --- a/src/common/struct/UpdateHandler.ts +++ b/src/common/struct/UpdateHandler.ts @@ -46,13 +46,6 @@ interface Handler { export default class UpdateHandler { private eventDictionary: { [key: string]: Handler[] | undefined } = {}; - /** - * Gets list of events in the update handler. - */ - public getEvents(): string[] { - return Object.keys(this.eventDictionary); - } - /** * Adds a new handler. * @@ -148,15 +141,4 @@ export default class UpdateHandler { this.eventDictionary[name] = (this.eventDictionary[name] as Handler[]) .filter((h): boolean => h !== handler); } - - /** - * Removes all handlers in the update handler. - */ - public clearHandlers(): void { - Object.keys(this.eventDictionary).forEach((event): void => { - (this.eventDictionary[event] as Handler[]).forEach((handler): void => { - handler.removeHandler(); - }); - }); - } } diff --git a/src/common/struct/Vehicle.ts b/src/common/struct/Vehicle.ts index e942f7df..4c414cc7 100644 --- a/src/common/struct/Vehicle.ts +++ b/src/common/struct/Vehicle.ts @@ -19,7 +19,14 @@ type ErrorCallback = (message?: string) => void; * Options to initialize a vehicle. */ export interface VehicleOptions { + /** + * ID of vehicle coming from connect message. + */ sid: number; + + /** + * Jobs vehicle can handle. + */ jobs: JobType[]; } @@ -96,12 +103,9 @@ export default class Vehicle { this.vehicleId = options.sid; this.jobs = options.jobs; - // We never want these handlers to disappear so always return false on their callback functions. - this.updateEventHandler.addHandler('status', (status, message): boolean => { this.status = status; if (status === 'error') { - // Calls the errorCallback function and provides errorMessage to it. this.errorCallback(message && message.errorMessage); } return false; @@ -125,7 +129,6 @@ export default class Vehicle { this.updateEventHandler.addHandler('battery', (battery, message): boolean => { if (battery > 1 || battery < 0) { const vehicleInfo = message && message.sid && vehicleConfig.vehicleInfos[message.sid]; - ipc.postLogMessages({ type: 'failure', message: `Received an invalid battery status (${battery * 100}%) from ${(vehicleInfo && vehicleInfo.name) || 'an unknown vehicle'}`, @@ -166,7 +169,7 @@ export default class Vehicle { * Converts vehicle to a plain object so that its private variables can be read * when it is sent through ipcRenderer. */ - public toPlainObject(): VehicleObject { + public toObject(): VehicleObject { return { vehicleId: this.vehicleId, status: this.status, @@ -239,10 +242,6 @@ export default class Vehicle { errorCallback?: ErrorCallback, options?: object, ): boolean { - /* - * Returns false if a vehicle has already been assigned a mission. - * Vehicle's status is ready if it has not been assigned a mission. - */ if (this.status !== 'ready') { return false; } @@ -257,13 +256,7 @@ export default class Vehicle { if (options) startMessage.options = options; - // Sends the start message to the vehicle with corresponding job name. this.sendMessage(startMessage); - - /* - * Create handler that will end when vehicle successfully receives the job and goes to a - * waiting state. - */ this.updateEventHandler.addHandler('status', (value): boolean => { if (value === 'waiting') { if (completionCallback) completionCallback(); @@ -284,11 +277,6 @@ export default class Vehicle { * @param task The task for the vehicle to perform. Must support the vehicle's job. */ public assignTask(task: Task): boolean { - /* - * Returns false if the vehicle has not been assigned a mission or if the provided task - * is not compatible with its job. - * Vehicle's status is "waiting" if it has been assigned a mission but is awaiting a task. - */ if (this.status !== 'waiting' || !this.assignedJob || !vehicleConfig.isValidTaskTypeForJob(task.taskType, this.assignedJob)) { return false; } @@ -305,10 +293,10 @@ export default class Vehicle { * Sends stop message to vehicle. */ public stop(): void { + this.assignedJob = ''; + this.sendMessage({ type: 'stop', }); - - this.assignedJob = ''; } } diff --git a/src/renderer/fixtures/updateVehiclesFixtures.ts b/src/renderer/fixtures/updateVehiclesFixtures.ts index f47b52c0..c2db5045 100644 --- a/src/renderer/fixtures/updateVehiclesFixtures.ts +++ b/src/renderer/fixtures/updateVehiclesFixtures.ts @@ -60,7 +60,7 @@ export default function updateVehicles(): void { status: status[Math.floor(Math.random() * status.length)] as VehicleStatus, }); - fixtures[i] = vehicles[i].toPlainObject(); + fixtures[i] = vehicles[i].toObject(); } ipc.postUpdateVehicles(...fixtures); diff --git a/src/util/ipc.ts b/src/util/ipc.ts index 06cb84e1..e606b8d3 100644 --- a/src/util/ipc.ts +++ b/src/util/ipc.ts @@ -30,6 +30,16 @@ import { VehicleObject, } from '../types/types'; +/** + * Post "acknowledgeMessage" notification. + * + * Files that take this notification: + * - common/Orchestrator + */ +function postAcknowledgeMessage(jsonMessage: JSONMessage): void { + ipcRenderer.send('post', 'acknowledgeMessage', jsonMessage); +} + /** * Post "centerMapToVehicle" notification. * @@ -329,6 +339,7 @@ function postUpdateVehicles(...vehicles: VehicleObject[]): void { } export default { + postAcknowledgeMessage, postCenterMapToVehicle, postCompleteMission, postConfirmCompleteMission, From 37095b97319220149ebf65ece429828c572bf2f2 Mon Sep 17 00:00:00 2001 From: Paul Matthew Barrameda Date: Tue, 16 Apr 2019 11:38:44 -0700 Subject: [PATCH 088/152] Fixed logic + comments Added check to Orchestrator to check Mission's vehicles before calling mission.update() Did check on all vehicle states before assigning jobs in Mission (instead of checking states while assigning jobs). Added check for new message for bad messages too. Fixed logic in Orchestrator to set completionParameters of this mission to the starting parameters for next mission. Logic in Mission will be to send stop to all vehicles if the stop() function is called while the Mission status is not "ready". If Mission status is "initializing" it will also stop sending current messages. Added logic to Vehicle's status handler to remove the callback when it disconnects, as the other callback should not happen if the vehicle is too late to accept its job. --- src/common/MessageHandler.ts | 7 +- src/common/Orchestrator.ts | 35 ++- src/common/struct/DictionaryList.ts | 29 ++- src/common/struct/Mission.ts | 383 +++++++++++++--------------- src/common/struct/Vehicle.ts | 2 +- src/types/messages.ts | 2 +- src/util/ipc.ts | 8 +- 7 files changed, 234 insertions(+), 232 deletions(-) diff --git a/src/common/MessageHandler.ts b/src/common/MessageHandler.ts index f5c949b9..84f44c59 100644 --- a/src/common/MessageHandler.ts +++ b/src/common/MessageHandler.ts @@ -168,7 +168,7 @@ class MessageHandler { ipc.postAcknowledgeMessage(jsonMessage); if (newMessage) ipc.postHandleUpdateMessage(jsonMessage); } else if (messageTypeGuard.isBadMessage(jsonMessage)) { - ipc.postHandleBadMessage(jsonMessage); + if (newMessage) ipc.postHandleBadMessage(jsonMessage); } else if (messageTypeGuard.isAcknowledgementMessage(jsonMessage)) { const { ackid } = jsonMessage as AcknowledgementMessage; @@ -192,10 +192,7 @@ class MessageHandler { * Stops sending all messages. */ private stopSendingMessages(): void { - const outbox = this.messageDictionary.get('outbox'); - if (!outbox) return; - - outbox.forEach((jsonMessage): void => { + this.messageDictionary.forEach('outbox', (jsonMessage): void => { const hash = `${jsonMessage.tid}#${jsonMessage.id}`; this.updateHandler.event(hash, true); }); diff --git a/src/common/Orchestrator.ts b/src/common/Orchestrator.ts index f5b0c7bd..2420a971 100644 --- a/src/common/Orchestrator.ts +++ b/src/common/Orchestrator.ts @@ -101,7 +101,7 @@ class Orchestrator { ipcRenderer.on('startNextMission', this.startNextMission); ipcRenderer.on('completeMission', (_: Event, missionName: string, completionParameters: MissionParameters): void => this.completeMission(missionName, completionParameters)); - ipcRenderer.on('stopMission', this.stopMission); + ipcRenderer.on('stopMissions', this.stopMissions); } /** @@ -191,7 +191,9 @@ class Orchestrator { } this.vehicles[jsonMessage.sid].update(jsonMessage); - if (this.currentMission) this.currentMission.update(jsonMessage); + if (this.currentMission && this.currentMission.getVehicles()[jsonMessage.sid]) { + this.currentMission.update(jsonMessage); + } ipc.postUpdateVehicles(this.vehicles[jsonMessage.sid].toObject()); } @@ -202,7 +204,12 @@ class Orchestrator { */ private handlePOIMessage(jsonMessage: JSONMessage): void { if (!this.vehicles[jsonMessage.sid] || this.vehicles[jsonMessage.sid].getStatus() === 'disconnected') { - Orchestrator.postOrchestratorError(`Received point of interest message from disconnected vehicle id ${jsonMessage.sid}`); + Orchestrator.postOrchestratorError(`Received point of interest message from ${vehicleConfig.vehicleInfos[jsonMessage.sid]} but it is currently disconnected`); + return; + } + + if (this.currentMission && !this.currentMission.getVehicles()[jsonMessage.sid]) { + Orchestrator.postOrchestratorError(`Received point of interest message from ${vehicleConfig.vehicleInfos[jsonMessage.sid]} while it is not assigned to mission`); return; } @@ -220,7 +227,12 @@ class Orchestrator { } if (!this.currentMission) { - Orchestrator.postOrchestratorError(`Received complete message from ${vehicleConfig.vehicleInfos[jsonMessage.sid]} while no mission was running`); + Orchestrator.postOrchestratorError(`Received complete message from ${vehicleConfig.vehicleInfos[jsonMessage.sid]} while no mission is running`); + return; + } + + if (!this.currentMission.getVehicles()[jsonMessage.sid]) { + Orchestrator.postOrchestratorError(`Received complete message from ${vehicleConfig.vehicleInfos[jsonMessage.sid]} while it is not assigned to the mission`); return; } @@ -265,16 +277,21 @@ class Orchestrator { * @param completionParameters Paramters for next mission. */ private completeMission(missionName: string, completionParameters: MissionParameters): void { - if (!this.running || (this.currentMissionIndex >= 0 - && this.missions[this.currentMissionIndex].missionName !== missionName)) { + if (!this.running + || (this.currentMissionIndex >= 0 + && this.missions[this.currentMissionIndex].missionName !== missionName)) { Orchestrator.postOrchestratorError('Invalid mission was completed'); return; } if (this.currentMissionIndex === this.missions.length - 1) { - this.stopMission(); ipc.postFinishMissions(completionParameters); - } else if (this.requireConfirmation) { + this.stopMissions(); + } else { + this.missions[this.currentMissionIndex + 1].information.parameters = completionParameters; + } + + if (this.requireConfirmation) { ipc.postConfirmCompleteMission(); // Start next mission on "startNextMission" notification. } else { this.startNextMission(); @@ -320,7 +337,7 @@ class Orchestrator { * Clears all missions in Orchestrator. Run after all missions are completed or * mission is stopped. */ - private stopMission(): void { + private stopMissions(): void { this.currentMissionIndex = -1; this.currentMission = null; this.missions = []; diff --git a/src/common/struct/DictionaryList.ts b/src/common/struct/DictionaryList.ts index cfb1604c..1cfb1780 100644 --- a/src/common/struct/DictionaryList.ts +++ b/src/common/struct/DictionaryList.ts @@ -144,7 +144,8 @@ export default class DictionaryList { } /** - * Removes all elements of an array that meets the condition specified in a callback function. + * Removes all elements of an array of a specified key that meets the condition + * specified in a callback function. * * @param key The key to access the list in the dictionary. * @param callback Test each element, from oldest to newest. All elements to pass @@ -170,7 +171,8 @@ export default class DictionaryList { } /** - * Returns the elements of an array that meet the condition specified in a callback function. + * Returns the elements of an array of a specified key that meet the condition + * specified in a callback function. * * @param key The key to access the list in the dictionary. * @param callback Test each element, from oldest to newest. All elements to pass @@ -183,6 +185,29 @@ export default class DictionaryList { return list.filter(callback); } + /** + * Performs the specified action for each element in an array of a specified key. + */ + public forEach(key: string, callback: (value: T, index: number, array: T[]) => void): void { + const list = this.dictionary[key]; + if (!list) return; + + list.forEach(callback); + } + + /** + * Determines whether the specified callback function returns true for any element of an array. + * + * @param key The key to access the list in the dictionary. + * @param callback Test each element, and return true if an element passes the callback. + */ + public some(key: string, callback: Callback): boolean { + const list = this.dictionary[key]; + if (!list) return true; + + return list.some(callback); + } + /** * Sorts a list for a given key. * diff --git a/src/common/struct/Mission.ts b/src/common/struct/Mission.ts index 8ee71537..90f02df2 100644 --- a/src/common/struct/Mission.ts +++ b/src/common/struct/Mission.ts @@ -65,11 +65,8 @@ export default abstract class Mission { /** * Map of the id of the vehicle that is currently performing all tasks - * related to the job type value. This is used from mission initliaization until - * assigning jobs to vehicles. When mission starts, this gets reassigned to - * waitingVehicles + * to the job they're for. * - * The values put together as an array must equal all of the mission's job types. * There will be no job types in this mapping that are irrelevant to the mission. */ private activeVehicleMapping: { [vehicleId: number]: JobType } = {}; @@ -90,9 +87,9 @@ export default abstract class Mission { /** * All tasks waiting to be executed, mapped by their job type. * - * Note that these tasks will be set in the Mission superclass, but each subclass is responsible - * of implementing the list of Tasks that are to be performed, in the order they are to be - * performed. + * Note that these tasks will be set in the Mission superclass. Subclasses are in charge + * of generating the tasks from the generateTasks() function. Subclasses can also add more + * tasks as the mission goes through the update function. */ private waitingTasks = new DictionaryList(); @@ -102,7 +99,7 @@ export default abstract class Mission { private waitingVehicles = new DictionaryList(); /** - * Handles different states of the vehicle. + * Automates the mission by handling its different states. */ private statusEventHandler = new UpdateHandler(); @@ -111,20 +108,15 @@ export default abstract class Mission { information: MissionInformation, activeVehicleMapping: { [vehicleId: number]: JobType }, ) { + if (!information.parameters) { + throw new Error('Parameters for mission are not provided'); + } + this.vehicles = vehicles; this.parameters = information.parameters; this.options = information.options; - /* - * TODO: Allow the user to select more than one vehicle for a job. The vehicles can be used - * as backup in the case that the first vehicle goes on an error state. However this data - * should not be activeVehicleMapping. activeVehicleMapping will always be vehicleId => jobType - */ - - /* - * Filters out any irrelevant jobs when adding to activeVehicleMapping in the case that - * extra jobs were included in the mapping. - */ + // Filters out any irrelevant jobs when adding to activeVehicleMapping. Object.keys(activeVehicleMapping).forEach((vehicleIdString): void => { const vehicleId = parseInt(vehicleIdString, 10); const jobType = activeVehicleMapping[vehicleId]; @@ -134,12 +126,12 @@ export default abstract class Mission { }); /* - * Create event handler that will handle all changes of status: - * 1. Will assignJobs if status changes to "initializing". - * 2. Will start mission if status changs to "waiting". - * 3. Will remove this handler if status goes back to "ready", which means: - * - mission was terminated before it completed - * - mission was terminated after it completed + * Create event handler that will handle all changes of mission status: + * 1. Runs assignJobs() when status changes to "initializing". + * 2. Runs startMission() when status changs to "waiting". + * 3. Removes this handler when status goes back to "ready", which means: + * - mission was terminated + * - mission was completed */ this.statusEventHandler.addHandler('status', (status): boolean => { this.status = status; @@ -147,7 +139,7 @@ export default abstract class Mission { if (status === 'initializing') { this.assignJobs(); } else if (status === 'waiting') { - this.start(); + this.startMission(); } return status === 'ready'; @@ -156,78 +148,69 @@ export default abstract class Mission { this.initialize(); } + /** + * Gets current status of mission. + */ public getStatus(): MissionStatus { return this.status; } + /** + * Gets vehicles passed on to mission when mission started. + */ public getVehicles(): { [vehicleId: number]: Vehicle } { return this.vehicles; } /** - * Initializes the mission. This is run first, and will run assignJobs if run successfully. - * Will set status to "initlializing" after running successfully. + * Initializes the mission. Called from the Message's constructor. + * Will set status to "initializing" if mission successfully initializes. */ private initialize(): void { - // Stops mission if mission is not in the "ready" state. if (this.status !== 'ready') { this.stop(false, `Something wrong happend in ${this.missionName} mission`); return; } // Fails to initialize if provided job types do not match up to this mission's job types. - if (!this.checkJobTypesAndActiveVehicles()) { - ipc.postLogMessages({ - type: 'failure', - message: `Cannot start mission: Provided vehicles for ${this.missionName} is incomplete`, - }); - this.statusEventHandler.event('status', 'ready'); + if (!this.checkActiveVehicleMapping()) { + this.stop(false, `Provided active vehicle mapping for ${this.missionName} is incomplete`); return; } - /* - * Sets mission status to "initializing". This will trigger the assignJobs function. - */ this.statusEventHandler.event('status', 'initializing'); } /** - * Assigns vehicles the jobs. Will set the mission status to "waiting" once - * all vehicles have been assigned their jobs successfully. + * Assigns vehicles the jobs to complete this mission. Called by statusEventHandler + * after initialize(). Will set status to "waiting" if mission successfully + * assigns jobs to vehicles. */ private assignJobs(): void { - // Fails to assign jobs if mission is not in the "initializing" state. if (this.status !== 'initializing') { this.stop(false, `Something wrong happened in ${this.missionName} mission`); return; } - let pendingVehicleIds: number[] = []; - const assignedVehicleIds: number[] = []; - Object.keys(this.activeVehicleMapping).forEach((vehicleIdString): void => { - const vehicleId = parseInt(vehicleIdString, 10); - pendingVehicleIds.push(vehicleId); - }); + const pendingAssignVehicleIds = Object.keys(this.activeVehicleMapping).map( + (vehicleIdString): number => parseInt(vehicleIdString, 10), + ); - /* - * The three functions below are callbacks to when we assign the vehicle its job. - * I put them here for readability. - */ + const allVehiclesReady = pendingAssignVehicleIds.some((vehicleId): boolean => this.vehicles[vehicleId].getStatus() !== 'ready'); + + if (!allVehiclesReady) { + this.stop(false, `Cannot assign jobs to all vehicles in ${this.missionName} as some are not in a ready state`); + return; + } /** - * Triggers when vehicle successfully receives the job assignment. + * Callback when a vehicle successfully receives the job assignment. + * Go to "waiting" state when all vehicles have been assigned a job. */ const onSuccess = (vehicleId: number): void => { - // Remove pendingId when vehicle is assigned the job successfully. - pendingVehicleIds = pendingVehicleIds.filter((v): boolean => v !== vehicleId); - assignedVehicleIds.push(vehicleId); + pendingAssignVehicleIds.splice(pendingAssignVehicleIds.indexOf(vehicleId), 1); - - /* - * Go to "waiting" state when all vehicles have been assigned a job. - * This will trigger this.start(). - */ - if (pendingVehicleIds.length === 0) { + if (pendingAssignVehicleIds.length === 0) { ipc.postLogMessages({ type: 'success', message: `Assigned jobs to all vehicles for ${this.missionName} mission`, @@ -238,19 +221,15 @@ export default abstract class Mission { }; /** - * Triggers when vehicle fails to accept the job assignment. + * Callback when a vehicle fails to acknowledge the job assignment. */ const onDisconnect = (vehicleId: number): void => { - /* - * Stop mission if any vehicle failed to get the job. Also stops sending messages - * to any other vehicle if there is somehow a "start" message on queue. - */ ipc.postStopSendingMessages(); this.stop(false, `Failed to assign job to ${(vehicleConfig.vehicleInfos[vehicleId] as VehicleInfo).name}, as it has disconnected`); }; /** - * Triggers when vehicle performing job enters an error state. + * Callback when a vehicle performing a job enters an error state. */ const onError = (vehicleId: number, message?: string): void => { ipc.postLogMessages({ @@ -261,101 +240,104 @@ export default abstract class Mission { this.handleUnresponsiveVehicle(vehicleId); }; - // Assign jobs to vehicles. - pendingVehicleIds.forEach((vehicleId): void => { + pendingAssignVehicleIds.forEach((vehicleId): void => { const jobType = this.activeVehicleMapping[vehicleId]; - const assigned = this.vehicles[vehicleId].assignJob(jobType, + this.vehicles[vehicleId].assignJob(jobType, (): void => onSuccess(vehicleId), (): void => onDisconnect(vehicleId), (message): void => onError(vehicleId, message)); - - // If vehicle was not in the "ready" state when assigned. - if (!assigned) { - ipc.postStopSendingMessages(); - this.stop(false, `Failed to assign job to ${(vehicleConfig.vehicleInfos[vehicleId] as VehicleInfo).name} as it was not in a ready state`); - } }); } /** - * Gets all the tasks and starts assigning them to vehicles. + * Gets all the tasks and starts assigning them to vehicles. Called by statusEventHandler + * after assignJobs(). Will set status to "running" if mission successfully + * assign tasks to vehicles. */ - private start(): void { - // Fails to initialize if mission is not in the "ready" state. + private startMission(): void { if (this.status !== 'waiting') { this.stop(false, `Something wrong happened in ${this.missionName} mission`); return; } - // Adds all the tasks to the waitingTasks, if the task's jobType relates to this mission. const jobTasks = this.generateTasks(); - Object.keys(jobTasks).forEach((jobType): void => { - if (vehicleConfig.isValidJobType(jobType) && this.jobTypes.has(jobType as JobType)) { + const allValidTasksForJobs = jobTasks.keys().some( + (jobType): boolean => !vehicleConfig.isValidJobType(jobType as JobType) + || jobTasks.some( + jobType, + (task): boolean => !vehicleConfig.isValidTaskTypeForJob( + task.taskType, + jobType as JobType, + ), + ), + ); + + if (!allValidTasksForJobs) { + this.stop(false, `Generated tasks in ${this.missionName} are invalid for their respective jobs`); + return; + } + + jobTasks.keys().forEach((jobType): void => { + if (this.jobTypes.has(jobType as JobType)) { + // Adds tasks in custom order if there is a compare provided for that job type. if (this.addTaskCompare[jobType]) { - // Will happen if there is a compare for this job type. - (jobTasks.get(jobType) as Task[]).forEach((task): void => { + jobTasks.forEach(jobType, (task): void => { const index = searchIndex( this.waitingTasks.get(jobType) || [], task, this.addTaskCompare[jobType] as CompareTaskFunction, ); + this.waitingTasks.insert(jobType, index, task); }); } else { - this.waitingTasks.push(jobType, ...jobTasks.get(jobType) as Task[]); + this.waitingTasks.push(jobType, ...(jobTasks.get(jobType) || [])); } } }); - /* - * Remaps the activeVehicleMapping to waitingVehicles - * (vehicleId => jobType to jobType => Vehicle[]). - * - * Using waitingVehicles to assign vehicles tasks is faster than using activeVehicleMapping, - * hence we need waitingVehicles. It is also more easy to understand. - */ - Object.keys(this.activeVehicleMapping).forEach((vehicleIdString): void => { - const vehicleId = parseInt(vehicleIdString, 10); + const pendingAssignVehicleIds = Object.keys(this.activeVehicleMapping).map( + (vehicleIdString): number => parseInt(vehicleIdString, 10), + ); + + // Puts vehicles in activeVehicleMapping to waitingVehicles. + pendingAssignVehicleIds.forEach((vehicleId): void => { const jobType = this.activeVehicleMapping[vehicleId]; const vehicle = this.vehicles[vehicleId]; this.waitingVehicles.push(jobType, vehicle); }); + const allVehiclesWaiting = pendingAssignVehicleIds.some( + (vehicleId): boolean => this.vehicles[vehicleId].getStatus() !== 'waiting', + ); + + if (!allVehiclesWaiting) { + this.stop(false, `Cannot assign tasks to all vehicles in ${this.missionName} as some are not in a waiting state`); + return; + } + /* * Assign tasks to vehicles. Will keep assigning tasks to vehicles until there is either * no more available waiting vehicles or no more available waiting tasks. * - * We add these tasks to activeTasks, where we keep track of all tasks that are being executed. * The mission is done when there are no more tasks inside activeTasks, as well as no more * tasks in waitingTasks. */ - Object.values(this.activeVehicleMapping).forEach((jobType): void => { + this.waitingVehicles.keys().forEach((jobType): void => { while (this.waitingTasks.size(jobType) > 0 && this.waitingVehicles.size(jobType) > 0) { const task = this.waitingTasks.shift(jobType) as Task; const vehicle = this.waitingVehicles.shift(jobType) as Vehicle; - if (!this.assignTask(vehicle, task)) return; + this.assignTask(vehicle, task); this.activeTasks.set(vehicle.getVehicleId(), task); } }); - // Update mission status to "running". this.statusEventHandler.event('status', 'running'); } - private assignTask(vehicle: Vehicle, task: Task): boolean { - const assigned = vehicle.assignTask(task); - - if (!assigned) { - ipc.postStopSendingMessages(); - this.stop(false, `Failed to assign task to ${(vehicleConfig.vehicleInfos[vehicle.getVehicleId()] as VehicleInfo).name} as it was not in a waiting state`); - } - - return assigned; - } - /** * This will be called from the Orchestrator. The Orchestrator should make sure that the * vehicleId being provided is a valid vehicleId of a vehicle that was connected when the @@ -364,13 +346,6 @@ export default abstract class Mission { * Subclasses can override this to check for even more types of messages. */ public update(jsonMessage: JSONMessage): void { - /* - * The base mission class only checks for update and completion messages. The base class will - * either reassign the vehicle the task, put the vehicle to wait for further tasks, - * or complete the mission when it receives a completion message. The update message - * is to simply keep its track of vehicles up to date. - */ - if (messageTypeGuard.isUpdateMessage(jsonMessage)) { this.vehicles[jsonMessage.sid].update(jsonMessage); } @@ -378,37 +353,33 @@ export default abstract class Mission { if (messageTypeGuard.isCompleteMessage(jsonMessage)) { const jobType = this.activeVehicleMapping[jsonMessage.sid]; + /* + * Mission is not yet finished, continue to assign tasks. One of the following will happen: + * 1. Assigns next task to vehicle. + * 2. Puts vehicle to waitingVehicles until new task for that job arrives. + */ if (this.waitingTasks.size(jobType) > 0) { - // Assigns the next task in that job to the vehicle. const newTask = this.waitingTasks.shift(jobType) as Task; if (!this.assignTask(this.vehicles[jsonMessage.sid], newTask)) return; this.activeTasks.set(jsonMessage.sid, newTask); } else { - // All tasks in that jobType are complete, so this vehicle will go back to waiting vehicles. this.waitingVehicles.push(jobType, this.vehicles[jsonMessage.sid]); this.activeTasks.delete(jsonMessage.sid); } - /* - * Will execute if mission is finally complete (no waiting tasks, no tasks happening). - * This includes sending completeMission to the user interface and the Orchestrator as well - * as perform a callback function that will use the terminated data (data that was obtained) - * throughout the mission. - */ + // Mission is finished. if (this.activeTasks.size === 0 && this.waitingTasks.size() === 0) { this.stop(true); } else { - // This should never happen. There is a big issue in the system if this somehow happens. - this.stop(false, `Failed to complete ${this.missionName} mission`); + this.stop(false, `${this.missionName} is not able to complete`); // Should never happen. } } } /** - * Adds a new task to the mission. Should only be called on its subclasses to add a task. - * For example, add tasks as certain information is found while the mission happens, or - * when user needs a vehicle to manually perform a task before another. + * Support function to a new task to the mission. Should only be called on its subclasses + * to add a task. * * @param jobType The job to add the task to. * @param task The task object itself. @@ -416,28 +387,25 @@ export default abstract class Mission { * instead of the back. */ protected addTask(jobType: string, task: Task, addToFront?: boolean): void { - // Fails to initialize if mission is not in the "ready" state. if (this.status !== 'running') { this.stop(false, `Tried to add ${task.taskType} task while ${this.missionName} is not running`); return; } + /* + * One of the four will happen: + * 1. Assign task to vehicle right away, if a vehicle is in waitingVehicles. + * 2. Add task to front of waitingTasks if user wants it in the front. + * 3. Add tasks in custom order if there is a compare provided for that job type. + * 4. Add task to the back of waitingTasks (default). + */ const vehicle = this.waitingVehicles.shift(jobType); if (vehicle) { - /* - * Assign the task right away if there is a vehicle waiting for a task for that - * specific jobType. - */ if (!this.assignTask(vehicle, task)) return; this.activeTasks.set(vehicle.getVehicleId(), task); } else if (addToFront) { - // Add to front of waitingTasks if requested. this.waitingTasks.unshift(jobType, task); } else if (this.addTaskCompare[jobType] && this.waitingTasks.get(jobType)) { - /* - * Add to proper spot in task (these tasks in this job are ordered) if there - * is a compare function. - */ const index = searchIndex( this.waitingTasks.get(jobType) as Task[], task, @@ -445,28 +413,53 @@ export default abstract class Mission { ); this.waitingTasks.insert(jobType, index, task); } else { - // Simply add the task to the back of the list. this.waitingTasks.push(jobType, task); } } /** - * Stops the mission (either means mission was stopped manually or mission was completed). - * Will print out terminated data (this mission's parameters) if the mission was - * stopped through an error (or manually stopped). + * Support function to assign a task to a vehicle. Stops mission if mission + * fails to assign task to vehicle. + * + * @param vehicle Vehicle to assign task to. + * @param task Task to assign vehicle. + */ + private assignTask(vehicle: Vehicle, task: Task): boolean { + const success = vehicle.assignTask(task); + + if (!success) { + ipc.postStopSendingMessages(); + this.stop(false, `Failed to assign task to ${(vehicleConfig.vehicleInfos[vehicle.getVehicleId()] as VehicleInfo).name} as it was not in a waiting state`); + } + + return success; + } + + /** + * Support function to stop the mission. Mission is stopped when an error occurs and it needs + * to be terminated, or when all tasks are successfully finished. * * @param success True if the mission was completed, false if the mission was stopped * manually/through error. * @param error The error message (if success === false). */ private stop(success: boolean, error?: string): void { - Object.keys(this.activeVehicleMapping).forEach((vehicleIdString): void => { - const vehicleId = parseInt(vehicleIdString, 10); - this.vehicles[vehicleId].stop(); - }); + if (this.status !== 'ready') { + if (this.status === 'initializing') ipc.postStopSendingMessages(); + + Object.keys(this.activeVehicleMapping).forEach((vehicleIdString): void => { + const vehicleId = parseInt(vehicleIdString, 10); + this.vehicles[vehicleId].stop(); + }); + } this.statusEventHandler.event('status', 'ready'); + /* + * Generates and prints out parameters. Completion parameters are parameters used + * for the next mission, and termination parameters are the parameters used for this + * mission. + */ if (success) { const completionParameters = this.generateCompletionParameters(); if (!completionParameters) { @@ -474,59 +467,49 @@ export default abstract class Mission { return; } - ipc.postCompleteMission(this.missionName, completionParameters as MissionParameters); + ipc.postCompleteMission(this.missionName, completionParameters); ipc.postLogMessages({ type: 'success', message: `Completion parameters for ${this.missionName}: ${JSON.stringify(completionParameters)}`, }); } else { - ipc.postStopMission(); + ipc.postStopMissions(); ipc.postLogMessages({ type: 'failure', message: `Stopped mission: ${error}`, }, { - /* - * Put terminated parameters on stop (on fail) so that the user can start the mission - * over again with the terminated data (assume this was no the first mission that ran, - * the user should not have to start from first mission since he/she did not know the - * parameters for this mission). This can also apply to the first mission, cause the user - * might not have written the parameters in paper. - */ message: `Terminated parameters for ${this.missionName}: ${JSON.stringify(this.parameters)}`, }); } } /** - * Check to ensure the following: + * Support function to check that the provided activeVehicleMapping variable is properly set. + * Performs check after the mission class has filtered out all irrelevant jobs from the + * activeVehicleMapping. + * + * Ensures the following: * 1. All job types provided cover the required job types for this mission. * 2. There is a vehicle assigned to each job type. * - * @param activeVehicleMapping User provided map of vehicle to their job type, function will check - * if this mapping is valid. + * @param activeVehicleMapping User provided map of vehicle to their job type. */ - private checkJobTypesAndActiveVehicles(): boolean { - const vehicleIds = Object.keys(this.activeVehicleMapping); + private checkActiveVehicleMapping(): boolean { + const providedJobTypes = new Set(Object.values(this.activeVehicleMapping)); - /* - * Check #1: ensure the jobs in the activeVehicleMapping cover the required jobs for this - * mission. - */ - if (!Array.from(Object.keys(this.activeVehicleMapping)).every((vehicleIdString): boolean => { - const vehicleId = parseInt(vehicleIdString, 10); - return this.jobTypes.has(this.activeVehicleMapping[vehicleId]); - })) { - return false; - } + const hasRequiredJobTypes = Array.from(this.jobTypes).every( + (requiredJobType): boolean => providedJobTypes.has(requiredJobType), + ); - /* - * Check #2: if all jobs in the activeVehicleMapping cover the required jobs for the mission, - * then there is a vehicle mapped for every job already. We need to check that all these - * vehicles are in the "ready" state too. - */ - if (!vehicleIds.every((vehicleId): boolean => this.vehicles[parseInt(vehicleId, 10)].getStatus() === 'ready')) { - return false; - } + if (!hasRequiredJobTypes) return false; + + const hasValidVehicleAssigned = Object.keys(this.activeVehicleMapping).map( + (vehicleIdString): number => parseInt(vehicleIdString, 10), + ).every((vehicleId): boolean => vehicleConfig.isValidVehicleId(vehicleId) + && this.vehicles[vehicleId] + && this.vehicles[vehicleId].getJobs().includes(this.activeVehicleMapping[vehicleId])); + + if (!hasValidVehicleAssigned) return false; return true; } @@ -535,54 +518,29 @@ export default abstract class Mission { * Handles a vehicle that is assigned a job when it goes to an error state. Tries to assign * another vehicle that can perform the same task and remove that vehicle. If not possible, * simply stops the mission. - * - * If no tasks is being executed, stop the mission. - * - * This never gets executed while mission's status is "ready", since no vehicles are assigned - * then. */ private handleUnresponsiveVehicle(vehicleId: number): void { - /* - * In the case that a vehicle that is not performing a job is in an error state - * and somehow this function gets called (which it shouldn't), let the vehicle - * be. - * - * Again, this should never be called for a vehicle that is not performing a job. - * All vehicles in the "ready" state are assumed to be stable (whether they're airborne, - * underwater, or on land). - */ + // Ignore vehicles that are not related to the mission. if (!this.activeVehicleMapping[vehicleId]) return; if (this.status === 'waiting') { - // Simply exit the mission if we are in the "waiting" stage. - this.stop(false, 'Take manual control over vehicle'); + this.stop(false, `Take manual control over ${(vehicleConfig.vehicleInfos[vehicleId] as VehicleInfo).name}`); } else if (this.status === 'initializing') { - // Stop sending all extra start messages and exit the mission. ipc.postStopSendingMessages(); - this.stop(false, 'Take manual control over vehicle'); + this.stop(false, `Take manual control over ${(vehicleConfig.vehicleInfos[vehicleId] as VehicleInfo).name}`); } else { const jobType = this.activeVehicleMapping[vehicleId]; - // Delete the error vehicle from vehicle mapping. delete this.activeVehicleMapping[vehicleId]; - /* - * Delete the task related to error vehicle and insert it to front of waitingTasks. - * It is possible that there is no tasks related to the vehicle and that the - * vehicle has errored out while waiting to be assigned a task (vehicle status was - * "waiting"). - */ + // Put the task vehicle is performing back to the front of waitingTasks. if (this.activeTasks.has(vehicleId)) { const task = this.activeTasks.get(vehicleId) as Task; this.activeTasks.delete(vehicleId); - this.addTask(jobType, task, true); } - /* - * Try to reassign that task (if there was one) to a waiting vehicle that can - * perform that job. - */ + // Get the next vehicle that can perform the job to start doing that task. const newVehicle = this.waitingVehicles.shift(jobType); const task = this.waitingTasks.shift(jobType); if (newVehicle && task) { @@ -590,8 +548,13 @@ export default abstract class Mission { if (!this.assignTask(newVehicle, task)) return; this.activeTasks.set(newVehicle.getVehicleId(), task); + + ipc.postLogMessages({ + type: 'success', + message: `Reassigned ${jobType} job to ${(vehicleConfig.vehicleInfos[newVehicle.getVehicleId()] as VehicleInfo).name}`, + }); } else { - this.stop(false, 'Failed to reassign job, take manual control over vehicle'); + this.stop(false, `Failed to assign manual control over ${(vehicleConfig.vehicleInfos[vehicleId] as VehicleInfo).name}`); } } } @@ -606,5 +569,5 @@ export default abstract class Mission { * user or to the next mission. For example, data produced here for an ISR Search mission should * be data for the VTOL Search mission (of type VTOLSearchMissionParameters). */ - protected abstract generateCompletionParameters(): MissionParameters | undefined; + protected abstract generateCompletionParameters(): MissionParameters; } diff --git a/src/common/struct/Vehicle.ts b/src/common/struct/Vehicle.ts index 4c414cc7..3bfa7a95 100644 --- a/src/common/struct/Vehicle.ts +++ b/src/common/struct/Vehicle.ts @@ -263,7 +263,7 @@ export default class Vehicle { } else if (value === 'disconnected') { if (disconnectionCallback) disconnectionCallback(); } - return value === 'waiting'; + return value === 'waiting' || value === 'disconnected'; }); return true; diff --git a/src/types/messages.ts b/src/types/messages.ts index 4d88c9e9..67bddc62 100644 --- a/src/types/messages.ts +++ b/src/types/messages.ts @@ -441,7 +441,7 @@ export interface MissionInformation { /** * Parameters for the mission. */ - parameters: MissionParameters; + parameters?: MissionParameters; /** * Options for the mission. diff --git a/src/util/ipc.ts b/src/util/ipc.ts index e606b8d3..9cf0c250 100644 --- a/src/util/ipc.ts +++ b/src/util/ipc.ts @@ -279,13 +279,13 @@ function postStartNextMission(): void { } /** - * Post "stopMission" notification. + * Post "stopMissions" notification. * * Files that take this notification: * - common/Orchestrator */ -function postStopMission(): void { - ipcRenderer.send('post', 'stopMission'); +function postStopMissions(): void { + ipcRenderer.send('post', 'stopMissions'); } /** @@ -361,7 +361,7 @@ export default { postShowMissionWindow, postStartMissions, postStartNextMission, - postStopMission, + postStopMissions, postStopSendingMessages, postToggleTheme, postUpdateMapLocation, From aea50e793888413ed60935b4fd9b9f8c161f435d Mon Sep 17 00:00:00 2001 From: Paul Matthew Barrameda Date: Tue, 16 Apr 2019 11:41:22 -0700 Subject: [PATCH 089/152] Fixed ISRSearch's generateCompletionParameters function --- src/common/missions/ISRSearch.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/common/missions/ISRSearch.ts b/src/common/missions/ISRSearch.ts index f1836bbc..8d6bdf6c 100644 --- a/src/common/missions/ISRSearch.ts +++ b/src/common/missions/ISRSearch.ts @@ -68,14 +68,15 @@ class ISRSearch extends Mission { return tasks; } - protected generateCompletionParameters(): VTOLSearchMissionParameters | undefined { + protected generateCompletionParameters(): VTOLSearchMissionParameters { // Will be changed depending on what VTOL wants from UAV. if (this.missionData.length === 0) { ipc.postLogMessages({ type: 'failure', message: 'No points of interests were found in the mission', }); - return undefined; + + throw new Error('No points of interests were found in the mission'); } let bottom: number = Number.MIN_VALUE; From 36d6d3fe8bef24f77e286e3eeddc4fed73be0042 Mon Sep 17 00:00:00 2001 From: Paul Matthew Barrameda Date: Tue, 16 Apr 2019 12:00:56 -0700 Subject: [PATCH 090/152] (Re)added undefined as an option for generateTasks/generatedCompletionParameters Throws an error, and mission will stop if undefined is returned. --- src/common/missions/ISRSearch.ts | 21 +++++++++++---------- src/common/struct/Mission.ts | 20 ++++++++++++++++---- 2 files changed, 27 insertions(+), 14 deletions(-) diff --git a/src/common/missions/ISRSearch.ts b/src/common/missions/ISRSearch.ts index 8d6bdf6c..8195f7dd 100644 --- a/src/common/missions/ISRSearch.ts +++ b/src/common/missions/ISRSearch.ts @@ -31,21 +31,24 @@ class ISRSearch extends Mission { protected addTaskCompare = {}; /** - * List of point of interests to get parameters for next mission (VTOL Search). + * List of point of interests. */ private missionData: LatLngZoom[] = []; - protected generateTasks(): DictionaryList { - // Check if options and parameters line up. + protected generateTasks(): DictionaryList | undefined { if (!this.options.isrSearch || missionParametersTypeGuard.isISRSearchMissionParameters(this.parameters)) { - throw new Error('Failed to generate tasks as options/parameters were invalid'); + ipc.postLogMessages({ + type: 'failure', + message: `Failed to generate tasks for ${this.missionName} as parameters were invalid`, + }); + + return undefined; // Mission will stop from this, not complete. } - const missionParameters = this.parameters as ISRSearchMissionParameters; + const missionParameters = this.parameters as ISRSearchMissionParameters; const tasks = new DictionaryList(); - // Add takeoff to our tasks. if (!this.options.isrSearch.noTakeoff) { tasks.push('isrSearch', { taskType: 'takeoff', @@ -68,15 +71,13 @@ class ISRSearch extends Mission { return tasks; } - protected generateCompletionParameters(): VTOLSearchMissionParameters { - // Will be changed depending on what VTOL wants from UAV. + protected generateCompletionParameters(): VTOLSearchMissionParameters | undefined { if (this.missionData.length === 0) { ipc.postLogMessages({ type: 'failure', message: 'No points of interests were found in the mission', }); - - throw new Error('No points of interests were found in the mission'); + return undefined; // Mission will stop from this, not complete. } let bottom: number = Number.MIN_VALUE; diff --git a/src/common/struct/Mission.ts b/src/common/struct/Mission.ts index 90f02df2..d617ca50 100644 --- a/src/common/struct/Mission.ts +++ b/src/common/struct/Mission.ts @@ -109,6 +109,11 @@ export default abstract class Mission { activeVehicleMapping: { [vehicleId: number]: JobType }, ) { if (!information.parameters) { + ipc.postLogMessages({ + type: 'failure', + message: 'Parameters for mission are not provided', + }); + throw new Error('Parameters for mission are not provided'); } @@ -262,6 +267,11 @@ export default abstract class Mission { } const jobTasks = this.generateTasks(); + if (!jobTasks) { + this.stop(false, `No tasks were generated from ${this.missionName}`); + return; + } + const allValidTasksForJobs = jobTasks.keys().some( (jobType): boolean => !vehicleConfig.isValidJobType(jobType as JobType) || jobTasks.some( @@ -463,7 +473,7 @@ export default abstract class Mission { if (success) { const completionParameters = this.generateCompletionParameters(); if (!completionParameters) { - this.stop(false, 'No parameters were generated from this mission'); + this.stop(false, `No parameters were generated from ${this.missionName}`); return; } @@ -476,7 +486,7 @@ export default abstract class Mission { ipc.postStopMissions(); ipc.postLogMessages({ type: 'failure', - message: `Stopped mission: ${error}`, + message: `Stopped ${this.missionName} mission: ${error}`, }, { message: `Terminated parameters for ${this.missionName}: ${JSON.stringify(this.parameters)}`, }); @@ -561,13 +571,15 @@ export default abstract class Mission { /** * Generates all tasks to perform, given this.options and this.parameters. + * Returns undefined if not able to generate tasks. */ - protected abstract generateTasks(): DictionaryList; + protected abstract generateTasks(): DictionaryList | undefined; /** * Generates new mission parameters after the mission has completed, either to be given to the * user or to the next mission. For example, data produced here for an ISR Search mission should * be data for the VTOL Search mission (of type VTOLSearchMissionParameters). + * Returns undefined if not able to generate parameters. */ - protected abstract generateCompletionParameters(): MissionParameters; + protected abstract generateCompletionParameters(): MissionParameters | undefined; } From 567cb6e9c4da25cd042d0d33e2e9602ebf616e52 Mon Sep 17 00:00:00 2001 From: Paul Matthew Barrameda Date: Wed, 17 Apr 2019 02:08:57 -0700 Subject: [PATCH 091/152] Updated typings Allocated the fat messages.ts file into many files that make more sense. Removed float hex, as we will use compression with msgpack to make things work. Also removed "options" from StartMessage as I believe this is difficult to handle in other languages (C in particular). --- src/common/Orchestrator.ts | 64 +- src/common/Xbee.ts | 2 +- src/common/missions/ISRSearch.ts | 79 +- src/common/missions/index.ts | 2 +- src/common/struct/Mission.ts | 64 +- src/common/struct/Vehicle.ts | 11 +- src/main/index.ts | 4 +- src/renderer/App.tsx | 2 +- .../fixtures/updateMessagesFixtures.ts | 2 +- .../fixtures/updateVehiclesFixtures.ts | 2 +- src/renderer/mainWindow/MainWindow.tsx | 2 +- src/renderer/mainWindow/log/LogContainer.tsx | 27 +- src/renderer/mainWindow/map/MapContainer.tsx | 15 +- src/renderer/mainWindow/map/VehicleMarker.tsx | 3 +- .../mainWindow/map/control/ThemeControl.tsx | 2 +- .../mainWindow/vehicle/VehicleContainer.tsx | 4 +- .../mainWindow/vehicle/VehicleTable.tsx | 3 +- src/renderer/missionWindow/MissionWindow.tsx | 2 +- src/static/index.ts | 29 +- src/types/componentStyle.ts | 50 ++ src/types/fileOption.ts | 26 + src/types/message.ts | 338 ++++++++ src/types/messages.ts | 754 ------------------ src/types/missionInformation.ts | 152 ++++ src/types/task.ts | 311 ++++++++ src/types/tasks.ts | 0 src/types/types.ts | 144 ---- src/types/vehicle.ts | 54 ++ src/util/ipc.ts | 50 +- src/util/util.ts | 2 +- 30 files changed, 1097 insertions(+), 1103 deletions(-) create mode 100644 src/types/componentStyle.ts create mode 100644 src/types/fileOption.ts create mode 100644 src/types/message.ts delete mode 100644 src/types/messages.ts create mode 100644 src/types/missionInformation.ts create mode 100644 src/types/task.ts delete mode 100644 src/types/tasks.ts delete mode 100644 src/types/types.ts create mode 100644 src/types/vehicle.ts diff --git a/src/common/Orchestrator.ts b/src/common/Orchestrator.ts index 2420a971..f334b7ca 100644 --- a/src/common/Orchestrator.ts +++ b/src/common/Orchestrator.ts @@ -1,18 +1,10 @@ import { ipcRenderer, Event } from 'electron'; -import { - config, - vehicleConfig, - VehicleInfo, -} from '../static/index'; - -import { - BadMessage, - ConnectMessage, - JSONMessage, - MissionDescription, - MissionParameters, -} from '../types/messages'; +import { config, vehicleConfig, VehicleInfo } from '../static/index'; + +import * as Message from '../types/message'; +import * as MissionInformation from '../types/missionInformation'; +import * as Task from '../types/task'; import ipc from '../util/ipc'; @@ -29,7 +21,7 @@ class Orchestrator { * Acknowledges a message. All messages are passed here through the MessageHandler. * Only messages that are no acknowledged are bad messages and acknowledgements. */ - private static acknowledgeMessage(jsonMessage: JSONMessage): void { + private static acknowledgeMessage(jsonMessage: Message.JSONMessage): void { ipc.postSendMessage(jsonMessage.sid, { type: 'ack', ackid: jsonMessage.id, @@ -49,7 +41,7 @@ class Orchestrator { /** * List of mission names and information for each mission. */ - private missions: MissionDescription[] = []; + private missions: MissionInformation.Information[] = []; /** * Current index of missions that mission is performing. @@ -81,26 +73,26 @@ class Orchestrator { private vehiclePinger = new UpdateHandler(); public constructor() { - ipcRenderer.on('connectToVehicle', (_: Event, jsonMessage: JSONMessage): void => this.connectToVehicle(jsonMessage)); + ipcRenderer.on('connectToVehicle', (_: Event, jsonMessage: Message.JSONMessage): void => this.connectToVehicle(jsonMessage)); ipcRenderer.on('disconnectFromVehicle', (_: Event, vehicleId: number): void => this.disconnectFromVehicle(vehicleId)); - ipcRenderer.on('acknowledgeMessage', (_: Event, jsonMessage: JSONMessage): void => Orchestrator.acknowledgeMessage(jsonMessage)); + ipcRenderer.on('acknowledgeMessage', (_: Event, jsonMessage: Message.JSONMessage): void => Orchestrator.acknowledgeMessage(jsonMessage)); - ipcRenderer.on('handleAcknowledgementMessage', (_: Event, jsonMessage: JSONMessage): void => this.handleAcknowledgementMessage(jsonMessage)); - ipcRenderer.on('handleBadMessage', (_: Event, jsonMessage: JSONMessage): void => this.handleBadMessage(jsonMessage)); - ipcRenderer.on('handleUpdateMessage', (_: Event, jsonMessage: JSONMessage): void => this.handleUpdateMessage(jsonMessage)); - ipcRenderer.on('handlePOIMessage', (_: Event, jsonMessage: JSONMessage): void => this.handlePOIMessage(jsonMessage)); - ipcRenderer.on('handleCompleteMessage', (_: Event, jsonMessage: JSONMessage): void => this.handleCompleteMessage(jsonMessage)); + ipcRenderer.on('handleAcknowledgementMessage', (_: Event, jsonMessage: Message.JSONMessage): void => this.handleAcknowledgementMessage(jsonMessage)); + ipcRenderer.on('handleBadMessage', (_: Event, jsonMessage: Message.JSONMessage): void => this.handleBadMessage(jsonMessage)); + ipcRenderer.on('handleUpdateMessage', (_: Event, jsonMessage: Message.JSONMessage): void => this.handleUpdateMessage(jsonMessage)); + ipcRenderer.on('handlePOIMessage', (_: Event, jsonMessage: Message.JSONMessage): void => this.handlePOIMessage(jsonMessage)); + ipcRenderer.on('handleCompleteMessage', (_: Event, jsonMessage: Message.JSONMessage): void => this.handleCompleteMessage(jsonMessage)); ipcRenderer.on('startMissions', ( _: Event, - missions: MissionDescription[], + missions: MissionInformation.Information[], requireConfirmation: boolean, ): void => this.startMissions(missions, requireConfirmation)); ipcRenderer.on('startNextMission', this.startNextMission); - ipcRenderer.on('completeMission', (_: Event, missionName: string, completionParameters: MissionParameters): void => this.completeMission(missionName, completionParameters)); + ipcRenderer.on('completeMission', (_: Event, missionName: string, completionParameters: Task.TaskParameters[]): void => this.completeMission(missionName, completionParameters)); ipcRenderer.on('stopMissions', this.stopMissions); } @@ -108,8 +100,8 @@ class Orchestrator { * Connects to vehicle specified by the connect message. * @param jsonMessage The connect message. */ - private connectToVehicle(jsonMessage: JSONMessage): void { - const connectMessage = jsonMessage as ConnectMessage; + private connectToVehicle(jsonMessage: Message.JSONMessage): void { + const connectMessage = jsonMessage as Message.ConnectMessage; this.vehicles[jsonMessage.sid] = new Vehicle({ sid: jsonMessage.sid, @@ -153,7 +145,7 @@ class Orchestrator { * Handles acknowledgement messages. * @param jsonMessage Message from vehicle. */ - private handleAcknowledgementMessage(jsonMessage: JSONMessage): void { + private handleAcknowledgementMessage(jsonMessage: Message.JSONMessage): void { if (!this.vehicles[jsonMessage.sid] || this.vehicles[jsonMessage.sid].getStatus() === 'disconnected') { Orchestrator.postOrchestratorError(`Received acknowledgement message from disconnected vehicle id ${jsonMessage.sid}`); return; @@ -165,7 +157,7 @@ class Orchestrator { * Handles bad messages. * @param jsonMessage Message from vehicle. */ - private handleBadMessage(jsonMessage: JSONMessage): void { + private handleBadMessage(jsonMessage: Message.JSONMessage): void { if (!this.vehicles[jsonMessage.sid] || this.vehicles[jsonMessage.sid].getStatus() === 'disconnected') { Orchestrator.postOrchestratorError(`Received bad message from disconnected vehicle id ${jsonMessage.sid}`); return; @@ -173,7 +165,7 @@ class Orchestrator { this.vehicles[jsonMessage.sid].updateLastConnectionTime(jsonMessage); - const badMessage = jsonMessage as BadMessage; + const badMessage = jsonMessage as Message.BadMessage; ipc.postLogMessages({ type: 'failure', message: `Received bad message from ${(vehicleConfig.vehicleInfos[jsonMessage.sid] as VehicleInfo).name}: ${badMessage.error || 'No error message specified'}}`, @@ -184,7 +176,7 @@ class Orchestrator { * Handles update messages. * @param jsonMessage Message from vehicle. */ - private handleUpdateMessage(jsonMessage: JSONMessage): void { + private handleUpdateMessage(jsonMessage: Message.JSONMessage): void { if (!this.vehicles[jsonMessage.sid] || this.vehicles[jsonMessage.sid].getStatus() === 'disconnected') { Orchestrator.postOrchestratorError(`Received update message from disconnected vehicle id ${jsonMessage.sid}`); return; @@ -202,7 +194,7 @@ class Orchestrator { * Handles point of interest messages. * @param jsonMessage Message from vehicle. */ - private handlePOIMessage(jsonMessage: JSONMessage): void { + private handlePOIMessage(jsonMessage: Message.JSONMessage): void { if (!this.vehicles[jsonMessage.sid] || this.vehicles[jsonMessage.sid].getStatus() === 'disconnected') { Orchestrator.postOrchestratorError(`Received point of interest message from ${vehicleConfig.vehicleInfos[jsonMessage.sid]} but it is currently disconnected`); return; @@ -220,7 +212,7 @@ class Orchestrator { * Handles complete messages. * @param jsonMessage Message from vehicle. */ - private handleCompleteMessage(jsonMessage: JSONMessage): void { + private handleCompleteMessage(jsonMessage: Message.JSONMessage): void { if (!this.vehicles[jsonMessage.sid] || this.vehicles[jsonMessage.sid].getStatus() === 'disconnected') { Orchestrator.postOrchestratorError(`Received complete message from disconnected vehicle id ${jsonMessage.sid}`); return; @@ -247,7 +239,7 @@ class Orchestrator { * required for mission. */ private startMissions( - missions: MissionDescription[], + missions: MissionInformation.Information[], requireConfirmation: boolean, ): void { if (this.running) { @@ -276,7 +268,7 @@ class Orchestrator { * being kept track of. * @param completionParameters Paramters for next mission. */ - private completeMission(missionName: string, completionParameters: MissionParameters): void { + private completeMission(missionName: string, completionParameters: Task.TaskParameters[]): void { if (!this.running || (this.currentMissionIndex >= 0 && this.missions[this.currentMissionIndex].missionName !== missionName)) { @@ -288,7 +280,7 @@ class Orchestrator { ipc.postFinishMissions(completionParameters); this.stopMissions(); } else { - this.missions[this.currentMissionIndex + 1].information.parameters = completionParameters; + this.missions[this.currentMissionIndex + 1].parameters = completionParameters; } if (this.requireConfirmation) { @@ -328,7 +320,7 @@ class Orchestrator { this.currentMissionIndex += 1; this.currentMission = new mission.constructor( this.vehicles, - this.missions[this.currentMissionIndex].information, + this.missions[this.currentMissionIndex], this.missions[this.currentMissionIndex].activeVehicleMapping, ); } diff --git a/src/common/Xbee.ts b/src/common/Xbee.ts index 9ffae842..3e5dce8d 100644 --- a/src/common/Xbee.ts +++ b/src/common/Xbee.ts @@ -3,7 +3,7 @@ import { constants as C, Frame, XBeeAPI } from 'xbee-api'; import { VehicleInfo, vehicleConfig } from '../static/index'; -import { JSONMessage } from '../types/messages'; +import { JSONMessage } from '../types/message'; import ipc from '../util/ipc'; diff --git a/src/common/missions/ISRSearch.ts b/src/common/missions/ISRSearch.ts index 8195f7dd..345be274 100644 --- a/src/common/missions/ISRSearch.ts +++ b/src/common/missions/ISRSearch.ts @@ -1,25 +1,17 @@ import DictionaryList from '../struct/DictionaryList'; import Mission from '../struct/Mission'; -import { JobType } from '../../static/index'; - -import { - ISRSearchMissionParameters, - JSONMessage, - messageTypeGuard, - missionParametersTypeGuard, - MissionName, - POIMessage, - Task, - toFloat, - toHexFloat, - VTOLSearchMissionParameters, -} from '../../types/messages'; -import { LatLngZoom } from '../../types/types'; +import { JobType, LatLngZoom } from '../../static/index'; + +import * as Message from '../../types/message'; +import * as MissionInformation from '../../types/missionInformation'; +import { Task, TaskParameters } from '../../types/task'; import ipc from '../../util/ipc'; -export const missionName: MissionName = 'isrSearch'; +import Vehicle from '../struct/Vehicle'; + +export const missionName: MissionInformation.MissionName = 'isrSearch'; export const jobTypes: JobType[] = ['isrSearch']; @@ -30,26 +22,27 @@ class ISRSearch extends Mission { protected addTaskCompare = {}; + protected information: MissionInformation.ISRSearchInformation; + /** * List of point of interests. */ private missionData: LatLngZoom[] = []; - protected generateTasks(): DictionaryList | undefined { - if (!this.options.isrSearch - || missionParametersTypeGuard.isISRSearchMissionParameters(this.parameters)) { - ipc.postLogMessages({ - type: 'failure', - message: `Failed to generate tasks for ${this.missionName} as parameters were invalid`, - }); - - return undefined; // Mission will stop from this, not complete. - } + public constructor( + vehicles: { [vehicleId: number]: Vehicle }, + information: MissionInformation.ISRSearchInformation, + activeVehicleMapping: { [vehicleId: number]: JobType }, + ) { + super(vehicles, information, activeVehicleMapping); + this.information = information; + } - const missionParameters = this.parameters as ISRSearchMissionParameters; + protected generateTasks(): DictionaryList | undefined { + const missionParameters = this.information.parameters; const tasks = new DictionaryList(); - if (!this.options.isrSearch.noTakeoff) { + if (!this.information.options.noTakeoff) { tasks.push('isrSearch', { taskType: 'takeoff', ...missionParameters.takeoff, @@ -61,7 +54,7 @@ class ISRSearch extends Mission { ...missionParameters.isrSearch, }); - if (!this.options.isrSearch.noLand) { + if (!this.information.options.noLand) { tasks.push('isrSearch', { taskType: 'land', ...missionParameters.land, @@ -71,7 +64,7 @@ class ISRSearch extends Mission { return tasks; } - protected generateCompletionParameters(): VTOLSearchMissionParameters | undefined { + protected generateCompletionParameters(): { [key: string]: TaskParameters } | undefined { if (this.missionData.length === 0) { ipc.postLogMessages({ type: 'failure', @@ -98,34 +91,34 @@ class ISRSearch extends Mission { quickScan: { waypoints: [ { - lat: toHexFloat(top), - lng: toHexFloat(left), + lat: top, + lng: left, }, { - lat: toHexFloat(top), - lng: toHexFloat(right), + lat: top, + lng: right, }, { - lat: toHexFloat(bottom), - lng: toHexFloat(left), + lat: bottom, + lng: left, }, { - lat: toHexFloat(bottom), - lng: toHexFloat(right), + lat: bottom, + lng: right, }, ], }, }; } - public update(jsonMessage: JSONMessage): void { + public update(jsonMessage: Message.JSONMessage): void { super.update(jsonMessage); - if (messageTypeGuard.isPOIMessage(jsonMessage)) { - const poiMessage = jsonMessage as POIMessage; + if (Message.TypeGuard.isPOIMessage(jsonMessage)) { + const poiMessage = jsonMessage as Message.POIMessage; this.missionData.push({ - lat: toFloat(poiMessage.lat), - lng: toFloat(poiMessage.lng), + lat: poiMessage.lat, + lng: poiMessage.lng, }); } } diff --git a/src/common/missions/index.ts b/src/common/missions/index.ts index 70fbff00..ce7d080f 100644 --- a/src/common/missions/index.ts +++ b/src/common/missions/index.ts @@ -1,6 +1,6 @@ import { JobType } from '../../static/index'; -import { MissionName } from '../../types/messages'; +import { MissionName } from '../../types/missionInformation'; import Mission from '../struct/Mission'; diff --git a/src/common/struct/Mission.ts b/src/common/struct/Mission.ts index d617ca50..ef264d6d 100644 --- a/src/common/struct/Mission.ts +++ b/src/common/struct/Mission.ts @@ -1,18 +1,8 @@ -import { - JobType, - vehicleConfig, - VehicleInfo, -} from '../../static/index'; - -import { - JSONMessage, - messageTypeGuard, - MissionInformation, - MissionName, - MissionOptions, - MissionParameters, - Task, -} from '../../types/messages'; +import { JobType, vehicleConfig, VehicleInfo } from '../../static/index'; + +import * as Message from '../../types/message'; +import * as MissionInformation from '../../types/missionInformation'; +import { Task, TaskParameters } from '../../types/task'; import ipc from '../../util/ipc'; import { searchIndex } from '../../util/util'; @@ -33,7 +23,7 @@ export default abstract class Mission { /** * Name of mission. */ - protected abstract missionName: MissionName; + protected abstract missionName: MissionInformation.MissionName; /** * Related job types to the mission. @@ -51,17 +41,15 @@ export default abstract class Mission { private status: MissionStatus = 'ready'; /** - * Options for the mission. Generated from the constructor's parameters variable. - * This can determine which tasks are generated as well as other potential things. - * Subclasses implement which tasks get generated so this variable is protected. + * Information for this mission, has three fields: + * 1. The mission's name. + * 2. Options for the mission. Generated from the constructor's parameters variable. + * This can determine which tasks are generated as well as other potential things. + * Subclasses implement which tasks get generated so this variable is protected. + * 3. Parameters for the mission. Used to generate tasks. + * Subclasses implement which tasks get generated so this variable is protected. */ - protected options: MissionOptions; - - /** - * Parameters for the mission. Used to generate tasks. - * Subclasses implement which tasks get generated so this variable is protected. - */ - protected parameters: MissionParameters; + protected information: MissionInformation.Information; /** * Map of the id of the vehicle that is currently performing all tasks @@ -105,21 +93,11 @@ export default abstract class Mission { public constructor( vehicles: { [vehicleId: number]: Vehicle }, - information: MissionInformation, + information: MissionInformation.Information, activeVehicleMapping: { [vehicleId: number]: JobType }, ) { - if (!information.parameters) { - ipc.postLogMessages({ - type: 'failure', - message: 'Parameters for mission are not provided', - }); - - throw new Error('Parameters for mission are not provided'); - } - this.vehicles = vehicles; - this.parameters = information.parameters; - this.options = information.options; + this.information = information; // Filters out any irrelevant jobs when adding to activeVehicleMapping. Object.keys(activeVehicleMapping).forEach((vehicleIdString): void => { @@ -355,12 +333,12 @@ export default abstract class Mission { * * Subclasses can override this to check for even more types of messages. */ - public update(jsonMessage: JSONMessage): void { - if (messageTypeGuard.isUpdateMessage(jsonMessage)) { + public update(jsonMessage: Message.JSONMessage): void { + if (Message.TypeGuard.isUpdateMessage(jsonMessage)) { this.vehicles[jsonMessage.sid].update(jsonMessage); } - if (messageTypeGuard.isCompleteMessage(jsonMessage)) { + if (Message.TypeGuard.isCompleteMessage(jsonMessage)) { const jobType = this.activeVehicleMapping[jsonMessage.sid]; /* @@ -488,7 +466,7 @@ export default abstract class Mission { type: 'failure', message: `Stopped ${this.missionName} mission: ${error}`, }, { - message: `Terminated parameters for ${this.missionName}: ${JSON.stringify(this.parameters)}`, + message: `Terminated parameters for ${this.missionName}: ${JSON.stringify(this.information.parameters)}`, }); } } @@ -581,5 +559,5 @@ export default abstract class Mission { * be data for the VTOL Search mission (of type VTOLSearchMissionParameters). * Returns undefined if not able to generate parameters. */ - protected abstract generateCompletionParameters(): MissionParameters | undefined; + protected abstract generateCompletionParameters(): { [key: string]: TaskParameters } | undefined; } diff --git a/src/common/struct/Vehicle.ts b/src/common/struct/Vehicle.ts index 3bfa7a95..0fdd740b 100644 --- a/src/common/struct/Vehicle.ts +++ b/src/common/struct/Vehicle.ts @@ -4,10 +4,10 @@ import { JSONMessage, Message, StartMessage, - Task, UpdateMessage, -} from '../../types/messages'; -import { VehicleObject, VehicleStatus } from '../../types/types'; +} from '../../types/message'; +import * as Task from '../../types/task'; +import { VehicleObject, VehicleStatus } from '../../types/vehicle'; import ipc from '../../util/ipc'; @@ -240,7 +240,6 @@ export default class Vehicle { completionCallback?: () => void, disconnectionCallback?: () => void, errorCallback?: ErrorCallback, - options?: object, ): boolean { if (this.status !== 'ready') { return false; @@ -254,8 +253,6 @@ export default class Vehicle { jobType, }; - if (options) startMessage.options = options; - this.sendMessage(startMessage); this.updateEventHandler.addHandler('status', (value): boolean => { if (value === 'waiting') { @@ -276,7 +273,7 @@ export default class Vehicle { * * @param task The task for the vehicle to perform. Must support the vehicle's job. */ - public assignTask(task: Task): boolean { + public assignTask(task: Task.Task): boolean { if (this.status !== 'waiting' || !this.assignedJob || !vehicleConfig.isValidTaskTypeForJob(task.taskType, this.assignedJob)) { return false; } diff --git a/src/main/index.ts b/src/main/index.ts index 7b1a5142..413ce0cb 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -13,9 +13,9 @@ import { import fs from 'fs'; import moment from 'moment'; -import { imageConfig, locationConfig } from '../static/index'; +import { imageConfig, LatLngZoom, locationConfig } from '../static/index'; -import { FileSaveOptions, LatLngZoom } from '../types/types'; +import { FileSaveOptions } from '../types/fileOption'; import ipc from '../util/ipc'; diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index f985f0b6..74633eec 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -1,7 +1,7 @@ import { ipcRenderer } from 'electron'; import React, { Component, ReactNode } from 'react'; -import { ThemeProps } from '../types/types'; +import { ThemeProps } from '../types/componentStyle'; import MainWindow from './mainWindow/MainWindow'; import MissionWindow from './missionWindow/MissionWindow'; diff --git a/src/renderer/fixtures/updateMessagesFixtures.ts b/src/renderer/fixtures/updateMessagesFixtures.ts index def9cd22..68c0e859 100644 --- a/src/renderer/fixtures/updateMessagesFixtures.ts +++ b/src/renderer/fixtures/updateMessagesFixtures.ts @@ -1,4 +1,4 @@ -import { LogMessage } from '../../types/types'; +import { LogMessage } from '../../types/componentStyle'; import ipc from '../../util/ipc'; diff --git a/src/renderer/fixtures/updateVehiclesFixtures.ts b/src/renderer/fixtures/updateVehiclesFixtures.ts index c2db5045..a9d5c127 100644 --- a/src/renderer/fixtures/updateVehiclesFixtures.ts +++ b/src/renderer/fixtures/updateVehiclesFixtures.ts @@ -2,7 +2,7 @@ import Vehicle, { VehicleOptions } from '../../common/struct/Vehicle'; import { locationConfig, vehicleConfig } from '../../static/index'; -import { VehicleObject, VehicleStatus } from '../../types/types'; +import { VehicleObject, VehicleStatus } from '../../types/vehicle'; import ipc from '../../util/ipc'; diff --git a/src/renderer/mainWindow/MainWindow.tsx b/src/renderer/mainWindow/MainWindow.tsx index 602f373d..4d96f4d2 100644 --- a/src/renderer/mainWindow/MainWindow.tsx +++ b/src/renderer/mainWindow/MainWindow.tsx @@ -1,6 +1,6 @@ import React, { ReactNode } from 'react'; -import { ThemeProps } from '../../types/types'; +import { ThemeProps } from '../../types/componentStyle'; import LogContainer from './log/LogContainer'; import MapContainer from './map/MapContainer'; diff --git a/src/renderer/mainWindow/log/LogContainer.tsx b/src/renderer/mainWindow/log/LogContainer.tsx index dce63cf9..11ab6202 100644 --- a/src/renderer/mainWindow/log/LogContainer.tsx +++ b/src/renderer/mainWindow/log/LogContainer.tsx @@ -9,12 +9,7 @@ import { ListRowProps, } from 'react-virtualized'; -import { - isMessageType, - LogMessage, - MessageType, - ThemeProps, -} from '../../../types/types'; +import * as ComponentStyle from '../../../types/componentStyle'; import './log.css'; @@ -23,13 +18,13 @@ interface State { * The current filter being applied to messages. If the filter is not "", then only messages * of the same type as the filter will be shown. */ - filter: MessageType; + filter: ComponentStyle.MessageType; /** * All messages that have been logged. This includes message that are being hidden * if a filter is being applied. */ - messages: LogMessage[]; + messages: ComponentStyle.LogMessage[]; /** * All messages that are being shown. If there's no filter, then this is the same @@ -37,14 +32,14 @@ interface State { * message every time the component is re-rendered) for a space (a duplicate array * of messages). */ - filteredMessages: LogMessage[]; + filteredMessages: ComponentStyle.LogMessage[]; } /** * Container that displays messages regarding status, error, etc. */ -export default class LogContainer extends Component { - public constructor(props: ThemeProps) { +export default class LogContainer extends Component { + public constructor(props: ComponentStyle.ThemeProps) { super(props); this.state = { @@ -65,7 +60,7 @@ export default class LogContainer extends Component { } public componentDidMount(): void { - ipcRenderer.on('logMessages', (_: Event, ...messages: LogMessage[]): void => this.logMessages(...messages)); + ipcRenderer.on('logMessages', (_: Event, ...messages: ComponentStyle.LogMessage[]): void => this.logMessages(...messages)); } /** @@ -122,10 +117,10 @@ export default class LogContainer extends Component { const newFilter = event.currentTarget.value; // Ensures our new value has a type of MessageType. - if ((!newFilter && newFilter !== '') || !isMessageType(newFilter)) return; + if ((!newFilter && newFilter !== '') || !ComponentStyle.isMessageType(newFilter)) return; this.setState({ - filter: newFilter as MessageType, + filter: newFilter as ComponentStyle.MessageType, filteredMessages: newFilter === '' ? messages.slice(0) : messages.filter((message): boolean => message.type === newFilter), }); } @@ -133,13 +128,13 @@ export default class LogContainer extends Component { /** * Updates the messages in the log. Will update filtered messages accordingly. */ - private logMessages(...messages: LogMessage[]): void { + private logMessages(...messages: ComponentStyle.LogMessage[]): void { const { filteredMessages, messages: thisMessages, filter } = this.state; const currentMessages = thisMessages; const currentFilteredMessages = filteredMessages; messages.forEach((message): void => { - const msg: LogMessage = { + const msg: ComponentStyle.LogMessage = { type: message.type || '', message: message.message, time: message.time || moment(), diff --git a/src/renderer/mainWindow/map/MapContainer.tsx b/src/renderer/mainWindow/map/MapContainer.tsx index 7a949e53..74614aea 100644 --- a/src/renderer/mainWindow/map/MapContainer.tsx +++ b/src/renderer/mainWindow/map/MapContainer.tsx @@ -10,15 +10,12 @@ import React, { } from 'react'; import { Map, TileLayer, Viewport } from 'react-leaflet'; -import { locationConfig } from '../../../static/index'; - -import { - FileLoadOptions, - FileSaveOptions, - LatLngZoom, - ThemeProps, - VehicleObject, -} from '../../../types/types'; +import { LatLngZoom, locationConfig } from '../../../static/index'; + +import { ThemeProps } from '../../../types/componentStyle'; +import { FileLoadOptions, FileSaveOptions } from '../../../types/fileOption'; +import { VehicleObject } from '../../../types/vehicle'; + import { updateVehicles } from '../../../util/util'; import GeolocationControl from './control/GeolocationControl'; diff --git a/src/renderer/mainWindow/map/VehicleMarker.tsx b/src/renderer/mainWindow/map/VehicleMarker.tsx index 4caea019..90b7cda8 100644 --- a/src/renderer/mainWindow/map/VehicleMarker.tsx +++ b/src/renderer/mainWindow/map/VehicleMarker.tsx @@ -10,7 +10,8 @@ import { VehicleStatus, } from '../../../static/index'; -import { MessageType, VehicleObject } from '../../../types/types'; +import { MessageType } from '../../../types/componentStyle'; +import { VehicleObject } from '../../../types/vehicle'; const vehicleIcons = (imageConfig.markers as RecursiveImageSignature) .vehicles as RecursiveImageSignature; diff --git a/src/renderer/mainWindow/map/control/ThemeControl.tsx b/src/renderer/mainWindow/map/control/ThemeControl.tsx index dec9a36e..d7be1a9a 100644 --- a/src/renderer/mainWindow/map/control/ThemeControl.tsx +++ b/src/renderer/mainWindow/map/control/ThemeControl.tsx @@ -1,6 +1,6 @@ import React, { Component, ReactNode } from 'react'; -import { ThemeProps } from '../../../../types/types'; +import { ThemeProps } from '../../../../types/componentStyle'; import ipc from '../../../../util/ipc'; diff --git a/src/renderer/mainWindow/vehicle/VehicleContainer.tsx b/src/renderer/mainWindow/vehicle/VehicleContainer.tsx index cd6dcb99..d088f5ee 100644 --- a/src/renderer/mainWindow/vehicle/VehicleContainer.tsx +++ b/src/renderer/mainWindow/vehicle/VehicleContainer.tsx @@ -3,7 +3,9 @@ import React, { Component, ReactNode } from 'react'; import VehicleTable from './VehicleTable'; -import { ThemeProps, VehicleObject } from '../../../types/types'; +import { ThemeProps } from '../../../types/componentStyle'; +import { VehicleObject } from '../../../types/vehicle'; + import { updateVehicles } from '../../../util/util'; import './vehicle.css'; diff --git a/src/renderer/mainWindow/vehicle/VehicleTable.tsx b/src/renderer/mainWindow/vehicle/VehicleTable.tsx index aaaeb904..2342db32 100644 --- a/src/renderer/mainWindow/vehicle/VehicleTable.tsx +++ b/src/renderer/mainWindow/vehicle/VehicleTable.tsx @@ -14,7 +14,8 @@ import { VehicleStatus, } from '../../../static/index'; -import { ThemeProps, VehicleObject } from '../../../types/types'; +import { ThemeProps } from '../../../types/componentStyle'; +import { VehicleObject } from '../../../types/vehicle'; import ipc from '../../../util/ipc'; diff --git a/src/renderer/missionWindow/MissionWindow.tsx b/src/renderer/missionWindow/MissionWindow.tsx index 9ca3c997..6369a2f3 100644 --- a/src/renderer/missionWindow/MissionWindow.tsx +++ b/src/renderer/missionWindow/MissionWindow.tsx @@ -4,7 +4,7 @@ import { ISRSearchMissionParameters, PayloadDropMissionParameters, UGVRetreiveMissionParameters, -} from '../../types/messages'; +} from '../../types/message'; import { ThemeProps } from '../../types/types'; import ISRSearch from './parameters/ISRSearch'; diff --git a/src/static/index.ts b/src/static/index.ts index c52d2636..5e9ac484 100644 --- a/src/static/index.ts +++ b/src/static/index.ts @@ -1,7 +1,5 @@ /* eslint-disable @typescript-eslint/camelcase */ -import { LatLngZoom } from '../types/types'; - import ngcp_calpoly from './images/logo/ngcp_calpoly.png'; import ngcp_pomona from './images/logo/ngcp_pomona.png'; @@ -34,15 +32,6 @@ import { vehicleStatuses as vehicleStatusesObject, } from './vehicle.json'; -// Add signature to json objects to allow us to access it with TypeScript. -const locations: { - [name: string]: { lat: number; lng: number; zoom?: number } | undefined; -} = locationsObject; - -export const vehicleIds: { - [name: string]: number | undefined; -} = vehicleIdsObject; - /** * Object contained in vehicleInfos in vehicle.json. */ @@ -52,6 +41,24 @@ export interface VehicleInfo { 'type': string; } +/** + * Latitude, longitude and zoom. + */ +export interface LatLngZoom { + lat: number; + lng: number; + zoom?: number; +} + +// Add signature to json objects to allow us to access it with TypeScript. +const locations: { + [name: string]: { lat: number; lng: number; zoom?: number } | undefined; +} = locationsObject; + +export const vehicleIds: { + [name: string]: number | undefined; +} = vehicleIdsObject; + /** * All valid job types. This should always match up to the job types in vehicle.json. */ diff --git a/src/types/componentStyle.ts b/src/types/componentStyle.ts new file mode 100644 index 00000000..1144c19f --- /dev/null +++ b/src/types/componentStyle.ts @@ -0,0 +1,50 @@ +import { Moment } from 'moment'; + +/** + * Props with theme as its child. Feel free to extend this prop. + */ +export interface ThemeProps { + /** + * "Light" for light theme, "dark" for dark theme. + */ + theme: 'light' | 'dark'; +} + +/** + * Type guard for ThemeProps. + */ +export function isThemeProps(props: { theme: string }): boolean { + return props.theme === 'light' || props.theme === 'dark'; +} + +/** + * Types for all messages. Consists of "success", "failure", "progress", or "". + */ +export type MessageType = '' | 'success' | 'failure' | 'progress'; + +/** + * Type guard for MessageType. + */ +export function isMessageType(type: string): boolean { + return type === '' || type === 'success' || type === 'failure' || type === 'progress'; +} + +/** + * Structure for a message displayed in log container. + */ +export interface LogMessage { + /** + * The type of the message. Defines the color the message will be printed in. + */ + type?: MessageType; + + /** + * The content of the message. + */ + message: string; + + /** + * The time was received and logged. + */ + time?: Moment; +} diff --git a/src/types/fileOption.ts b/src/types/fileOption.ts new file mode 100644 index 00000000..d65d928d --- /dev/null +++ b/src/types/fileOption.ts @@ -0,0 +1,26 @@ +import { LatLngZoom } from '../static/index'; + +/** + * Data contents for information that is loaded from a configuration file. + */ +export interface FileLoadOptions { + /** + * Information related to the map. + */ + map: LatLngZoom; +} + +/** + * Object structure for information stored into a configuration file. + */ +export interface FileSaveOptions { + /** + * Filepath of the configuration file being saved. + */ + filePath: string; + + /** + * Data contents. Will be modified by classes through the "loadConfig" notification. + */ + data: FileLoadOptions; +} diff --git a/src/types/message.ts b/src/types/message.ts new file mode 100644 index 00000000..614a7855 --- /dev/null +++ b/src/types/message.ts @@ -0,0 +1,338 @@ +/* + * Definitions and typeguards for all messages that are sent between GCS and vehicles. + * https://ground-control-station.readthedocs.io/en/latest/communications/messages.html + * + * Includes definitions for tasks too, as those are part of a message. + * https://ground-control-station.readthedocs.io/en/latest/communications/jobs.html + */ + +import { JobType, vehicleConfig } from '../static/index'; + +import { isVehicleStatus, VehicleStatus } from './types'; + +import * as Task from './task'; + +interface MessageBase { + /** + * Type of message. + */ + type: string; +} + +// Definitions for all messages from GCS to vehicles. + +export interface StartMessage extends MessageBase { + type: 'start'; + + /** + * Name of job to perform. + */ + jobType: JobType; +} + +/** + * Type guard for Start Message. + */ +function isStartMessage(message: Message): boolean { + return message.type === 'start' + && vehicleConfig.isValidJobType(message.jobType); +} + +export interface AddMissionMessage extends MessageBase { + type: 'addMission'; + + /** + * Information related to accomplishing specific job. + */ + missionInfo: Task.Task; +} + +/** + * Type guard for AddMission Message. + */ +function isAddMissionMessage(message: Message): boolean { + return message.type === 'addMission' + && message.missionInfo && Task.TypeGuard.isTask(message.missionInfo); +} + +export interface PauseMessage extends MessageBase { + type: 'pause'; +} + +/** + * Type guard for Pause Message. + */ +function isPauseMessage(message: Message): boolean { + return message.type === 'pause'; +} + +export interface ResumeMessage extends MessageBase { + type: 'resume'; +} + +/** + * Type guard for Resume Message. + */ +function isResumeMessage(message: Message): boolean { + return message.type === 'resume'; +} + +export interface StopMessage extends MessageBase { + type: 'stop'; +} + +/** + * Type guard for Stop Message. + */ +function isStopMessage(message: Message): boolean { + return message.type === 'stop'; +} + +export interface ConnectionAckMessage extends MessageBase { + type: 'connectionAck'; +} + +/** + * Type guard for Connection Acknowledge Message. + */ +function isConnectionAcknowledgementMessage(message: Message): boolean { + return message.type === 'connectionAck'; +} + +// Definitions for all messages from vehicles to GCS. + +export interface UpdateMessage extends MessageBase { + type: 'update'; + + /** + * Latitude of the vehicle. + */ + lat: number; + + /** + * Longitude of the vehicle. + */ + lng: number; + + /** + * Altitude of the vehicle. + */ + alt?: number; + + /** + * Current vehicle heading. Value is in degrees. + */ + heading?: number; + + /** + * Current battery of the vehicle, expressed as a decimal. Will vary from 0 to 1. + */ + battery?: number; + + /** + * Status of the vehicle. + */ + status: VehicleStatus; + + /** + * Message generated if vehicle is in an error state. + * + * It is best practice to provide this value to be able to see why the the vehicle's + * status is error. + */ + errorMessage?: string; +} + +/** + * Type guard for Update Message. + */ +function isUpdateMessage(message: Message): boolean { + const mandatoryCheck = message.type === 'update' + && !Number.isNaN(message.lat) + && !Number.isNaN(message.lng) + && isVehicleStatus(message.status); + + if (!mandatoryCheck) return false; + + const updateMessage = message as UpdateMessage; + + if (updateMessage.alt && Number.isNaN(updateMessage.alt)) return false; + if (updateMessage.heading && Number.isNaN(updateMessage.heading)) return false; + if (updateMessage.battery && Number.isNaN(updateMessage.battery)) return false; + + return true; +} + +export interface POIMessage extends MessageBase { + type: 'poi'; + + /** + * Latitude of the point of interest. + */ + lat: number; + + /** + * Longitude of the point of interest. + */ + lng: number; +} + +/** + * Type guard for Point of Interest Message. + */ +function isPOIMessage(message: Message): boolean { + return message.type === 'poi' + && !Number.isNaN(message.lat) + && !Number.isNaN(message.lng); +} + +export interface CompleteMessage extends MessageBase { + type: 'complete'; +} + +/** + * Type guard for Complete Message. + */ +function isCompleteMessage(message: Message): boolean { + return message.type === 'complete'; +} + +export interface ConnectMessage extends MessageBase { + type: 'connect'; + + /** + * List of all the different jobs the vehicle is capable of doing. + */ + jobsAvailable: JobType[]; +} + + +/** + * Type guard for Connect Message. + */ +export function isConnectMessage(message: Message): boolean { + return message.type === 'connect' + + // Check if jobsAvailable is a string array of valid job types. + && message.jobsAvailable.every(vehicleConfig.isValidJobType); +} + +// Definitions for all other message types. + +export interface AcknowledgementMessage extends MessageBase { + type: 'ack'; + + /** + * ID of the message that is being acknowledged. + */ + ackid: number; +} + +/** + * Type guard for Acknowledgement Message. + */ +export function isAcknowledgementMessage(message: Message): boolean { + return message.type === 'ack' + && !Number.isNaN(message.ackid); +} + +export interface BadMessage extends MessageBase { + type: 'badMessage'; + + /** + * Description of why message was bad. + * + * It is best practice to provide this value to be able to see why the message received + * is bad. + */ + error?: string; +} + +/** + * Type guard for Bad Message. + */ +export function isBadMessage(message: Message): boolean { + return message.type === 'badMessage'; +} + +/** + * All types of messages sent to and from the GCS. + */ +export type Message = StartMessage | AddMissionMessage | PauseMessage | ResumeMessage | StopMessage +| ConnectionAckMessage | UpdateMessage | POIMessage | CompleteMessage | ConnectMessage +| AcknowledgementMessage | BadMessage; + +/** + * Simply checks if the message has a valid type field. This is different from the type + * guards from types/missionInformation and types/task. + * + * Use the more specific checks for each message to see which message it its. + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function isMessage(message: { [key: string]: any }): boolean { + return message.type && typeof message.type === 'string' && [ + 'start', + 'addMission', + 'pause', + 'resume', + 'stop', + 'connectionAck', + 'update', + 'poi', + 'complete', + 'connect', + 'ack', + 'badMessage', + ].includes(message.type.toLowerCase()); +} + +/** + * Same as a message, but has the required id, tid, sid, time fields. + */ +export type JSONMessage = Message & { + id: number; + tid: number; + sid: number; + time: number; +}; + +/** + * Type guard for a JSON Message. Check the type guard for a Message for information. + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function isJSONMessage(message: { [key: string]: any }): boolean { + if (!isMessage(message)) return false; + + // Check if id is a number. + const idCheck = message.id !== '' && message.id !== true && !Number.isNaN(message.id); + + // Check if tid is a number and is valid. + const tidCheck = !Number.isNaN(message.tid) + && vehicleConfig.vehicleInfos[message.tid] !== undefined; + + // Check if sid is a number and is valid. + const sidCheck = !Number.isNaN(message.sid) + && vehicleConfig.vehicleInfos[message.sid] !== undefined; + + return idCheck && tidCheck && sidCheck; +} + +/** + * Type guards for a message. + */ +export const TypeGuard = { + isStartMessage, + isAddMissionMessage, + isPauseMessage, + isResumeMessage, + isStopMessage, + isConnectionAcknowledgementMessage, + isUpdateMessage, + isPOIMessage, + isCompleteMessage, + isConnectMessage, + isAcknowledgementMessage, + isBadMessage, + isMessage, + isJSONMessage, +}; diff --git a/src/types/messages.ts b/src/types/messages.ts deleted file mode 100644 index 67bddc62..00000000 --- a/src/types/messages.ts +++ /dev/null @@ -1,754 +0,0 @@ -/* - * Definitions and typeguards for all messages that are sent between GCS and vehicles. - * https://ground-control-station.readthedocs.io/en/latest/communications/messages.html - * - * Includes definitions for tasks too, as those are part of a message. - * https://ground-control-station.readthedocs.io/en/latest/communications/jobs.html - */ - -import { JobType, vehicleConfig } from '../static/index'; - -import { isVehicleStatus, VehicleStatus } from './types'; - -/** - * A float represented as a hexadecimal string. - * This is used to reduce the size of messages being sent between vehicles. - * Uses IEEE 754 standard to convert float to hex (and vice versa). - */ -export type HexFloat = string; - -/** - * Type guard for a hex float. - */ -export function isHexFloat(hex: string): boolean { - const modifiedHex = hex.startsWith('0x') ? hex.slice(2) : hex; - return modifiedHex.match(/^[0-9a-fA-F]+$/) !== null; -} - -/** - * Converts a float number to a hex float. - */ -/* eslint-disable no-bitwise */ -export function toHexFloat(float: number): HexFloat { - const getHex = (index: number): string => `00${index.toString(16)}`.slice(-2); - - const view = new DataView(new ArrayBuffer(4)); - view.setFloat32(0, float); - return `0x${[0, 0, 0, 0].map((_, index): string => getHex(view.getUint8(index))).join('')}`; -} - -/** - * Converts a hex string to a float number. - */ -export function toFloat(hex: HexFloat): number { - const int = parseInt(hex, 16); - if (int === 0) return 0; - - const sign = (int >>> 31) ? -1 : 1; - let exp = ((int >>> 23) & 0xff) - 127; - const mantissa = ((int & 0x7fffff) + 0x800000).toString(2); - let float = 0; - - for (let i = 0; i < mantissa.length; i += 1, exp -= 1) { - if (parseInt(mantissa[i], 10)) { - float += 2 ** exp; - } - } - - return float * sign; -} -/* eslint-enable no-bitwise */ - -// Definitions for all tasks. - -interface TaskBase { - taskType: string; -} - -interface TakeoffTaskParameters { - lat: HexFloat; - lng: HexFloat; - alt: HexFloat; - loiter: { - lat: HexFloat; - lng: HexFloat; - alt: HexFloat; - radius: HexFloat; - direction: HexFloat; - }; -} - -export interface TakeoffTask extends TaskBase, TakeoffTaskParameters { - taskType: 'takeoff'; -} - -function isTakeoffTask(task: Task): boolean { - return task.taskType === 'takeoff' - && isHexFloat(task.lat) - && isHexFloat(task.lng) - && isHexFloat(task.alt) - - && task.loiter - && isHexFloat(task.loiter.lat) - && isHexFloat(task.loiter.lng) - && isHexFloat(task.loiter.alt) - && isHexFloat(task.loiter.radius) - && isHexFloat(task.loiter.direction); -} - -interface LoiterTaskParameters { - lat: HexFloat; - lng: HexFloat; - alt: HexFloat; - radius: HexFloat; - direction: HexFloat; -} - -export interface LoiterTask extends TaskBase, LoiterTaskParameters { - taskType: 'loiter'; -} - -function isLoiterTask(task: Task): boolean { - return task.taskType === 'loiter' - && isHexFloat(task.lat) - && isHexFloat(task.lng) - && isHexFloat(task.alt) - && isHexFloat(task.radius) - && isHexFloat(task.direction); -} - -// eslint-disable-next-line @typescript-eslint/interface-name-prefix -interface ISRSearchTaskParameters { - alt: HexFloat; - waypoints: [ - { - lat: HexFloat; - lng: HexFloat; - }, - { - lat: HexFloat; - lng: HexFloat; - }, - { - lat: HexFloat; - lng: HexFloat; - } - ]; -} - -// eslint-disable-next-line @typescript-eslint/interface-name-prefix -export interface ISRSearchTask extends TaskBase, ISRSearchTaskParameters { - taskType: 'isrSearch'; -} - -function isISRSearchTask(task: Task): boolean { - return task.taskType === 'isrSearch' - && isHexFloat(task.alt) - - && task.waypoints && task.waypoints.length === 3 - && task.waypoints.every( - (waypoint): boolean => isHexFloat(waypoint.lat) && isHexFloat(waypoint.lng), - ); -} - -interface PayloadDropTaskParameters { - waypoints: [ - { - lat: HexFloat; - lng: HexFloat; - alt: HexFloat; - }, - { - lat: HexFloat; - lng: HexFloat; - alt: HexFloat; - } - ]; -} - -export interface PayloadDropTask extends TaskBase, PayloadDropTaskParameters { - taskType: 'payloadDrop'; -} - -function isPayloadDropTask(task: Task): boolean { - return task.taskType === 'payloadDrop' - && task.waypoints && task.waypoints.length === 2 - && task.waypoints.every( - (waypoint): boolean => isHexFloat(waypoint.lat) - && isHexFloat(waypoint.lng) - && isHexFloat(waypoint.alt), - ); -} - -interface LandTaskParameters { - waypoints: [ - { - lat: HexFloat; - lng: HexFloat; - alt: HexFloat; - }, - { - lat: HexFloat; - lng: HexFloat; - alt: HexFloat; - } - ]; -} - -export interface LandTask extends TaskBase, LandTaskParameters { - taskType: 'land'; -} - -function isLandTask(task: Task): boolean { - return task.taskType === 'land' - && task.waypoints && task.waypoints.length === 2 - && task.waypoints.every( - (waypoint): boolean => isHexFloat(waypoint.lat) - && isHexFloat(waypoint.lng) - && isHexFloat(waypoint.alt), - ); -} - -interface UGVRetrieveTargetTaskParameters extends TaskBase { - lat: HexFloat; - lng: HexFloat; -} - -export interface UGVRetrieveTargetTask extends TaskBase, UGVRetrieveTargetTaskParameters { - taskType: 'retrieveTarget'; -} - -function isUGVRetrieveTargetTask(task: Task): boolean { - if (task.taskType !== 'retrieveTarget') return false; - - const retrieveTargetTask = task as UGVRetrieveTargetTask; - - return isHexFloat(retrieveTargetTask.lat) && isHexFloat(retrieveTargetTask.lng); -} - -interface DeliverTargetTaskParameters { - taskType: 'deliverTarget'; - - lat: HexFloat; - lng: HexFloat; -} - -export interface DeliverTargetTask extends TaskBase, DeliverTargetTaskParameters { - taskType: 'deliverTarget'; -} - -function isDeliverTargetTask(task: Task): boolean { - return task.taskType === 'deliverTarget' - && isHexFloat(task.lat) - && isHexFloat(task.lng); -} - -// UUVRetrieveTargetTask does not have any parameters. - -export interface UUVRetrieveTargetTask extends TaskBase { - taskType: 'retrieveTarget'; -} - -function isUUVRetrieveTargetTask(task: Task): boolean { - if (task.taskType !== 'retrieveTarget') return false; - - // Cast task to UGV retrieve to ensure that this task is not UGV task, and is UUV. - const retrieveTargetTask = task as UGVRetrieveTargetTask; - - // Check if values for lat and lng are undefined, since UUV version should not have these. - return !retrieveTargetTask.lat && !retrieveTargetTask.lng; -} - -export interface QuickScanTaskParameters { - waypoints: [ - { - lat: HexFloat; - lng: HexFloat; - }, - { - lat: HexFloat; - lng: HexFloat; - }, - { - lat: HexFloat; - lng: HexFloat; - }, - { - lat: HexFloat; - lng: HexFloat; - } - ]; -} - -export interface QuickScanTask extends TaskBase, QuickScanTaskParameters { - taskType: 'quickScan'; -} - -function isQuickScanTask(task: Task): boolean { - return task.taskType === 'quickScan' - && task.waypoints && task.waypoints.length === 4 - && task.waypoints.every( - (waypoint): boolean => isHexFloat(waypoint.lat) && isHexFloat(waypoint.lng), - ); -} - -/* - * DetailedSearchTask parameters are provided by QuickScan and are generated - * autonomously, no need for user interaction. - */ - -export interface DetailedSearchTask extends TaskBase { - taskType: 'detailedSearch'; - - lat: HexFloat; - lng: HexFloat; -} - -function isDetailedSearchTask(task: Task): boolean { - return task.taskType === 'detailedSearch' - && isHexFloat(task.lat) - && isHexFloat(task.lng); -} - -export type Task = TakeoffTask | LoiterTask | ISRSearchTask | PayloadDropTask | LandTask -| UGVRetrieveTargetTask | DeliverTargetTask | UUVRetrieveTargetTask | QuickScanTask -| DetailedSearchTask; - -/** - * Checks if an object is a task. To be a task, the object must match one of the tasks above. - */ -// eslint-disable-next-line @typescript-eslint/no-explicit-any -function isTask(task: { [key: string]: any }): boolean { - if (!task.taskType) return false; - - const specificTask = task as Task; - - return isTakeoffTask(specificTask) - || isLoiterTask(specificTask) - || isISRSearchTask(specificTask) - || isPayloadDropTask(specificTask) - || isLandTask(specificTask) - || isUGVRetrieveTargetTask(specificTask) - || isDeliverTargetTask(specificTask) - || isUUVRetrieveTargetTask(specificTask) - || isQuickScanTask(specificTask) - || isDetailedSearchTask(specificTask); -} - -export const taskTypeGuard = { - isTakeoffTask, - isLoiterTask, - isISRSearchTask, - isPayloadDropTask, - isLandTask, - isUGVRetrieveTargetTask, - isDeliverTargetTask, - isUUVRetrieveTargetTask, - isQuickScanTask, - isDetailedSearchTask, - isTask, -}; - -// eslint-disable-next-line @typescript-eslint/interface-name-prefix -export interface ISRSearchMissionParameters { - takeoff: TakeoffTaskParameters; - isrSearch: ISRSearchTaskParameters; - land: LandTaskParameters; -} - -export function isISRSearchMissionParameters(parameters: MissionParameters): boolean { - return 'takeoff' in parameters && 'isrSearch' in parameters && 'land' in parameters; -} - -export interface VTOLSearchMissionParameters { - quickScan: QuickScanTaskParameters; -} - -export function isVTOLSearchMissionParameters(parameters: MissionParameters): boolean { - return 'quickScan' in parameters; -} - -export interface PayloadDropMissionParameters { - takeoff: TakeoffTaskParameters; - payloadDrop: PayloadDropTaskParameters; - land: LandTaskParameters; -} - -export function isPayloadDropMissionParameters(parameters: MissionParameters): boolean { - return 'takeoff' in parameters && 'payloadDrop' in parameters && 'land' in parameters; -} - -export interface UGVRetreiveMissionParameters { - retrieveTarget: UGVRetreiveMissionParameters; - deliverTarget: DeliverTargetTaskParameters; -} - -export function isUGVRetreiveMissionParameters(parameters: MissionParameters): boolean { - return 'retrieveTarget' in parameters && 'deliverTarget' in parameters; -} - -// UUVRetrieveMission does not need parameters, as its one task does not need any. - -export function isUUVRetrieveMissionParameters(parameters: MissionParameters): boolean { - return Object.keys(parameters).length === 0; -} - -export type MissionParameters = ISRSearchMissionParameters | VTOLSearchMissionParameters -| PayloadDropTaskParameters | UGVRetreiveMissionParameters | {}; - -export const missionParametersTypeGuard = { - isISRSearchMissionParameters, - isVTOLSearchMissionParameters, - isPayloadDropMissionParameters, - isUGVRetreiveMissionParameters, - isUUVRetrieveMissionParameters, -}; - -/** - * Options for the mission. All variables are optional so the UI should - * be able to detect which of the variables are required or not. - */ -export interface MissionOptions { - isrSearch?: { - /** - * Will not require takeoff parameters if this is true. - */ - noTakeoff: boolean; - - /** - * Will not require land parameters if this is true. - */ - noLand: boolean; - }; - - payloadDrop?: { - /** - * Will not require takeoff parameters if this is true. - */ - noTakeoff: boolean; - - /** - * Will not require land parameters if this is true. - */ - noLand: boolean; - }; -} - -/** - * Type of information that should be passed from mission window to the Orchestrator. - */ -export interface MissionInformation { - /** - * Parameters for the mission. - */ - parameters?: MissionParameters; - - /** - * Options for the mission. - */ - options: MissionOptions; -} - -export type MissionName = 'isrSearch' | 'vtolSearch' | 'payloadDrop' | 'ugvRetrieve' | 'uuvRetrieve'; - -export interface MissionDescription { - missionName: MissionName; - activeVehicleMapping: { [vehicleId: number]: JobType }; - information: MissionInformation; -} - -interface MessageBase { - /** - * Type of message. - */ - type: string; -} - -/* - * Definitions for all messages from GCS to vehicles. No need for type guards, TypeScript - * will handle all of it as we write up the message. - * - * MessageHex types are the same as their normal Message class, however, their - */ - -export interface StartMessage extends MessageBase { - type: 'start'; - - /** - * Name of job to perform. - */ - jobType: JobType; - - /** - * Any other information the vehicle should have before performing the job. - */ - options?: any; // eslint-disable-line @typescript-eslint/no-explicit-any -} - -function isStartMessage(message: Message): boolean { - return message.type === 'start' - && vehicleConfig.isValidJobType(message.jobType); -} - -export interface AddMissionMessage extends MessageBase { - type: 'addMission'; - - /** - * Information related to accomplishing specific job. - */ - missionInfo: Task; -} - -function isAddMissionMessage(message: Message): boolean { - return message.type === 'addMission' - && message.missionInfo && isTask(message.missionInfo); -} - -export interface PauseMessage extends MessageBase { - type: 'pause'; -} - -function isPauseMessage(message: Message): boolean { - return message.type === 'pause'; -} - -export interface ResumeMessage extends MessageBase { - type: 'resume'; -} - -function isResumeMessage(message: Message): boolean { - return message.type === 'resume'; -} - -export interface StopMessage extends MessageBase { - type: 'stop'; -} - -function isStopMessage(message: Message): boolean { - return message.type === 'stop'; -} - -export interface ConnectionAckMessage extends MessageBase { - type: 'connectionAck'; -} - -function isConnectionAckMessage(message: Message): boolean { - return message.type === 'connectionAck'; -} - -// Definitions for all messages from vehicles to GCS. - -export interface UpdateMessage extends MessageBase { - type: 'update'; - - /** - * Latitude of the vehicle. - */ - lat: HexFloat; - - /** - * Longitude of the vehicle. - */ - lng: HexFloat; - - /** - * Altitude of the vehicle. - */ - alt?: HexFloat; - - /** - * Current vehicle heading. Value is in degrees. - */ - heading?: HexFloat; - - /** - * Current battery of the vehicle, expressed as a decimal. Will vary from 0 to 1. - */ - battery?: HexFloat; - - /** - * Status of the vehicle. - */ - status: VehicleStatus; - - /** - * Message generated if vehicle is in an error state. - * - * It is best practice to provide this value to be able to see why the the vehicle's - * status is error. - */ - errorMessage?: string; -} - -function isUpdateMessage(message: Message): boolean { - const mandatoryCheck = message.type === 'update' - && isHexFloat(message.lat) - && isHexFloat(message.lng) - && isVehicleStatus(message.status); - - if (!mandatoryCheck) return false; - - const updateMessage = message as UpdateMessage; - - if (updateMessage.alt && !isHexFloat(updateMessage.alt)) return false; - if (updateMessage.heading && !isHexFloat(updateMessage.heading)) return false; - if (updateMessage.battery && !isHexFloat(updateMessage.battery)) return false; - - return true; -} - -export interface POIMessage extends MessageBase { - type: 'poi'; - - /** - * Latitude of the point of interest. - */ - lat: HexFloat; - - /** - * Longitude of the point of interest. - */ - lng: HexFloat; -} - -function isPOIMessage(message: Message): boolean { - return message.type === 'poi' - && isHexFloat(message.lat) - && isHexFloat(message.lng); -} - -export interface CompleteMessage extends MessageBase { - type: 'complete'; -} - -function isCompleteMessage(message: Message): boolean { - return message.type === 'complete'; -} - -export interface ConnectMessage extends MessageBase { - type: 'connect'; - - /** - * List of all the different jobs the vehicle is capable of doing. - */ - jobsAvailable: JobType[]; -} - -export function isConnectMessage(message: Message): boolean { - return message.type === 'connect' - - // Check if jobsAvailable is a string array of valid job types. - && message.jobsAvailable.every(vehicleConfig.isValidJobType); -} - -// Definitions for all other message types. - -export interface AcknowledgementMessage extends MessageBase { - type: 'ack'; - - /** - * ID of the message that is being acknowledged. - */ - ackid: number; -} - -export function isAcknowledgementMessage(message: Message): boolean { - return message.type === 'ack' - && !Number.isNaN(message.ackid); -} - -export interface BadMessage extends MessageBase { - type: 'badMessage'; - - /** - * Description of why message was bad. - * - * It is best practice to provide this value to be able to see why the message received - * is bad. - */ - error?: string; -} - -export function isBadMessage(message: Message): boolean { - return message.type === 'badMessage'; -} - -export type Message = StartMessage | AddMissionMessage | PauseMessage | ResumeMessage | StopMessage -| ConnectionAckMessage | UpdateMessage | POIMessage | CompleteMessage | ConnectMessage -| AcknowledgementMessage | BadMessage; - -/** - * Logic for this function is slightly different from isTask since the GCS does not need to know - * which task it is sending to the vehicle. It just needs to know that the task is a valid task, - * and contains a valid taskType and task information for a task. - * - * For this function, GCS does need to know what messages it receives. GCS needs to check if - * an object is a message (if the object has a type field then it is a message). If not, GCS - * can alert the sender that the object sent is not a message. - * - * After this function, the GCS will check which message the sender has sent with the functions - * above, and if the functions fail (which means that the fields that have been included with - * the message are wrong), then the GCS will tell the sender that the format for the message - * is wrong. - */ -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export function isMessage(message: { [key: string]: any }): boolean { - return message.type && typeof message.type === 'string' && [ - 'start', - 'addMission', - 'pause', - 'resume', - 'stop', - 'connectionAck', - 'update', - 'poi', - 'complete', - 'connect', - 'ack', - 'badMessage', - ].includes(message.type.toLowerCase()); -} - -export type JSONMessage = Message & { - id: number; - tid: number; - sid: number; - time: number; -}; - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export function isJSONMessage(message: { [key: string]: any }): boolean { - if (!isMessage(message)) return false; - - // Check if id is a number. - const idCheck = message.id !== '' && message.id !== true && !Number.isNaN(message.id); - - // Check if tid is a number and is valid. - const tidCheck = !Number.isNaN(message.tid) - && vehicleConfig.vehicleInfos[message.tid] !== undefined; - - // Check if sid is a number and is valid. - const sidCheck = !Number.isNaN(message.sid) - && vehicleConfig.vehicleInfos[message.sid] !== undefined; - - return idCheck && tidCheck && sidCheck; -} - -export const messageTypeGuard = { - isStartMessage, - isAddMissionMessage, - isPauseMessage, - isResumeMessage, - isStopMessage, - isConnectionAckMessage, - isUpdateMessage, - isPOIMessage, - isCompleteMessage, - isConnectMessage, - isAcknowledgementMessage, - isBadMessage, - isMessage, - isJSONMessage, -}; - -export default { taskTypeGuard, messageTypeGuard }; diff --git a/src/types/missionInformation.ts b/src/types/missionInformation.ts new file mode 100644 index 00000000..e8b926ba --- /dev/null +++ b/src/types/missionInformation.ts @@ -0,0 +1,152 @@ +import { JobType } from '../static/index'; + +import * as Task from './task'; + +export type MissionName = 'isrSearch' | 'vtolSearch' | 'payloadDrop' | 'ugvRescue' | 'uuvRescue'; + +interface InformationBase { + /** + * Name of mission. + */ + missionName: MissionName; + + /** + * Parameters for the mission. Both user and mission generated. + */ + parameters: {}; + + /** + * Options for the mission. User generated. + */ + options: {}; + + /** + * Vehicle mapping for specific mission. User generated. + */ + activeVehicleMapping: { [vehicleId: number]: JobType }; +} + +// eslint-disable-next-line @typescript-eslint/interface-name-prefix +export interface ISRSearchInformation extends InformationBase { + missionName: 'isrSearch'; + parameters: { + takeoff: Task.TakeoffTaskParameters; + isrSearch: Task.ISRSearchTaskParameters; + land: Task.LandTaskParameters; + }; + options: { + /** + * Will not require takeoff task if true. + */ + noTakeoff: boolean; + + /** + * Will not require land task if true. + */ + noLand: boolean; + }; +} + +/** + * Type guard for ISR Search mission information. + */ +function isISRSearchInformation(information: Information): boolean { + return information.missionName === 'isrSearch'; +} + +export interface VTOLSearchInformation extends InformationBase { + missionName: 'vtolSearch'; + parameters: { + quickScan: Task.QuickScanTaskParameters; + }; +} + +/** + * Type guard for VTOL Search mission information. + */ +function isVTOLSearchInformation(information: Information): boolean { + return information.missionName === 'vtolSearch'; +} + +/** + * Type guard for Payload Drop mission information. + */ +export interface PayloadDropInformation extends InformationBase { + missionName: 'payloadDrop'; + parameters: { + takeoff: Task.TakeoffTaskParameters; + payloadDrop: Task.PayloadDropTaskParameters; + land: Task.LandTaskParameters; + }; + options: { + /** + * Will not require takeoff task if true. + */ + noTakeoff: boolean; + + /** + * Will not require land task if true. + */ + noLand: boolean; + }; +} + +function isPayloadDropInformation(information: Information): boolean { + return information.missionName === 'payloadDrop'; +} + +export interface UGVRescueInformation extends InformationBase { + missionName: 'ugvRescue'; + parameters: { + retrieveTarget: Task.UGVRetrieveTargetTask; + deliverTarget: Task.DeliverTargetTaskParameters; + }; +} + +function isUGVRetreiveInformation(information: Information): boolean { + return information.missionName === 'ugvRescue'; +} + +export interface UUVRescueInformation extends InformationBase { + missionName: 'uuvRescue'; +} + +function isUUVRetrieveInformation(information: Information): boolean { + return information.missionName === 'uuvRescue'; +} + +/** + * All types of information that can be provided to a mission. + */ +export type Information = ISRSearchInformation | VTOLSearchInformation +| PayloadDropInformation | UGVRescueInformation | UUVRescueInformation; + + +/** + * Checks if an object is a mission information. + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function isMissionInformation(object: { [key: string]: any }): boolean { + if (!object.missionName) return false; + + const information = object as Information; + + return isISRSearchInformation(information) + || isVTOLSearchInformation(information) + || isPayloadDropInformation(information) + || isUGVRetreiveInformation(information) + || isUUVRetrieveInformation(information) + || isMissionInformation(information); +} + +/** + * Type guards for a mission information. + */ +export const TypeGuard = { + isISRSearchInformation, + isVTOLSearchInformation, + isPayloadDropInformation, + isUGVRetreiveInformation, + isUUVRetrieveInformation, + isMissionInformation, +}; diff --git a/src/types/task.ts b/src/types/task.ts new file mode 100644 index 00000000..63779259 --- /dev/null +++ b/src/types/task.ts @@ -0,0 +1,311 @@ +interface TaskBase { + /** + * Type of task. + */ + taskType: string; +} + +/** + * Do not import this outside of types/missionInformation. + */ +export interface TakeoffTaskParameters { + lat: number; + lng: number; + alt: number; + loiter: { + lat: number; + lng: number; + alt: number; + radius: number; + direction: number; + }; +} + +export interface TakeoffTask extends TaskBase, TakeoffTaskParameters { + taskType: 'takeoff'; +} + +/** + * Type guard for Takeoff task. + */ +function isTakeoffTask(task: Task): boolean { + return task.taskType === 'takeoff'; +} + +/** + * Do not import this outside of types/missionInformation. + */ +export interface LoiterTaskParameters { + lat: number; + lng: number; + alt: number; + radius: number; + direction: number; +} + +export interface LoiterTask extends TaskBase, LoiterTaskParameters { + taskType: 'loiter'; +} + +/** + * Type guard for Loiter task. + */ +export function isLoiterTask(task: Task): boolean { + return task.taskType === 'loiter'; +} + +/** + * Do not import this outside of types/missionInformation. + */ +// eslint-disable-next-line @typescript-eslint/interface-name-prefix +export interface ISRSearchTaskParameters { + alt: number; + waypoints: [ + { + lat: number; + lng: number; + }, + { + lat: number; + lng: number; + }, + { + lat: number; + lng: number; + } + ]; +} + +// eslint-disable-next-line @typescript-eslint/interface-name-prefix +export interface ISRSearchTask extends TaskBase, ISRSearchTaskParameters { + taskType: 'isrSearch'; +} + +/** + * Type guard for ISR Search task. + */ +function isISRSearchTask(task: Task): boolean { + return task.taskType === 'isrSearch' + && task.waypoints && task.waypoints.length === 3; +} + +/** + * Do not import this outside of types/missionInformation. + */ +export interface PayloadDropTaskParameters { + waypoints: [ + { + lat: number; + lng: number; + alt: number; + }, + { + lat: number; + lng: number; + alt: number; + } + ]; +} + +export interface PayloadDropTask extends TaskBase, PayloadDropTaskParameters { + taskType: 'payloadDrop'; +} + +/** + * Type guard for PayloadDrop task. + */ +function isPayloadDropTask(task: Task): boolean { + return task.taskType === 'payloadDrop'; +} + +/** + * Do not import this outside of types/missionInformation. + */ +export interface LandTaskParameters { + waypoints: [ + { + lat: number; + lng: number; + alt: number; + }, + { + lat: number; + lng: number; + alt: number; + } + ]; +} + +export interface LandTask extends TaskBase, LandTaskParameters { + taskType: 'land'; +} + +/** + * Type guard for Land task. + */ +function isLandTask(task: Task): boolean { + return task.taskType === 'land'; +} + +/** + * Do not import this outside of types/missionInformation. + */ +export interface UGVRetrieveTargetTaskParameters extends TaskBase { + lat: number; + lng: number; +} + +export interface UGVRetrieveTargetTask extends TaskBase, UGVRetrieveTargetTaskParameters { + taskType: 'retrieveTarget'; +} + +/** + * Type guard for UGV's RetriveTarget task. + */ +function isUGVRetrieveTargetTask(task: Task): boolean { + if (task.taskType !== 'retrieveTarget') return false; + return Object.keys(task).length === 3; // Keys are taskType, lat, lng. +} + +/** + * Do not import this outside of types/missionInformation. + */ +export interface DeliverTargetTaskParameters { + taskType: 'deliverTarget'; + + lat: number; + lng: number; +} + +export interface DeliverTargetTask extends TaskBase, DeliverTargetTaskParameters { + taskType: 'deliverTarget'; +} + +/** + * Type guard for DeliverTarget task. + */ +function isDeliverTargetTask(task: Task): boolean { + return task.taskType === 'deliverTarget'; +} + +export interface UUVRetrieveTargetTask extends TaskBase { + taskType: 'retrieveTarget'; +} + +/** + * Type guard for UUV's RetrieveTarget task. + */ +function isUUVRetrieveTargetTask(task: Task): boolean { + if (task.taskType !== 'retrieveTarget') return false; + return Object.keys(task).length === 1; // Only key is taskType. +} + +/** + * Do not import this outside of types/missionInformation. + */ +export interface QuickScanTaskParameters { + waypoints: [ + { + lat: number; + lng: number; + }, + { + lat: number; + lng: number; + }, + { + lat: number; + lng: number; + }, + { + lat: number; + lng: number; + } + ]; +} + +export interface QuickScanTask extends TaskBase, QuickScanTaskParameters { + taskType: 'quickScan'; +} + +/** + * Type guard for QuickScan task. + */ +function isQuickScanTask(task: Task): boolean { + return task.taskType === 'quickScan'; +} + +/** + * Do not import this outside of types/missionInformation. + */ +export interface DetailedSearchParameters { + lat: number; + lng: number; +} + + +export interface DetailedSearchTask extends TaskBase, DetailedSearchParameters { + taskType: 'detailedSearch'; +} + +/** + * Type guard for quickScan task. + */ +function isDetailedSearchTask(task: Task): boolean { + return task.taskType === 'detailedSearch'; +} + +/** + * A task for a vehicle to perform. Check which specific task it is by + * checking through the type guards. + */ +export type Task = TakeoffTask | LoiterTask | ISRSearchTask | PayloadDropTask | LandTask +| UGVRetrieveTargetTask | DeliverTargetTask | UUVRetrieveTargetTask | QuickScanTask +| DetailedSearchTask; + +/** + * Special type for missions only to use. This is to ensure that missions can properly + * pass in tasks. + */ +export type TaskParameters = TakeoffTaskParameters | LoiterTaskParameters | ISRSearchTaskParameters +| PayloadDropTaskParameters | LandTaskParameters | UGVRetrieveTargetTaskParameters +| DeliverTargetTaskParameters | UUVRetrieveTargetTask | QuickScanTaskParameters +| DetailedSearchParameters; + +/** + * Checks if an object is a task. + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function isTask(object: { [key: string]: any }): boolean { + if (!object.taskType) return false; + + const task = object as Task; + + return isTakeoffTask(task) + || isLoiterTask(task) + || isISRSearchTask(task) + || isPayloadDropTask(task) + || isLandTask(task) + || isUGVRetrieveTargetTask(task) + || isDeliverTargetTask(task) + || isUUVRetrieveTargetTask(task) + || isQuickScanTask(task) + || isDetailedSearchTask(task); +} + +/** + * Type guards for a task. + */ +export const TypeGuard = { + isTakeoffTask, + isLoiterTask, + isISRSearchTask, + isPayloadDropTask, + isLandTask, + isUGVRetrieveTargetTask, + isDeliverTargetTask, + isUUVRetrieveTargetTask, + isQuickScanTask, + isDetailedSearchTask, + isTask, +}; diff --git a/src/types/tasks.ts b/src/types/tasks.ts deleted file mode 100644 index e69de29b..00000000 diff --git a/src/types/types.ts b/src/types/types.ts deleted file mode 100644 index 9e00b2d9..00000000 --- a/src/types/types.ts +++ /dev/null @@ -1,144 +0,0 @@ -import { Moment } from 'moment'; - -/** - * Status of the vehicle, lets us know of where the vehicle is in its mission. - */ -export type VehicleStatus = 'ready' | 'error' | 'disconnected' | 'waiting' | 'running' | 'paused'; - -export function isVehicleStatus(status: string): boolean { - return status === 'ready' || status === 'error' || status === 'disconnected' || status === 'waiting' || status === 'running' || status === 'paused'; -} - -/** - * Props with theme as its child. Feel free to extend this prop. - */ -export interface ThemeProps { - /** - * "Light" for light theme, "dark" for dark theme. - */ - theme: 'light' | 'dark'; -} - -export function isThemeProps(props: { theme: string }): boolean { - return props.theme === 'light' || props.theme === 'dark'; -} - -/** - * Types for all messages. Consists of "success", "failure", "progress", or "". - */ -export type MessageType = '' | 'success' | 'failure' | 'progress'; - -export function isMessageType(type: string): boolean { - return type === '' || type === 'success' || type === 'failure' || type === 'progress'; -} - -/** - * Latitude, longitude and zoom. - */ -export interface LatLngZoom { - /** - * Latitude of center of map. - */ - lat: number; - - /** - * Longitude of center of map. - */ - lng: number; - - /** - * Zoom level of map. - */ - zoom?: number; -} - -/** - * Vehicle object for all classes to use. This is necessary because Vehicle class will - * be uncasted when being sent through ipcRenderer. - */ -export interface VehicleObject { - /** - * ID of the vehicle. - */ - vehicleId: number; - - /** - * Current status of the vehicle. - */ - status: VehicleStatus; - - /** - * Jobs of the vehicle. - */ - jobs: string[]; - - /** - * Current latitude of the vehicle. Starts at 0. - */ - lat: number; - - /** - * Current longitude of the vehicle. Starts at 0. - */ - lng: number; - - /** - * Current altitude of the vehicle. - */ - alt?: number; - /** - * - * Current battery of the vehicle, expressed as a decimal. Will vary from 0 to 1. - */ - battery?: number; - - /** - * Current vehicle heading. Value is in degrees. - */ - heading?: number; -} - -/** - * Data contents for information that is loaded from a configuration file. - */ -export interface FileLoadOptions { - /** - * Information related to the map. - */ - map: LatLngZoom; -} - -/** - * Object structure for information stored into a configuration file. - */ -export interface FileSaveOptions { - /** - * Filepath of the configuration file being saved. - */ - filePath: string; - - /** - * Data contents. Will be modified by classes through the "loadConfig" notification. - */ - data: FileLoadOptions; -} - -/** - * Structure for a message displayed in log container. - */ -export interface LogMessage { - /** - * The type of the message. Defines the color the message will be printed in. - */ - type?: MessageType; - - /** - * The content of the message. - */ - message: string; - - /** - * The time was received and logged. - */ - time?: Moment; -} diff --git a/src/types/vehicle.ts b/src/types/vehicle.ts new file mode 100644 index 00000000..b7e4e9dc --- /dev/null +++ b/src/types/vehicle.ts @@ -0,0 +1,54 @@ +/** + * Status of the vehicle, lets us know of where the vehicle is in its mission. + */ +export type VehicleStatus = 'ready' | 'error' | 'disconnected' | 'waiting' | 'running' | 'paused'; + +export function isVehicleStatus(status: string): boolean { + return status === 'ready' || status === 'error' || status === 'disconnected' || status === 'waiting' || status === 'running' || status === 'paused'; +} + +/** + * Vehicle object for all classes to use. This is necessary because Vehicle class will + * be uncasted when being sent through ipcRenderer. + */ +export interface VehicleObject { + /** + * ID of the vehicle. + */ + vehicleId: number; + + /** + * Current status of the vehicle. + */ + status: VehicleStatus; + + /** + * Jobs of the vehicle. + */ + jobs: string[]; + + /** + * Current latitude of the vehicle. Starts at 0. + */ + lat: number; + + /** + * Current longitude of the vehicle. Starts at 0. + */ + lng: number; + + /** + * Current altitude of the vehicle. + */ + alt?: number; + /** + * + * Current battery of the vehicle, expressed as a decimal. Will vary from 0 to 1. + */ + battery?: number; + + /** + * Current vehicle heading. Value is in degrees. + */ + heading?: number; +} diff --git a/src/util/ipc.ts b/src/util/ipc.ts index 9cf0c250..0950e66d 100644 --- a/src/util/ipc.ts +++ b/src/util/ipc.ts @@ -16,19 +16,14 @@ import { BrowserWindow, ipcRenderer } from 'electron'; -import { - JSONMessage, - Message, - MissionDescription, - MissionParameters, -} from '../types/messages'; -import { - FileLoadOptions, - FileSaveOptions, - LatLngZoom, - LogMessage, - VehicleObject, -} from '../types/types'; +import { LatLngZoom } from '../static/index'; + +import { LogMessage } from '../types/componentStyle'; +import * as FileOptions from '../types/fileOption'; +import * as Message from '../types/message'; +import { Information } from '../types/missionInformation'; +import * as Task from '../types/task'; +import { VehicleObject } from '../types/vehicle'; /** * Post "acknowledgeMessage" notification. @@ -36,7 +31,7 @@ import { * Files that take this notification: * - common/Orchestrator */ -function postAcknowledgeMessage(jsonMessage: JSONMessage): void { +function postAcknowledgeMessage(jsonMessage: Message.JSONMessage): void { ipcRenderer.send('post', 'acknowledgeMessage', jsonMessage); } @@ -56,7 +51,10 @@ function postCenterMapToVehicle(vehicle: VehicleObject): void { * Files that take this notification: * - common/Orchestrator */ -function postCompleteMission(missionName: string, completionParameters: MissionParameters): void { +function postCompleteMission( + missionName: string, + completionParameters: { [key: string]: Task.TaskParameters }, +): void { ipcRenderer.send('post', 'completeMission', missionName, completionParameters); } @@ -75,7 +73,7 @@ function postConfirmCompleteMission(): void { * Files that take this notification: * - common/Orchestrator */ -function postConnectToVehicle(jsonMessage: JSONMessage): void { +function postConnectToVehicle(jsonMessage: Message.JSONMessage): void { ipcRenderer.send('post', 'connectToVehicle', jsonMessage); } @@ -95,7 +93,7 @@ function postDisconnectFromVehicle(vehicleId: number): void { * * Files that take this notification: */ -function postFinishMissions(completionParameters: MissionParameters): void { +function postFinishMissions(completionParameters: Task.TaskParameters[]): void { ipcRenderer.send('post', 'finishMissions', completionParameters); } @@ -105,7 +103,7 @@ function postFinishMissions(completionParameters: MissionParameters): void { * Files that take this notification: * - common/Orchestrator */ -function postHandleAcknowledgementMessage(jsonMessage: JSONMessage): void { +function postHandleAcknowledgementMessage(jsonMessage: Message.JSONMessage): void { ipcRenderer.send('post', 'handleAcknowledgementMessage', jsonMessage); } @@ -115,7 +113,7 @@ function postHandleAcknowledgementMessage(jsonMessage: JSONMessage): void { * Files that take this notification: * - common/Orchestrator */ -function postHandleBadMessage(jsonMessage: JSONMessage): void { +function postHandleBadMessage(jsonMessage: Message.JSONMessage): void { ipcRenderer.send('post', 'handleBadMessage', jsonMessage); } @@ -125,7 +123,7 @@ function postHandleBadMessage(jsonMessage: JSONMessage): void { * Files that take this notification: * - common/Orchestrator */ -function postHandleCompleteMessage(jsonMessage: JSONMessage): void { +function postHandleCompleteMessage(jsonMessage: Message.JSONMessage): void { ipcRenderer.send('post', 'handleCompleteMessage', jsonMessage); } @@ -135,7 +133,7 @@ function postHandleCompleteMessage(jsonMessage: JSONMessage): void { * Files that take this notification: * - common/Orchestrator */ -function postHandlePOIMessage(jsonMessage: JSONMessage): void { +function postHandlePOIMessage(jsonMessage: Message.JSONMessage): void { ipcRenderer.send('post', 'handlePOIMessage', jsonMessage); } @@ -145,7 +143,7 @@ function postHandlePOIMessage(jsonMessage: JSONMessage): void { * Files that take this notification: * - common/Orchestrator */ -function postHandleUpdateMessage(jsonMessage: JSONMessage): void { +function postHandleUpdateMessage(jsonMessage: Message.JSONMessage): void { ipcRenderer.send('post', 'handleUpdateMessage', jsonMessage); } @@ -166,7 +164,7 @@ function postHideMissionWindow(): void { * - renderer/mainWindow/map/MapContainer */ function postLoadConfig( - loadOptions: FileLoadOptions, + loadOptions: FileOptions.FileLoadOptions, mainWindow?: BrowserWindow | null, missionWindow?: BrowserWindow | null, ): void { @@ -205,7 +203,7 @@ function postReceiveMessage(text: string): void { * - renderer/mainWindow/map/MapContainer */ function postSaveConfig( - saveOptions: FileSaveOptions, + saveOptions: FileOptions.FileSaveOptions, mainWindow?: BrowserWindow | null, missionWindow?: BrowserWindow | null, ): void { @@ -223,7 +221,7 @@ function postSaveConfig( * Files that take this notification: * - common/MessageHandler */ -function postSendMessage(vehicleId: number, message: Message): void { +function postSendMessage(vehicleId: number, message: Message.Message): void { ipcRenderer.send('post', 'sendMessage', vehicleId, message); } @@ -262,7 +260,7 @@ function postShowMissionWindow(): void { * - common/Orchestrator */ function postStartMissions( - missions: MissionDescription[], + missions: Information[], requireConfirmation: boolean, ): void { ipcRenderer.send('post', 'startMissions', missions, requireConfirmation); diff --git a/src/util/util.ts b/src/util/util.ts index 3ade0bbe..e0b545c0 100644 --- a/src/util/util.ts +++ b/src/util/util.ts @@ -1,6 +1,6 @@ import { Component } from 'react'; -import { VehicleObject } from '../types/types'; +import { VehicleObject } from '../types/vehicle'; /** * Updates vehicles being shown. This is run on map and vehicle containers. From a0911650864dc7984e11483a778f55b41db3430f Mon Sep 17 00:00:00 2001 From: Paul Matthew Barrameda Date: Wed, 17 Apr 2019 02:09:21 -0700 Subject: [PATCH 092/152] MessageHandler was not included in last commit Not sure why --- src/common/MessageHandler.ts | 46 ++++++++++++++++-------------------- 1 file changed, 21 insertions(+), 25 deletions(-) diff --git a/src/common/MessageHandler.ts b/src/common/MessageHandler.ts index 84f44c59..98477508 100644 --- a/src/common/MessageHandler.ts +++ b/src/common/MessageHandler.ts @@ -2,12 +2,7 @@ import { Event, ipcRenderer } from 'electron'; import { config, vehicleConfig } from '../static/index'; -import { - AcknowledgementMessage, - messageTypeGuard, - JSONMessage, - Message, -} from '../types/messages'; +import * as Message from '../types/message'; import ipc from '../util/ipc'; import { isJSON } from '../util/util'; @@ -27,7 +22,7 @@ class MessageHandler { * "sent": will have a list of messages that have been sent. * "received": will have a list of messages that have been received. */ - private messageDictionary = new DictionaryList(); + private messageDictionary = new DictionaryList(); /** * Handler that listens for different update events. @@ -45,7 +40,7 @@ class MessageHandler { private receivedMessageId: { [key: string]: number | undefined } = {}; public constructor() { - ipcRenderer.on('sendMessage', (_: Event, vehicleId: number, message: Message): void => this.sendMessage(vehicleId, message)); + ipcRenderer.on('sendMessage', (_: Event, vehicleId: number, message: Message.Message): void => this.sendMessage(vehicleId, message)); ipcRenderer.on('receiveMessage', (_: Event, text: string): void => this.receiveMessage(text)); ipcRenderer.on('stopSendingMessages', this.stopSendingMessages); } @@ -55,8 +50,9 @@ class MessageHandler { * * @param id Message id */ - private removeMessage(key: 'outbox' | 'sent' | 'received', id: number): JSONMessage | undefined { - return this.messageDictionary.remove(key, (message: JSONMessage): boolean => message.id === id); + private removeMessage(key: 'outbox' | 'sent' | 'received', id: number): Message.JSONMessage | undefined { + return this.messageDictionary.remove(key, + (message: Message.JSONMessage): boolean => message.id === id); } /** @@ -65,8 +61,8 @@ class MessageHandler { * @param vehicleId Vehicle id to send the message to. * @param message Message format (note this does not have sid, tid, id, and time). */ - private sendMessage(vehicleId: number, message: Message): void { - const jsonMessage: JSONMessage = { + private sendMessage(vehicleId: number, message: Message.Message): void { + const jsonMessage: Message.JSONMessage = { id: this.id, sid: 0, tid: vehicleId, @@ -78,8 +74,8 @@ class MessageHandler { this.messageDictionary.push('sent', jsonMessage); - if (messageTypeGuard.isAcknowledgementMessage(message) - || messageTypeGuard.isBadMessage(message)) { + if (Message.TypeGuard.isAcknowledgementMessage(message) + || Message.TypeGuard.isBadMessage(message)) { xbee.sendMessage(jsonMessage); return; } @@ -113,7 +109,7 @@ class MessageHandler { * @param jsonMessage The bad message. * @param error Error message. */ - private sendBadMessage(jsonMessage: JSONMessage, error?: string): void { + private sendBadMessage(jsonMessage: Message.JSONMessage, error?: string): void { this.sendMessage(jsonMessage.sid, { type: 'badMessage', error, @@ -135,9 +131,9 @@ class MessageHandler { } const json = JSON.parse(text); - if (!messageTypeGuard.isJSONMessage(json)) { + if (!Message.TypeGuard.isMessage(json)) { if (!Number.isNaN(json.sid) && vehicleConfig.isValidVehicleId(json.sid as number)) { - this.sendBadMessage(json as JSONMessage, 'Invalid message, does not meet requirements for a message'); + this.sendBadMessage(json as Message.JSONMessage, 'Invalid message, does not meet requirements for a message'); } else { ipc.postLogMessages({ message: `Received JSON from Xbee that is not a valid message, could not send bad message to sender: ${text}`, @@ -147,7 +143,7 @@ class MessageHandler { } json.type = (json.type as string).toLowerCase(); - const jsonMessage = json as JSONMessage; + const jsonMessage = json as Message.JSONMessage; // Ignore messages from unrecognized vehicles. if (!vehicleConfig.isValidVehicleId(jsonMessage.sid)) return; @@ -155,22 +151,22 @@ class MessageHandler { const newMessage = !this.receivedMessageId[jsonMessage.sid] || this.receivedMessageId[jsonMessage.sid] as number < jsonMessage.id; - if (messageTypeGuard.isConnectMessage(jsonMessage)) { + if (Message.TypeGuard.isConnectMessage(jsonMessage)) { ipc.postAcknowledgeMessage(jsonMessage); if (newMessage) ipc.postConnectToVehicle(jsonMessage); - } else if (messageTypeGuard.isCompleteMessage(jsonMessage)) { + } else if (Message.TypeGuard.isCompleteMessage(jsonMessage)) { ipc.postAcknowledgeMessage(jsonMessage); if (newMessage) ipc.postHandleCompleteMessage(jsonMessage); - } else if (messageTypeGuard.isPOIMessage(jsonMessage)) { + } else if (Message.TypeGuard.isPOIMessage(jsonMessage)) { ipc.postAcknowledgeMessage(jsonMessage); if (newMessage) ipc.postHandlePOIMessage(jsonMessage); - } else if (messageTypeGuard.isUpdateMessage(jsonMessage)) { + } else if (Message.TypeGuard.isUpdateMessage(jsonMessage)) { ipc.postAcknowledgeMessage(jsonMessage); if (newMessage) ipc.postHandleUpdateMessage(jsonMessage); - } else if (messageTypeGuard.isBadMessage(jsonMessage)) { + } else if (Message.TypeGuard.isBadMessage(jsonMessage)) { if (newMessage) ipc.postHandleBadMessage(jsonMessage); - } else if (messageTypeGuard.isAcknowledgementMessage(jsonMessage)) { - const { ackid } = jsonMessage as AcknowledgementMessage; + } else if (Message.TypeGuard.isAcknowledgementMessage(jsonMessage)) { + const { ackid } = jsonMessage as Message.AcknowledgementMessage; if (newMessage) { const hash = `${jsonMessage.sid}#${ackid}`; From 81db431ea56c64cd2f1ac4e62f3a343ac10a365f Mon Sep 17 00:00:00 2001 From: Paul Matthew Barrameda Date: Wed, 17 Apr 2019 02:33:09 -0700 Subject: [PATCH 093/152] Fixed typings Mission Window + Missions need work --- src/renderer/missionWindow/MissionWindow.tsx | 20 +++++--------------- src/types/message.ts | 2 +- 2 files changed, 6 insertions(+), 16 deletions(-) diff --git a/src/renderer/missionWindow/MissionWindow.tsx b/src/renderer/missionWindow/MissionWindow.tsx index 6369a2f3..5652d9de 100644 --- a/src/renderer/missionWindow/MissionWindow.tsx +++ b/src/renderer/missionWindow/MissionWindow.tsx @@ -1,11 +1,7 @@ import React, { Component, ReactNode } from 'react'; -import { - ISRSearchMissionParameters, - PayloadDropMissionParameters, - UGVRetreiveMissionParameters, -} from '../../types/message'; -import { ThemeProps } from '../../types/types'; +import { ThemeProps } from '../../types/componentStyle'; +// import * as MissionInformation from '../../types/missionInformation'; import ISRSearch from './parameters/ISRSearch'; import PayLoadDrop from './parameters/PayloadDrop'; @@ -16,22 +12,16 @@ import VTOLSearch from './parameters/VTOLSearch'; const layouts: { [missionName: string]: () => ReactNode } = { payloadDrop: PayLoadDrop, isrSearch: ISRSearch, - ugvRetrieve: UGVRetrieveTarget, - uuvRetrieve: UUVRetreiveTarget, + ugvRescue: UGVRetrieveTarget, + uuvRescue: UUVRetreiveTarget, vtolSearch: VTOLSearch, }; -interface Parameters { - isrSearch: ISRSearchMissionParameters; - payloadDrop: PayloadDropMissionParameters; - ugvRetrieve: UGVRetreiveMissionParameters; -} - type MissionName = 'isrSearch' | 'vtolSearch' | 'payloadDrop' | 'ugvRetrieve' | 'uuvRetrieve'; interface State { firstMission: MissionName; - parameters?: Parameters; + // information: Information; } /** diff --git a/src/types/message.ts b/src/types/message.ts index 614a7855..673ae752 100644 --- a/src/types/message.ts +++ b/src/types/message.ts @@ -8,7 +8,7 @@ import { JobType, vehicleConfig } from '../static/index'; -import { isVehicleStatus, VehicleStatus } from './types'; +import { isVehicleStatus, VehicleStatus } from './vehicle'; import * as Task from './task'; From 6832825e645c6c007c1718934a73ba268b3bec33 Mon Sep 17 00:00:00 2001 From: Paul Matthew Barrameda Date: Wed, 17 Apr 2019 16:10:17 -0700 Subject: [PATCH 094/152] Added msgpack support to Xbee --- package.json | 3 ++- src/common/Xbee.ts | 5 +++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index de25a65a..2666e472 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "@yaga/leaflet-cached-tile-layer": "^1.0.0", "leaflet": "^1.4.0", "moment": "^2.24.0", + "msgpack-lite": "^0.1.26", "pouchdb": "^7.0.0", "prop-types": "^15.7.2", "react": "^16.8.6", @@ -34,6 +35,7 @@ "@babel/preset-react": "^7.0.0", "@types/chai": "^4.1.7", "@types/mocha": "^5.2.6", + "@types/msgpack-lite": "^0.1.6", "@types/node": "^11.13.4", "@types/pouchdb": "^6.3.3", "@types/react": "^16.8.13", @@ -62,7 +64,6 @@ "stylelint": "^9.10.1", "stylelint-config-standard": "^18.2.0", "stylelint-order": "^2.2.1", - "ts-mocha": "^6.0.0", "ts-node": "^8.0.3", "typescript": "^3.4.3", "webpack": "^4.30.0" diff --git a/src/common/Xbee.ts b/src/common/Xbee.ts index 3e5dce8d..5736d1e0 100644 --- a/src/common/Xbee.ts +++ b/src/common/Xbee.ts @@ -1,3 +1,4 @@ +import msgpack from 'msgpack-lite'; import SerialPort from 'serialport'; import { constants as C, Frame, XBeeAPI } from 'xbee-api'; @@ -45,7 +46,7 @@ function sendMessage(message: JSONMessage): void { xbeeAPI.builder.write({ type: C.FRAME_TYPE.ZIGBEE_TRANSMIT_STATUS, destination64: macAddress, - data: message, + data: msgpack.encode(message), }); } @@ -89,7 +90,7 @@ serialport.on('close', (): void => { xbeeAPI.parser.on('data', (frame: Frame): void => { if (frame.type !== C.FRAME_TYPE.ZIGBEE_RECEIVE_PACKET || !frame.data) return; - ipc.postReceiveMessage(frame.data.toString()); + ipc.postReceiveMessage(msgpack.decode(frame.data)); }); export default { From 78c39c19fd20e3d9488b31146241101f7955f8fd Mon Sep 17 00:00:00 2001 From: Paul Matthew Barrameda Date: Thu, 18 Apr 2019 01:13:13 -0700 Subject: [PATCH 095/152] Log container has timeout Fixes #64 --- package.json | 14 +-- src/renderer/mainWindow/log/LogContainer.tsx | 94 ++++++++++++++++++-- 2 files changed, 92 insertions(+), 16 deletions(-) diff --git a/package.json b/package.json index 2666e472..d18d7f77 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "GCS", - "version": "0.6.3", + "version": "0.6.4", "license": "MIT", "author": "Northrop Grumman Collaboration Project", "description": "Ground Control Station for autonomous vehicle platforms in NGCP", @@ -36,7 +36,7 @@ "@types/chai": "^4.1.7", "@types/mocha": "^5.2.6", "@types/msgpack-lite": "^0.1.6", - "@types/node": "^11.13.4", + "@types/node": "^11.13.5", "@types/pouchdb": "^6.3.3", "@types/react": "^16.8.13", "@types/react-dom": "^16.8.4", @@ -49,7 +49,7 @@ "chai": "^4.2.0", "dotenv-webpack": "^1.7.0", "electron": "^3.1.8", - "electron-builder": "^20.39.0", + "electron-builder": "^20.40.2", "electron-rebuild": "^1.8.4", "electron-webpack": "^2.6.2", "electron-webpack-ts": "^3.1.1", @@ -57,14 +57,14 @@ "eslint-config-airbnb": "^17.1.0", "eslint-import-resolver-typescript": "^1.1.1", "eslint-plugin-chai-friendly": "^0.4.1", - "eslint-plugin-import": "^2.17.0", + "eslint-plugin-import": "^2.17.2", "eslint-plugin-jsx-a11y": "^6.2.1", "eslint-plugin-react": "^7.12.4", "mocha": "^6.1.3", - "stylelint": "^9.10.1", - "stylelint-config-standard": "^18.2.0", + "stylelint": "^10.0.1", + "stylelint-config-standard": "^18.3.0", "stylelint-order": "^2.2.1", - "ts-node": "^8.0.3", + "ts-node": "^8.1.0", "typescript": "^3.4.3", "webpack": "^4.30.0" }, diff --git a/src/renderer/mainWindow/log/LogContainer.tsx b/src/renderer/mainWindow/log/LogContainer.tsx index 11ab6202..e45a0239 100644 --- a/src/renderer/mainWindow/log/LogContainer.tsx +++ b/src/renderer/mainWindow/log/LogContainer.tsx @@ -1,6 +1,12 @@ import { Event, ipcRenderer } from 'electron'; import moment, { Moment } from 'moment'; -import React, { Component, FormEvent, ReactNode } from 'react'; +import React, { + Component, + createRef, + FormEvent, + ReactNode, + RefObject, +} from 'react'; import { AutoSizer, CellMeasurerCache, @@ -33,12 +39,40 @@ interface State { * of messages). */ filteredMessages: ComponentStyle.LogMessage[]; + + /** + * Scroll to newest element or not. + */ + scrollToBottom: boolean; } /** * Container that displays messages regarding status, error, etc. */ export default class LogContainer extends Component { + /** + * Value to ensure the onScroll works as intended (at least in our case). + */ + private scrollFromUser = true; + + /** + * Timeout for scrollFromUser variable. + */ + private scrollFromUserTimer: NodeJS.Timeout = setTimeout((): void => {}, 200);; + + /** + * Cache that stores the height for all log messages. Allows the messages to have proper height. + */ + private heightCache = new CellMeasurerCache({ + fixedWidth: true, + minHeight: 20, + });; + + /** + * Reference to log. + */ + private ref: RefObject = createRef(); + public constructor(props: ComponentStyle.ThemeProps) { super(props); @@ -46,17 +80,20 @@ export default class LogContainer extends Component { + this.setState({ scrollToBottom: true }); + this.onRowsRenderered(); + }, 5000); } public componentDidMount(): void { @@ -64,9 +101,43 @@ export default class LogContainer extends Component { + this.setState({ scrollToBottom: true }); + this.onRowsRenderered(); + }, 3000); + } + + /** + * Checks whenever rows are rendered. + */ + private onRowsRenderered(): void { + const { filteredMessages, scrollToBottom } = this.state; + + if (scrollToBottom) { + const list = this.ref.current; + if (!list) return; + + this.scrollFromUser = false; + clearTimeout(this.scrollFromUserTimer); + this.scrollFromUserTimer = setTimeout((): void => { this.scrollFromUser = true; }, 150); + list.scrollToRow(filteredMessages.length - 1); + } + } + + /** + * Timeout that will scroll to bottom when it times out. + */ + private scrollTimer: NodeJS.Timeout; /** * Custom function to render a row in the list. @@ -123,6 +194,8 @@ export default class LogContainer extends Component message.type === newFilter), }); + + this.onRowsRenderered(); } /** @@ -150,6 +223,8 @@ export default class LogContainer extends Component )} From fc5ef7a99de003aeb58b919b31c0523559c350e4 Mon Sep 17 00:00:00 2001 From: Paul Matthew Barrameda Date: Thu, 18 Apr 2019 02:00:19 -0700 Subject: [PATCH 096/152] Finish ISR Search backend --- src/common/missions/ISRSearch.ts | 31 ++++++++++-------------------- src/util/util.ts | 33 ++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 21 deletions(-) diff --git a/src/common/missions/ISRSearch.ts b/src/common/missions/ISRSearch.ts index 345be274..539d2e66 100644 --- a/src/common/missions/ISRSearch.ts +++ b/src/common/missions/ISRSearch.ts @@ -8,6 +8,7 @@ import * as MissionInformation from '../../types/missionInformation'; import { Task, TaskParameters } from '../../types/task'; import ipc from '../../util/ipc'; +import { getBoundingBox } from '../../util/util'; import Vehicle from '../struct/Vehicle'; @@ -73,38 +74,26 @@ class ISRSearch extends Mission { return undefined; // Mission will stop from this, not complete. } - let bottom: number = Number.MIN_VALUE; - let top: number = Number.MAX_VALUE; - - let left: number = Number.MIN_VALUE; - let right: number = Number.MAX_VALUE; - - this.missionData.forEach((data): void => { - if (!bottom || data.lat < bottom) bottom = data.lat; - if (!top || data.lat > top) top = data.lat; - - if (!left || data.lng < left) left = data.lng; - if (!right || data.lng > right) right = data.lng; - }); + const boundingBox = getBoundingBox(this.missionData, 15); return { quickScan: { waypoints: [ { - lat: top, - lng: left, + lat: boundingBox.top, + lng: boundingBox.left, }, { - lat: top, - lng: right, + lat: boundingBox.top, + lng: boundingBox.right, }, { - lat: bottom, - lng: left, + lat: boundingBox.bottom, + lng: boundingBox.left, }, { - lat: bottom, - lng: right, + lat: boundingBox.bottom, + lng: boundingBox.right, }, ], }, diff --git a/src/util/util.ts b/src/util/util.ts index e0b545c0..1778bdf1 100644 --- a/src/util/util.ts +++ b/src/util/util.ts @@ -1,5 +1,7 @@ import { Component } from 'react'; +import { LatLngZoom } from '../static/index'; + import { VehicleObject } from '../types/vehicle'; /** @@ -69,3 +71,34 @@ export function searchIndex( return left; } + +interface BoundingBox { + top: number; + bottom: number; + left: number; + right: number; +} + +/** + * Calculates bounding box given a list of (lat, lng). + * + * @param waypoints Points in bounding box. + * @param error Extra margin of error around box. + */ +export function getBoundingBox(waypoints: LatLngZoom[], error: number): BoundingBox { + const top = Math.max(...waypoints.map((waypoint): number => waypoint.lat)); + const bottom = Math.min(...waypoints.map((waypoint): number => waypoint.lat)); + const left = Math.max(...waypoints.map((waypoint): number => waypoint.lng)); + const right = Math.min(...waypoints.map((waypoint): number => waypoint.lng)); + + // Units in meters. https://stackoverflow.com/a/39540339. + const deltaLat = error / 111.32 / 1000; + const deltaLng = error * 360 / 40075 / Math.cos((top + bottom) / 2) / 1000; + + return { + top: top + deltaLat, + bottom: bottom - deltaLat, + left: left - deltaLng, + right: right + deltaLng, + }; +} From f4a34e109fda5878916a99b57b73fe1cbb28ec05 Mon Sep 17 00:00:00 2001 From: Paul Matthew Barrameda Date: Thu, 18 Apr 2019 03:02:06 -0700 Subject: [PATCH 097/152] Added all other missions All that is needed from GCS is the UI now --- src/common/missions/ISRSearch.ts | 31 ++++---- src/common/missions/PayloadDrop.ts | 102 ++++++++++++++++++++++++ src/common/missions/UGVRescue.ts | 60 +++++++++++++++ src/common/missions/UUVRescue.ts | 54 +++++++++++++ src/common/missions/VTOLSearch.ts | 120 +++++++++++++++++++++++++++++ src/common/missions/index.ts | 8 ++ src/common/struct/Mission.ts | 6 +- src/static/index.ts | 2 +- src/static/vehicle.json | 4 +- src/util/util.ts | 7 ++ 10 files changed, 373 insertions(+), 21 deletions(-) create mode 100644 src/common/missions/PayloadDrop.ts create mode 100644 src/common/missions/UGVRescue.ts create mode 100644 src/common/missions/UUVRescue.ts create mode 100644 src/common/missions/VTOLSearch.ts diff --git a/src/common/missions/ISRSearch.ts b/src/common/missions/ISRSearch.ts index 539d2e66..340bbea6 100644 --- a/src/common/missions/ISRSearch.ts +++ b/src/common/missions/ISRSearch.ts @@ -12,9 +12,9 @@ import { getBoundingBox } from '../../util/util'; import Vehicle from '../struct/Vehicle'; -export const missionName: MissionInformation.MissionName = 'isrSearch'; +const missionName: MissionInformation.MissionName = 'isrSearch'; -export const jobTypes: JobType[] = ['isrSearch']; +const jobTypes: JobType[] = ['isrSearch']; class ISRSearch extends Mission { protected missionName = missionName; @@ -65,11 +65,24 @@ class ISRSearch extends Mission { return tasks; } + public update(jsonMessage: Message.JSONMessage): void { + super.update(jsonMessage); + + if (Message.TypeGuard.isPOIMessage(jsonMessage) + && this.activeVehicleMapping[jsonMessage.sid] === 'isrSearch') { + const poiMessage = jsonMessage as Message.POIMessage; + this.missionData.push({ + lat: poiMessage.lat, + lng: poiMessage.lng, + }); + } + } + protected generateCompletionParameters(): { [key: string]: TaskParameters } | undefined { if (this.missionData.length === 0) { ipc.postLogMessages({ type: 'failure', - message: 'No points of interests were found in the mission', + message: 'No points of interests were found in ISR search', }); return undefined; // Mission will stop from this, not complete. } @@ -99,18 +112,6 @@ class ISRSearch extends Mission { }, }; } - - public update(jsonMessage: Message.JSONMessage): void { - super.update(jsonMessage); - - if (Message.TypeGuard.isPOIMessage(jsonMessage)) { - const poiMessage = jsonMessage as Message.POIMessage; - this.missionData.push({ - lat: poiMessage.lat, - lng: poiMessage.lng, - }); - } - } } export default { diff --git a/src/common/missions/PayloadDrop.ts b/src/common/missions/PayloadDrop.ts new file mode 100644 index 00000000..c3b24b5e --- /dev/null +++ b/src/common/missions/PayloadDrop.ts @@ -0,0 +1,102 @@ +import DictionaryList from '../struct/DictionaryList'; +import Mission from '../struct/Mission'; + +import { JobType, LatLngZoom } from '../../static/index'; + +import * as Message from '../../types/message'; +import * as MissionInformation from '../../types/missionInformation'; +import { Task, TaskParameters } from '../../types/task'; + +import ipc from '../../util/ipc'; + +import Vehicle from '../struct/Vehicle'; + +const missionName: MissionInformation.MissionName = 'payloadDrop'; + +const jobTypes: JobType[] = ['payloadDrop']; + +class PayloadDrop extends Mission { + protected missionName = missionName; + + protected jobTypes = new Set(jobTypes); + + protected addTaskCompare = {}; + + protected information: MissionInformation.PayloadDropInformation; + + /** + * Point of interest. + */ + private missionData: LatLngZoom | null = null; + + public constructor( + vehicles: { [vehicleId: number]: Vehicle }, + information: MissionInformation.PayloadDropInformation, + activeVehicleMapping: { [vehicleId: number]: JobType }, + ) { + super(vehicles, information, activeVehicleMapping); + this.information = information; + } + + protected generateTasks(): DictionaryList | undefined { + const missionParameters = this.information.parameters; + const tasks = new DictionaryList(); + + if (!this.information.options.noTakeoff) { + tasks.push('payloadDrop', { + taskType: 'takeoff', + ...missionParameters.takeoff, + }); + } + + tasks.push('payloadDrop', { + taskType: 'payloadDrop', + ...missionParameters.payloadDrop, + }); + + if (!this.information.options.noLand) { + tasks.push('payloadDrop', { + taskType: 'land', + ...missionParameters.land, + }); + } + + return tasks; + } + + public update(jsonMessage: Message.JSONMessage): void { + super.update(jsonMessage); + + if (Message.TypeGuard.isPOIMessage(jsonMessage) + && this.activeVehicleMapping[jsonMessage.sid] === 'payloadDrop') { + const poiMessage = jsonMessage as Message.POIMessage; + this.missionData = { + lat: poiMessage.lat, + lng: poiMessage.lng, + }; + } + } + + protected generateCompletionParameters(): { [key: string]: TaskParameters } | undefined { + if (!this.missionData) { + ipc.postLogMessages({ + type: 'failure', + message: 'No target location was provided in payload drop', + }); + return undefined; // Mission will stop from this, not complete. + } + + return { + payloadDrop: { + lat: this.missionData.lat, + lng: this.missionData.lng, + }, + }; + } +} + +export default { + missionName, + jobTypes, + constructor: PayloadDrop, +}; diff --git a/src/common/missions/UGVRescue.ts b/src/common/missions/UGVRescue.ts new file mode 100644 index 00000000..d4282632 --- /dev/null +++ b/src/common/missions/UGVRescue.ts @@ -0,0 +1,60 @@ +import DictionaryList from '../struct/DictionaryList'; +import Mission from '../struct/Mission'; + +import { JobType } from '../../static/index'; + +import * as MissionInformation from '../../types/missionInformation'; +import { Task, TaskParameters } from '../../types/task'; + +import Vehicle from '../struct/Vehicle'; + +const missionName: MissionInformation.MissionName = 'ugvRescue'; + +const jobTypes: JobType[] = ['ugvRescue']; + +class UGVRescue extends Mission { + protected missionName = missionName; + + protected jobTypes = new Set(jobTypes); + + protected addTaskCompare = {}; + + protected information: MissionInformation.UGVRescueInformation; + + public constructor( + vehicles: { [vehicleId: number]: Vehicle }, + information: MissionInformation.UGVRescueInformation, + activeVehicleMapping: { [vehicleId: number]: JobType }, + ) { + super(vehicles, information, activeVehicleMapping); + this.information = information; + } + + protected generateTasks(): DictionaryList | undefined { + const missionParameters = this.information.parameters; + const tasks = new DictionaryList(); + + tasks.push('ugvRescue', { + taskType: 'retrieveTarget', + ...missionParameters.retrieveTarget, + }); + + tasks.push('ugvRescue', { + taskType: 'deliverTarget', + ...missionParameters.deliverTarget, + }); + + return tasks; + } + + // eslint-disable-next-line class-methods-use-this + protected generateCompletionParameters(): { [key: string]: TaskParameters } | undefined { + return {}; + } +} + +export default { + missionName, + jobTypes, + constructor: UGVRescue, +}; diff --git a/src/common/missions/UUVRescue.ts b/src/common/missions/UUVRescue.ts new file mode 100644 index 00000000..dfaa3487 --- /dev/null +++ b/src/common/missions/UUVRescue.ts @@ -0,0 +1,54 @@ +import DictionaryList from '../struct/DictionaryList'; +import Mission from '../struct/Mission'; + +import { JobType } from '../../static/index'; + +import * as MissionInformation from '../../types/missionInformation'; +import { Task, TaskParameters } from '../../types/task'; + +import Vehicle from '../struct/Vehicle'; + +const missionName: MissionInformation.MissionName = 'uuvRescue'; + +const jobTypes: JobType[] = ['uuvRescue']; + +class UUVRescue extends Mission { + protected missionName = missionName; + + protected jobTypes = new Set(jobTypes); + + protected addTaskCompare = {}; + + protected information: MissionInformation.UUVRescueInformation; + + public constructor( + vehicles: { [vehicleId: number]: Vehicle }, + information: MissionInformation.UUVRescueInformation, + activeVehicleMapping: { [vehicleId: number]: JobType }, + ) { + super(vehicles, information, activeVehicleMapping); + this.information = information; + } + + // eslint-disable-next-line class-methods-use-this + protected generateTasks(): DictionaryList | undefined { + const tasks = new DictionaryList(); + + tasks.push('uuvRescue', { + taskType: 'retrieveTarget', + }); + + return tasks; + } + + // eslint-disable-next-line class-methods-use-this + protected generateCompletionParameters(): { [key: string]: TaskParameters } | undefined { + return {}; + } +} + +export default { + missionName, + jobTypes, + constructor: UUVRescue, +}; diff --git a/src/common/missions/VTOLSearch.ts b/src/common/missions/VTOLSearch.ts new file mode 100644 index 00000000..e4289fa5 --- /dev/null +++ b/src/common/missions/VTOLSearch.ts @@ -0,0 +1,120 @@ +import DictionaryList from '../struct/DictionaryList'; +import Mission from '../struct/Mission'; + +import { JobType, LatLngZoom } from '../../static/index'; + +import * as Message from '../../types/message'; +import * as MissionInformation from '../../types/missionInformation'; +import * as Task from '../../types/task'; + +import ipc from '../../util/ipc'; +import { getDistance } from '../../util/util'; + +import Vehicle from '../struct/Vehicle'; + +const missionName: MissionInformation.MissionName = 'vtolSearch'; + +const jobTypes: JobType[] = ['quickScan', 'detailedSearch']; + +class VTOLSearch extends Mission { + protected missionName = missionName; + + protected jobTypes = new Set(jobTypes); + + protected addTaskCompare = { + detailedSearch: (a: Task.Task, b: Task.Task): number => { + if (!Task.TypeGuard.isDetailedSearchTask(a)) return 1; + if (!Task.TypeGuard.isDetailedSearchTask(b)) return 1; + + const vehicle = this.getDetailedSearchVehicle(); + if (!vehicle) return 1; + + const xa = a as Task.DetailedSearchTask; + const xb = b as Task.DetailedSearchTask; + + return getDistance({ lat: vehicle.getLat(), lng: vehicle.getLng() }, xa) + - getDistance({ lat: vehicle.getLat(), lng: vehicle.getLng() }, xb); + }, + }; + + protected information: MissionInformation.VTOLSearchInformation; + + /** + * Point of interest. + */ + private missionData: LatLngZoom | null = null; + + public constructor( + vehicles: { [vehicleId: number]: Vehicle }, + information: MissionInformation.VTOLSearchInformation, + activeVehicleMapping: { [vehicleId: number]: JobType }, + ) { + super(vehicles, information, activeVehicleMapping); + this.information = information; + } + + private getDetailedSearchVehicle(): Vehicle | undefined { + const vehicleIdString = Object.keys(this.activeVehicleMapping) + .find((idString): boolean => { + const id = parseInt(idString, 10); + return this.activeVehicleMapping[id] === 'detailedSearch'; + }); + if (!vehicleIdString) return undefined; + + const vehicleId = parseInt(vehicleIdString, 10); + return this.vehicles[vehicleId]; + } + + protected generateTasks(): DictionaryList | undefined { + const missionParameters = this.information.parameters; + const tasks = new DictionaryList(); + + tasks.push('quickScan', { + taskType: 'quickScan', + ...missionParameters.quickScan, + }); + + return tasks; + } + + public update(jsonMessage: Message.JSONMessage): void { + super.update(jsonMessage); + + if (Message.TypeGuard.isPOIMessage(jsonMessage)) { + const poiMessage = jsonMessage as Message.POIMessage; + + if (this.activeVehicleMapping[jsonMessage.sid] === 'quickScan') { + this.addTask('detailedSearch', { + taskType: 'detailedSearch', + lat: poiMessage.lat, + lng: poiMessage.lng, + }); + } else if (this.activeVehicleMapping[jsonMessage.sid] === 'detailedSearch') { + this.missionData = { lat: poiMessage.lat, lng: poiMessage.lng }; + } + } + } + + protected generateCompletionParameters(): { [key: string]: Task.TaskParameters } | undefined { + if (!this.missionData) { + ipc.postLogMessages({ + type: 'failure', + message: 'No points of interest was found in detailed search', + }); + return undefined; // Mission will stop from this, not complete. + } + + return { + payloadDrop: { + lat: this.missionData.lat, + lng: this.missionData.lng, + }, + }; + } +} + +export default { + missionName, + jobTypes, + constructor: VTOLSearch, +}; diff --git a/src/common/missions/index.ts b/src/common/missions/index.ts index ce7d080f..e7d78a28 100644 --- a/src/common/missions/index.ts +++ b/src/common/missions/index.ts @@ -5,6 +5,10 @@ import { MissionName } from '../../types/missionInformation'; import Mission from '../struct/Mission'; import ISRSearch from './ISRSearch'; +import PayloadDrop from './PayloadDrop'; +import UGVRescue from './UGVRescue'; +import UUVRescue from './UUVRescue'; +import VTOLSearch from './VTOLSearch'; type MissionType = typeof Mission; @@ -33,6 +37,10 @@ export interface MissionObject { const missionObject: { [missionName: string]: MissionObject | undefined } = { isrSearch: ISRSearch, + vtolSearch: VTOLSearch, + payloadDrop: PayloadDrop, + ugvRescue: UGVRescue, + uuvRescue: UUVRescue, }; export default missionObject; diff --git a/src/common/struct/Mission.ts b/src/common/struct/Mission.ts index ef264d6d..c1de5ff2 100644 --- a/src/common/struct/Mission.ts +++ b/src/common/struct/Mission.ts @@ -33,7 +33,7 @@ export default abstract class Mission { /** * All connected vehicles (at least when this class was created). */ - private vehicles: { [vehicleId: number]: Vehicle }; + protected vehicles: { [vehicleId: number]: Vehicle }; /** * Current status of mission. @@ -57,7 +57,7 @@ export default abstract class Mission { * * There will be no job types in this mapping that are irrelevant to the mission. */ - private activeVehicleMapping: { [vehicleId: number]: JobType } = {}; + protected activeVehicleMapping: { [vehicleId: number]: JobType } = {}; /** * Comparison to how tasks are added to the waiting task list. If there is no @@ -374,7 +374,7 @@ export default abstract class Mission { * @param addToFront True if the task should be added to the front of the queue * instead of the back. */ - protected addTask(jobType: string, task: Task, addToFront?: boolean): void { + protected addTask(jobType: JobType, task: Task, addToFront?: boolean): void { if (this.status !== 'running') { this.stop(false, `Tried to add ${task.taskType} task while ${this.missionName} is not running`); return; diff --git a/src/static/index.ts b/src/static/index.ts index 5e9ac484..da3092b0 100644 --- a/src/static/index.ts +++ b/src/static/index.ts @@ -62,7 +62,7 @@ export const vehicleIds: { /** * All valid job types. This should always match up to the job types in vehicle.json. */ -export type JobType = 'isrSearch' | 'payloadDrop' | 'ugvRetrieve' | 'uuvRetrieve' +export type JobType = 'isrSearch' | 'payloadDrop' | 'ugvRescue' | 'uuvRescue' | 'quickScan' | 'detailedSearch'; const vehicleInfos: { diff --git a/src/static/vehicle.json b/src/static/vehicle.json index e945a3d4..6988bca4 100644 --- a/src/static/vehicle.json +++ b/src/static/vehicle.json @@ -53,8 +53,8 @@ "vehicleJobs": { "isrSearch": ["takeoff", "loiter", "isrSearch", "land"], "payloadDrop": ["takeoff", "loiter", "payloadDrop", "land"], - "ugvRetrieve": ["retrieveTarget", "deliverTarget"], - "uuvRetrieve": ["retrieveTarget"], + "ugvRescue": ["retrieveTarget", "deliverTarget"], + "uuvRescue": ["retrieveTarget"], "quickScan": ["quickScan"], "detailedSearch": ["detailedSearch"] }, diff --git a/src/util/util.ts b/src/util/util.ts index 1778bdf1..72d56203 100644 --- a/src/util/util.ts +++ b/src/util/util.ts @@ -102,3 +102,10 @@ export function getBoundingBox(waypoints: LatLngZoom[], error: number): Bounding right: right + deltaLng, }; } + +/** + * Calculates distance between two points. + */ +export function getDistance(x: LatLngZoom, y: LatLngZoom): number { + return Math.sqrt(((x.lat - y.lat) ** 2) + ((x.lng - y.lng) ** 2)); +} From 88213bd771d4271b860965ef4f1da9087eb11d4d Mon Sep 17 00:00:00 2001 From: Paul Matthew Barrameda Date: Thu, 18 Apr 2019 03:07:56 -0700 Subject: [PATCH 098/152] Update updateVehiclesFixtures.ts --- src/renderer/fixtures/updateVehiclesFixtures.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/renderer/fixtures/updateVehiclesFixtures.ts b/src/renderer/fixtures/updateVehiclesFixtures.ts index a9d5c127..acc043b4 100644 --- a/src/renderer/fixtures/updateVehiclesFixtures.ts +++ b/src/renderer/fixtures/updateVehiclesFixtures.ts @@ -13,11 +13,11 @@ const fixtureOptions: VehicleOptions[] = [ }, { sid: 200, - jobs: ['ugvRetrieve'], + jobs: ['ugvRescue'], }, { sid: 300, - jobs: ['uuvRetrieve'], + jobs: ['uuvRescue'], }, { sid: 400, From a17576d8705211fc3b03ca38c649889d4bcb2947 Mon Sep 17 00:00:00 2001 From: Paul Matthew Barrameda Date: Thu, 18 Apr 2019 18:42:15 -0700 Subject: [PATCH 099/152] Added boundingBox and waypoints variable to MapContainer MapContainer needs events that will update and remove those variables accordingly from the MissionUI. Perhaps at mission complete, delete all variables, at change mission change displayed variables, etc... Will need to emit a mission change from Orchestrator if mission changes automatically, or from UI if mission changes manually (thru user selecting a different start mission). Look into the following for a resizeable bounding box: https://github.com/w8r/Leaflet.Path.Transform --- src/common/missions/ISRSearch.ts | 4 +- src/common/missions/PayloadDrop.ts | 4 +- src/common/missions/VTOLSearch.ts | 4 +- src/main/index.ts | 4 +- src/renderer/mainWindow/map/MapContainer.tsx | 102 +++++++++++++++--- .../map/{ => marker}/VehicleMarker.tsx | 8 +- .../mainWindow/map/marker/WaypointMarker.tsx | 36 +++++++ src/static/index.ts | 12 ++- src/types/componentStyle.ts | 10 ++ src/types/fileOption.ts | 4 +- src/util/ipc.ts | 12 +-- src/util/util.ts | 14 +-- 12 files changed, 162 insertions(+), 52 deletions(-) rename src/renderer/mainWindow/map/{ => marker}/VehicleMarker.tsx (86%) create mode 100644 src/renderer/mainWindow/map/marker/WaypointMarker.tsx diff --git a/src/common/missions/ISRSearch.ts b/src/common/missions/ISRSearch.ts index 340bbea6..ff1821d0 100644 --- a/src/common/missions/ISRSearch.ts +++ b/src/common/missions/ISRSearch.ts @@ -1,7 +1,7 @@ import DictionaryList from '../struct/DictionaryList'; import Mission from '../struct/Mission'; -import { JobType, LatLngZoom } from '../../static/index'; +import { JobType, Location } from '../../static/index'; import * as Message from '../../types/message'; import * as MissionInformation from '../../types/missionInformation'; @@ -28,7 +28,7 @@ class ISRSearch extends Mission { /** * List of point of interests. */ - private missionData: LatLngZoom[] = []; + private missionData: Location[] = []; public constructor( vehicles: { [vehicleId: number]: Vehicle }, diff --git a/src/common/missions/PayloadDrop.ts b/src/common/missions/PayloadDrop.ts index c3b24b5e..33ecca21 100644 --- a/src/common/missions/PayloadDrop.ts +++ b/src/common/missions/PayloadDrop.ts @@ -1,7 +1,7 @@ import DictionaryList from '../struct/DictionaryList'; import Mission from '../struct/Mission'; -import { JobType, LatLngZoom } from '../../static/index'; +import { JobType, Location } from '../../static/index'; import * as Message from '../../types/message'; import * as MissionInformation from '../../types/missionInformation'; @@ -27,7 +27,7 @@ class PayloadDrop extends Mission { /** * Point of interest. */ - private missionData: LatLngZoom | null = null; + private missionData: Location | null = null; public constructor( vehicles: { [vehicleId: number]: Vehicle }, diff --git a/src/common/missions/VTOLSearch.ts b/src/common/missions/VTOLSearch.ts index e4289fa5..ab6cd62b 100644 --- a/src/common/missions/VTOLSearch.ts +++ b/src/common/missions/VTOLSearch.ts @@ -1,7 +1,7 @@ import DictionaryList from '../struct/DictionaryList'; import Mission from '../struct/Mission'; -import { JobType, LatLngZoom } from '../../static/index'; +import { JobType, Location } from '../../static/index'; import * as Message from '../../types/message'; import * as MissionInformation from '../../types/missionInformation'; @@ -42,7 +42,7 @@ class VTOLSearch extends Mission { /** * Point of interest. */ - private missionData: LatLngZoom | null = null; + private missionData: Location | null = null; public constructor( vehicles: { [vehicleId: number]: Vehicle }, diff --git a/src/main/index.ts b/src/main/index.ts index 413ce0cb..0273afcf 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -13,7 +13,7 @@ import { import fs from 'fs'; import moment from 'moment'; -import { imageConfig, LatLngZoom, locationConfig } from '../static/index'; +import { imageConfig, Location, locationConfig } from '../static/index'; import { FileSaveOptions } from '../types/fileOption'; @@ -385,7 +385,7 @@ function setLocationMenu(): void { label, click: (menuItem): void => { ipc.postUpdateMapLocation( - locationConfig.locations[menuItem.label] as LatLngZoom, + locationConfig.locations[menuItem.label] as Location, mainWindow, missionWindow, ); diff --git a/src/renderer/mainWindow/map/MapContainer.tsx b/src/renderer/mainWindow/map/MapContainer.tsx index 74614aea..7b52c49d 100644 --- a/src/renderer/mainWindow/map/MapContainer.tsx +++ b/src/renderer/mainWindow/map/MapContainer.tsx @@ -8,11 +8,16 @@ import React, { ReactNode, RefObject, } from 'react'; -import { Map, TileLayer, Viewport } from 'react-leaflet'; +import { + Map, + Rectangle, + TileLayer, + Viewport, +} from 'react-leaflet'; -import { LatLngZoom, locationConfig } from '../../../static/index'; +import { Location, locationConfig } from '../../../static/index'; -import { ThemeProps } from '../../../types/componentStyle'; +import { BoundingBoxBounds, ThemeProps } from '../../../types/componentStyle'; import { FileLoadOptions, FileSaveOptions } from '../../../types/fileOption'; import { VehicleObject } from '../../../types/vehicle'; @@ -22,7 +27,8 @@ import GeolocationControl from './control/GeolocationControl'; import ThemeControl from './control/ThemeControl'; // import CachedTileLayer from './CachedTileLayer'; -import VehicleMarker from './VehicleMarker'; +import VehicleMarker from './marker/VehicleMarker'; +import WaypointMarker from './marker/WaypointMarker'; import './map.css'; @@ -36,17 +42,43 @@ const mapOptions = { /** * State of the map container. */ -interface State extends LatLngZoom { +interface State { /** - * Object of vehicles displayed in the user interface. + * Latitude location of map. + */ + lat: number; + + /** + * Longitude location of map. + */ + lng: number; + + /** + * Zoom of map. */ - vehicles: { [vehicleId: string]: VehicleObject }; + zoom?: number; /** * Viewport storing locations on location of map. This is more precise than * the (lat, lng, zoom) coordinates. */ viewport: Viewport; + + /** + * Object of vehicles displayed in the user interface. + */ + vehicles: { [vehicleId: number]: VehicleObject }; + + /** + * Bounding boxes for the mission. Not all missions need bounding boxes. + */ + boundingBoxes: { [name: string]: { bounds: BoundingBoxBounds; color?: string } }; + + /** + * Waypoints with radius. Basically points that are required for a mission. + * Not all waypoints require a radius. + */ + waypoints: { [name: string]: Location }; } /** @@ -59,12 +91,16 @@ export default class MapContainer extends Component { const { startLocation } = locationConfig; this.state = { - vehicles: {}, - ...startLocation, + lat: startLocation.lat, + lng: startLocation.lng, + zoom: startLocation.zoom, viewport: { center: [startLocation.lat, startLocation.lng], zoom: startLocation.zoom, }, + vehicles: {}, + boundingBoxes: {}, + waypoints: {}, }; this.ref = createRef(); @@ -84,7 +120,7 @@ export default class MapContainer extends Component { ipcRenderer.on('centerMapToVehicle', (_: Event, vehicle: VehicleObject): void => this.centerMapToVehicle(vehicle)); ipcRenderer.on('setMapToUserLocation', this.setMapToUserLocation); - ipcRenderer.on('updateMapLocation', (_: Event, location: LatLngZoom): void => this.updateMapLocation(location)); + ipcRenderer.on('updateMapLocation', (_: Event, location: Location): void => this.updateMapLocation(location)); ipcRenderer.on('updateVehicles', (_: Event, ...vehicles: VehicleObject[]): void => updateVehicles(this, ...vehicles)); } @@ -130,7 +166,9 @@ export default class MapContainer extends Component { private saveConfig(file: FileSaveOptions): void { // Performs a hard copy of this.state, deletes vehicles from it, and saves it to file. const data = { ...this.state }; + delete data.boundingBoxes; delete data.vehicles; + delete data.waypoints; fs.writeFileSync(file.filePath, JSON.stringify({ ...file.data, @@ -148,15 +186,18 @@ export default class MapContainer extends Component { /** * Callback to whenever the map location has been changed. */ - public updateMapLocation(location: LatLngZoom): void { + public updateMapLocation(location: Location): void { const { lat, lng, zoom } = location; const { viewport } = this.state; + // Do not allow null for viewport zoom. + const viewportZoom = viewport.zoom === null ? undefined : viewport.zoom; + this.setState({ lat, lng, - zoom: zoom || (viewport.zoom as number | undefined), + zoom: zoom || viewportZoom, }); this.onViewportChanged({ @@ -167,14 +208,39 @@ export default class MapContainer extends Component { public render(): ReactNode { const { theme } = this.props; - const { viewport, vehicles } = this.state; + const { + boundingBoxes, + vehicles, + viewport, + waypoints, + } = this.state; - const markers = Object - .keys(vehicles) + const boundingBoxRectangles = Object.keys(boundingBoxes) + .map((name): ReactNode => ( + + )); + + const vehicleMarkers = Object.keys(vehicles) .map((vehicleId): ReactNode => ( + )); + + const waypointMarkers = Object.keys(waypoints) + .map((name): ReactNode => ( + )); @@ -190,7 +256,9 @@ export default class MapContainer extends Component { - {markers} + {boundingBoxRectangles} + {vehicleMarkers} + {waypointMarkers} ); } diff --git a/src/renderer/mainWindow/map/VehicleMarker.tsx b/src/renderer/mainWindow/map/marker/VehicleMarker.tsx similarity index 86% rename from src/renderer/mainWindow/map/VehicleMarker.tsx rename to src/renderer/mainWindow/map/marker/VehicleMarker.tsx index 90b7cda8..dea7bb82 100644 --- a/src/renderer/mainWindow/map/VehicleMarker.tsx +++ b/src/renderer/mainWindow/map/marker/VehicleMarker.tsx @@ -8,10 +8,10 @@ import { vehicleConfig, VehicleInfo, VehicleStatus, -} from '../../../static/index'; +} from '../../../../static/index'; -import { MessageType } from '../../../types/componentStyle'; -import { VehicleObject } from '../../../types/vehicle'; +import { MessageType } from '../../../../types/componentStyle'; +import { VehicleObject } from '../../../../types/vehicle'; const vehicleIcons = (imageConfig.markers as RecursiveImageSignature) .vehicles as RecursiveImageSignature; @@ -39,7 +39,7 @@ export default class VehicleMarker extends PureComponent { { + public render(): ReactNode { + const { name, location } = this.props; + + return ( + + +

{name}

+
+
+ ); + } +} diff --git a/src/static/index.ts b/src/static/index.ts index da3092b0..7e4403cf 100644 --- a/src/static/index.ts +++ b/src/static/index.ts @@ -42,17 +42,19 @@ export interface VehicleInfo { } /** - * Latitude, longitude and zoom. + * Latitude and longitude. Optional zoom and radius, used for different + * parts of the application. */ -export interface LatLngZoom { +export interface Location { lat: number; lng: number; zoom?: number; + radius?: number; } // Add signature to json objects to allow us to access it with TypeScript. const locations: { - [name: string]: { lat: number; lng: number; zoom?: number } | undefined; + [name: string]: Location | undefined; } = locationsObject; export const vehicleIds: { @@ -86,8 +88,8 @@ const vehicleStatuses: { } = vehicleStatusesObject; // Add logic to set startLocation. -const startLocation: LatLngZoom = startLocationString && locations[startLocationString] - ? locations[startLocationString] as LatLngZoom : { +const startLocation: Location = startLocationString && locations[startLocationString] + ? locations[startLocationString] as Location : { lat: 0, lng: 0, zoom: 18, diff --git a/src/types/componentStyle.ts b/src/types/componentStyle.ts index 1144c19f..7fd77a3a 100644 --- a/src/types/componentStyle.ts +++ b/src/types/componentStyle.ts @@ -48,3 +48,13 @@ export interface LogMessage { */ time?: Moment; } + +/** + * Bounding box used in a map container as well as in missions. + */ +export interface BoundingBoxBounds { + top: number; + bottom: number; + left: number; + right: number; +} diff --git a/src/types/fileOption.ts b/src/types/fileOption.ts index d65d928d..8b6a2cd4 100644 --- a/src/types/fileOption.ts +++ b/src/types/fileOption.ts @@ -1,4 +1,4 @@ -import { LatLngZoom } from '../static/index'; +import { Location } from '../static/index'; /** * Data contents for information that is loaded from a configuration file. @@ -7,7 +7,7 @@ export interface FileLoadOptions { /** * Information related to the map. */ - map: LatLngZoom; + map: Location; } /** diff --git a/src/util/ipc.ts b/src/util/ipc.ts index 0950e66d..4e6e72cb 100644 --- a/src/util/ipc.ts +++ b/src/util/ipc.ts @@ -16,7 +16,7 @@ import { BrowserWindow, ipcRenderer } from 'electron'; -import { LatLngZoom } from '../static/index'; +import { Location } from '../static/index'; import { LogMessage } from '../types/componentStyle'; import * as FileOptions from '../types/fileOption'; @@ -169,8 +169,8 @@ function postLoadConfig( missionWindow?: BrowserWindow | null, ): void { if (mainWindow !== undefined) { - if (mainWindow) mainWindow.webContents.send('post', 'loadConfig', loadOptions); - if (missionWindow) missionWindow.webContents.send('post', 'loadConfig', loadOptions); + if (mainWindow) mainWindow.webContents.send('loadConfig', loadOptions); + if (missionWindow) missionWindow.webContents.send('loadConfig', loadOptions); } else { ipcRenderer.send('post', 'loadConfig', loadOptions); } @@ -208,8 +208,8 @@ function postSaveConfig( missionWindow?: BrowserWindow | null, ): void { if (mainWindow !== undefined) { - if (mainWindow) mainWindow.webContents.send('post', 'saveConfig', saveOptions); - if (missionWindow) missionWindow.webContents.send('post', 'saveConfig', saveOptions); + if (mainWindow) mainWindow.webContents.send('saveConfig', saveOptions); + if (missionWindow) missionWindow.webContents.send('saveConfig', saveOptions); } else { ipcRenderer.send('post', 'saveConfig', saveOptions); } @@ -313,7 +313,7 @@ function postToggleTheme(): void { * - renderer/mainWindow/map/MapContainer */ function postUpdateMapLocation( - location: LatLngZoom, + location: Location, mainWindow?: BrowserWindow | null, missionWindow?: BrowserWindow | null, ): void { diff --git a/src/util/util.ts b/src/util/util.ts index 72d56203..e34c1181 100644 --- a/src/util/util.ts +++ b/src/util/util.ts @@ -1,7 +1,8 @@ import { Component } from 'react'; -import { LatLngZoom } from '../static/index'; +import { Location } from '../static/index'; +import { BoundingBoxBounds } from '../types/componentStyle'; import { VehicleObject } from '../types/vehicle'; /** @@ -72,20 +73,13 @@ export function searchIndex( return left; } -interface BoundingBox { - top: number; - bottom: number; - left: number; - right: number; -} - /** * Calculates bounding box given a list of (lat, lng). * * @param waypoints Points in bounding box. * @param error Extra margin of error around box. */ -export function getBoundingBox(waypoints: LatLngZoom[], error: number): BoundingBox { +export function getBoundingBox(waypoints: Location[], error: number): BoundingBoxBounds { const top = Math.max(...waypoints.map((waypoint): number => waypoint.lat)); const bottom = Math.min(...waypoints.map((waypoint): number => waypoint.lat)); const left = Math.max(...waypoints.map((waypoint): number => waypoint.lng)); @@ -106,6 +100,6 @@ export function getBoundingBox(waypoints: LatLngZoom[], error: number): Bounding /** * Calculates distance between two points. */ -export function getDistance(x: LatLngZoom, y: LatLngZoom): number { +export function getDistance(x: Location, y: Location): number { return Math.sqrt(((x.lat - y.lat) ** 2) + ((x.lng - y.lng) ** 2)); } From e1d30c65512ca80ffe380d20854c859f3edf4eae Mon Sep 17 00:00:00 2001 From: Paul Matthew Barrameda Date: Thu, 18 Apr 2019 22:12:31 -0700 Subject: [PATCH 100/152] Version bump + update waypoint logic Will test with fixtures in a bit. Then need to work on bounding box. --- package.json | 8 +- src/renderer/mainWindow/map/MapContainer.tsx | 86 +++++++++++++++++-- .../mainWindow/map/marker/VehicleMarker.tsx | 4 +- .../mainWindow/map/marker/WaypointMarker.tsx | 29 ++++++- src/util/ipc.ts | 26 +++++- 5 files changed, 136 insertions(+), 17 deletions(-) diff --git a/package.json b/package.json index d18d7f77..37af15bf 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "GCS", - "version": "0.6.4", + "version": "0.6.5", "license": "MIT", "author": "Northrop Grumman Collaboration Project", "description": "Ground Control Station for autonomous vehicle platforms in NGCP", @@ -60,12 +60,12 @@ "eslint-plugin-import": "^2.17.2", "eslint-plugin-jsx-a11y": "^6.2.1", "eslint-plugin-react": "^7.12.4", - "mocha": "^6.1.3", + "mocha": "^6.1.4", "stylelint": "^10.0.1", "stylelint-config-standard": "^18.3.0", - "stylelint-order": "^2.2.1", + "stylelint-order": "^3.0.0", "ts-node": "^8.1.0", - "typescript": "^3.4.3", + "typescript": "^3.4.4", "webpack": "^4.30.0" }, "build": { diff --git a/src/renderer/mainWindow/map/MapContainer.tsx b/src/renderer/mainWindow/map/MapContainer.tsx index 7b52c49d..9050d8fa 100644 --- a/src/renderer/mainWindow/map/MapContainer.tsx +++ b/src/renderer/mainWindow/map/MapContainer.tsx @@ -1,6 +1,6 @@ import { Event, ipcRenderer } from 'electron'; import fs from 'fs'; -import { LocationEvent } from 'leaflet'; +import Leaflet from 'leaflet'; import React, { Component, createRef, @@ -72,13 +72,42 @@ interface State { /** * Bounding boxes for the mission. Not all missions need bounding boxes. */ - boundingBoxes: { [name: string]: { bounds: BoundingBoxBounds; color?: string } }; + boundingBoxes: { + [name: string]: { + /** + * Boundaries of the bounding box. + */ + bounds: BoundingBoxBounds; + + /** + * Color of the bounding box. + */ + color?: string; + + /** + * Whether or not the box is modifiable (resizeable, etc). + */ + locked: boolean; + }; + }; /** * Waypoints with radius. Basically points that are required for a mission. * Not all waypoints require a radius. */ - waypoints: { [name: string]: Location }; + waypoints: { + [name: string]: { + /** + * Location of the waypoint itself. + */ + location: Location; + + /** + * Whether or not the waypoint is modifiable (draggable, etc). + */ + locked: boolean; + }; + }; } /** @@ -112,6 +141,8 @@ export default class MapContainer extends Component { this.saveConfig = this.saveConfig.bind(this); this.centerMapToVehicle = this.centerMapToVehicle.bind(this); this.updateMapLocation = this.updateMapLocation.bind(this); + this.updateWaypoints = this.updateWaypoints.bind(this); + this.updateBoundingBoxes = this.updateBoundingBoxes.bind(this); } public componentDidMount(): void { @@ -122,6 +153,8 @@ export default class MapContainer extends Component { ipcRenderer.on('setMapToUserLocation', this.setMapToUserLocation); ipcRenderer.on('updateMapLocation', (_: Event, location: Location): void => this.updateMapLocation(location)); ipcRenderer.on('updateVehicles', (_: Event, ...vehicles: VehicleObject[]): void => updateVehicles(this, ...vehicles)); + ipcRenderer.on('updateWaypoints', (_: Event, ...waypoints: { name: string; location: Location }[]): void => this.updateWaypoints(...waypoints)); + ipcRenderer.on('updateBoundingBoxes', (_: Event, ...boundingBoxes: { name: string; bounds: BoundingBoxBounds}[]): void => this.updateBoundingBoxes(...boundingBoxes)); } /** @@ -134,7 +167,7 @@ export default class MapContainer extends Component { /** * Callback to whenever geolocation call is successful. */ - private onlocationfound(location: LocationEvent): void { + private onlocationfound(location: Leaflet.LocationEvent): void { this.updateMapLocation(location.latlng); } @@ -183,10 +216,51 @@ export default class MapContainer extends Component { this.updateMapLocation(vehicle); } + /** + * Updates waypoints. + */ + private updateWaypoints(...waypoints: { name: string; location: Location }[]): void { + const { waypoints: thisWaypoints } = this.state; + const currentWaypoints = thisWaypoints; + + waypoints.forEach((waypoint): void => { + if (!currentWaypoints[waypoint.name]) { + currentWaypoints[waypoint.name] = { location: waypoint.location, locked: false }; + } else { + currentWaypoints[waypoint.name].location = waypoint.location; + } + }); + + this.setState({ waypoints: currentWaypoints }); + } + + /** + * Updates bounding boxes. + */ + private updateBoundingBoxes( + ...boundingBoxes: { name: string; bounds: BoundingBoxBounds }[] + ): void { + const { boundingBoxes: thisBoundingBoxes } = this.state; + const currentBoundingBoxes = thisBoundingBoxes; + + boundingBoxes.forEach((boundingBox): void => { + if (!currentBoundingBoxes[boundingBox.name]) { + currentBoundingBoxes[boundingBox.name] = { + bounds: boundingBox.bounds, + locked: false, + }; + } else { + currentBoundingBoxes[boundingBox.name].bounds = boundingBox.bounds; + } + }); + + this.setState({ boundingBoxes: currentBoundingBoxes }); + } + /** * Callback to whenever the map location has been changed. */ - public updateMapLocation(location: Location): void { + private updateMapLocation(location: Location): void { const { lat, lng, zoom } = location; const { viewport } = this.state; @@ -240,7 +314,7 @@ export default class MapContainer extends Component { )); diff --git a/src/renderer/mainWindow/map/marker/VehicleMarker.tsx b/src/renderer/mainWindow/map/marker/VehicleMarker.tsx index dea7bb82..803f59fb 100644 --- a/src/renderer/mainWindow/map/marker/VehicleMarker.tsx +++ b/src/renderer/mainWindow/map/marker/VehicleMarker.tsx @@ -1,4 +1,4 @@ -import L from 'leaflet'; +import Leaflet from 'leaflet'; import React, { PureComponent, ReactNode } from 'react'; import { Marker, Tooltip } from 'react-leaflet'; @@ -38,7 +38,7 @@ export default class VehicleMarker extends PureComponent { return ( { + public constructor(props: WaypointMarkerProps) { + super(props); + + this.postUpdateWaypoint = this.postUpdateWaypoint.bind(this); + } + + private postUpdateWaypoint(event: Leaflet.LeafletEvent): void { + const { name } = this.props; + const location: Location = event.target.latlng; + + ipc.postUpdateWaypoint({ name, location }); + } + public render(): ReactNode { - const { name, location } = this.props; + const { + locked, + name, + location, + } = this.props; return ( Date: Thu, 18 Apr 2019 23:27:04 -0700 Subject: [PATCH 101/152] Fixed waypoint logic WIll not update the location of the waypoint in the MapContainer since it does not matter. If I try to update the waypoint in the MapContainer, the waypoint will not be able to drag since it will "undrag" itself back to its previous spot. --- src/renderer/fixtures/index.ts | 10 +-- ...agesFixtures.ts => logMessagesFixtures.ts} | 4 +- .../fixtures/updateVehiclesFixtures.ts | 4 +- .../fixtures/updateWaypointsFixtures.ts | 10 +++ src/renderer/mainWindow/map/MapContainer.tsx | 85 ++++++------------- .../mainWindow/map/marker/WaypointMarker.tsx | 9 +- src/util/ipc.ts | 9 +- 7 files changed, 58 insertions(+), 73 deletions(-) rename src/renderer/fixtures/{updateMessagesFixtures.ts => logMessagesFixtures.ts} (86%) create mode 100644 src/renderer/fixtures/updateWaypointsFixtures.ts diff --git a/src/renderer/fixtures/index.ts b/src/renderer/fixtures/index.ts index 334d26d3..fd8a21e2 100644 --- a/src/renderer/fixtures/index.ts +++ b/src/renderer/fixtures/index.ts @@ -1,9 +1,7 @@ // Import all fixtures in here. Feel free to enable/disable fixtures by commenting them out. -import updateMessages from './updateMessagesFixtures'; -import updateVehicles from './updateVehiclesFixtures'; +/* eslint-disable multiline-comment-style */ -setInterval((): void => { - updateMessages(); - updateVehicles(); -}, 1000); +// import './logMessagesFixtures'; +// import './updateVehiclesFixtures'; +import './updateWaypointsFixtures'; diff --git a/src/renderer/fixtures/updateMessagesFixtures.ts b/src/renderer/fixtures/logMessagesFixtures.ts similarity index 86% rename from src/renderer/fixtures/updateMessagesFixtures.ts rename to src/renderer/fixtures/logMessagesFixtures.ts index 68c0e859..e5867779 100644 --- a/src/renderer/fixtures/updateMessagesFixtures.ts +++ b/src/renderer/fixtures/logMessagesFixtures.ts @@ -23,7 +23,9 @@ const fixtures: LogMessage[] = [ /** * Sends an updateMessages notification with random message fixtures. */ -export default function updateMessages(): void { +function updateMessages(): void { const fixture = fixtures[Math.floor(Math.random() * fixtures.length)]; ipc.postLogMessages(fixture); } + +setInterval((): void => { updateMessages(); }, 1000); diff --git a/src/renderer/fixtures/updateVehiclesFixtures.ts b/src/renderer/fixtures/updateVehiclesFixtures.ts index acc043b4..b4a5e263 100644 --- a/src/renderer/fixtures/updateVehiclesFixtures.ts +++ b/src/renderer/fixtures/updateVehiclesFixtures.ts @@ -52,7 +52,7 @@ const fixtures: VehicleObject[] = []; /** * Sends an updateVehicle notification with random vehicle fixtures. */ -export default function updateVehicles(): void { +function updateVehicles(): void { for (let i = 0; i < vehicles.length; i += 1) { vehicles[i].getUpdateEventHandler().events({ lat: vehicles[i].getLat() + (Math.random() / 5000) - 0.0001, @@ -65,3 +65,5 @@ export default function updateVehicles(): void { ipc.postUpdateVehicles(...fixtures); } + +setInterval((): void => { updateVehicles(); }, 1000); diff --git a/src/renderer/fixtures/updateWaypointsFixtures.ts b/src/renderer/fixtures/updateWaypointsFixtures.ts new file mode 100644 index 00000000..eb629bed --- /dev/null +++ b/src/renderer/fixtures/updateWaypointsFixtures.ts @@ -0,0 +1,10 @@ +import { locationConfig } from '../../static/index'; + +import ipc from '../../util/ipc'; + +const waypoint = { + name: 'Test Marker', + location: locationConfig.startLocation, +}; + +ipc.postUpdateWaypoint(true, waypoint); diff --git a/src/renderer/mainWindow/map/MapContainer.tsx b/src/renderer/mainWindow/map/MapContainer.tsx index 9050d8fa..3699e646 100644 --- a/src/renderer/mainWindow/map/MapContainer.tsx +++ b/src/renderer/mainWindow/map/MapContainer.tsx @@ -44,23 +44,7 @@ const mapOptions = { */ interface State { /** - * Latitude location of map. - */ - lat: number; - - /** - * Longitude location of map. - */ - lng: number; - - /** - * Zoom of map. - */ - zoom?: number; - - /** - * Viewport storing locations on location of map. This is more precise than - * the (lat, lng, zoom) coordinates. + * Keeps track of map location and zoom. */ viewport: Viewport; @@ -120,12 +104,9 @@ export default class MapContainer extends Component { const { startLocation } = locationConfig; this.state = { - lat: startLocation.lat, - lng: startLocation.lng, - zoom: startLocation.zoom, viewport: { center: [startLocation.lat, startLocation.lng], - zoom: startLocation.zoom, + zoom: startLocation.zoom || 18, }, vehicles: {}, boundingBoxes: {}, @@ -134,13 +115,12 @@ export default class MapContainer extends Component { this.ref = createRef(); - this.onViewportChanged = this.onViewportChanged.bind(this); this.onlocationfound = this.onlocationfound.bind(this); + this.onViewportChanged = this.onViewportChanged.bind(this); this.setMapToUserLocation = this.setMapToUserLocation.bind(this); this.loadConfig = this.loadConfig.bind(this); this.saveConfig = this.saveConfig.bind(this); this.centerMapToVehicle = this.centerMapToVehicle.bind(this); - this.updateMapLocation = this.updateMapLocation.bind(this); this.updateWaypoints = this.updateWaypoints.bind(this); this.updateBoundingBoxes = this.updateBoundingBoxes.bind(this); } @@ -153,22 +133,22 @@ export default class MapContainer extends Component { ipcRenderer.on('setMapToUserLocation', this.setMapToUserLocation); ipcRenderer.on('updateMapLocation', (_: Event, location: Location): void => this.updateMapLocation(location)); ipcRenderer.on('updateVehicles', (_: Event, ...vehicles: VehicleObject[]): void => updateVehicles(this, ...vehicles)); - ipcRenderer.on('updateWaypoints', (_: Event, ...waypoints: { name: string; location: Location }[]): void => this.updateWaypoints(...waypoints)); + ipcRenderer.on('updateWaypoints', (_: Event, updateMap?: boolean, ...waypoints: { name: string; location: Location }[]): void => this.updateWaypoints(updateMap, ...waypoints)); ipcRenderer.on('updateBoundingBoxes', (_: Event, ...boundingBoxes: { name: string; bounds: BoundingBoxBounds}[]): void => this.updateBoundingBoxes(...boundingBoxes)); } /** - * Callback to whenever map viewport is changed. + * Callback to whenever geolocation call is successful. */ - private onViewportChanged(viewport: Viewport): void { - this.setState({ viewport }); + private onlocationfound(location: Leaflet.LocationEvent): void { + this.updateMapLocation(location.latlng); } /** - * Callback to whenever geolocation call is successful. + * Callback to whenever the viewport has changed. */ - private onlocationfound(location: Leaflet.LocationEvent): void { - this.updateMapLocation(location.latlng); + private onViewportChanged(viewport: Viewport): void { + this.setState({ viewport }); } /** @@ -181,6 +161,16 @@ export default class MapContainer extends Component { } } + /** + * Updates map location. + */ + private updateMapLocation(location: Location): void { + const { viewport } = this.state; + const { lat, lng, zoom } = location; + + this.onViewportChanged({ center: [lat, lng], zoom: zoom || viewport.zoom }); + } + /** * Reference to map (to access its leaflet element). */ @@ -219,14 +209,17 @@ export default class MapContainer extends Component { /** * Updates waypoints. */ - private updateWaypoints(...waypoints: { name: string; location: Location }[]): void { + private updateWaypoints( + updateMap?: boolean, + ...waypoints: { name: string; location: Location }[] + ): void { const { waypoints: thisWaypoints } = this.state; const currentWaypoints = thisWaypoints; waypoints.forEach((waypoint): void => { if (!currentWaypoints[waypoint.name]) { currentWaypoints[waypoint.name] = { location: waypoint.location, locked: false }; - } else { + } else if (updateMap) { currentWaypoints[waypoint.name].location = waypoint.location; } }); @@ -257,35 +250,12 @@ export default class MapContainer extends Component { this.setState({ boundingBoxes: currentBoundingBoxes }); } - /** - * Callback to whenever the map location has been changed. - */ - private updateMapLocation(location: Location): void { - const { lat, lng, zoom } = location; - - const { viewport } = this.state; - - // Do not allow null for viewport zoom. - const viewportZoom = viewport.zoom === null ? undefined : viewport.zoom; - - this.setState({ - lat, - lng, - zoom: zoom || viewportZoom, - }); - - this.onViewportChanged({ - center: [lat, lng], - zoom: zoom || viewport.zoom, - }); - } - public render(): ReactNode { const { theme } = this.props; const { + viewport, boundingBoxes, vehicles, - viewport, waypoints, } = this.state; @@ -321,8 +291,7 @@ export default class MapContainer extends Component { return ( { public constructor(props: WaypointMarkerProps) { super(props); - this.postUpdateWaypoint = this.postUpdateWaypoint.bind(this); + this.ondrag = this.ondrag.bind(this); } - private postUpdateWaypoint(event: Leaflet.LeafletEvent): void { + private ondrag(event: Leaflet.LeafletMouseEvent): void { const { name } = this.props; - const location: Location = event.target.latlng; - ipc.postUpdateWaypoint({ name, location }); + ipc.postUpdateWaypoint(false, { name, location: event.latlng }); } public render(): ReactNode { @@ -36,7 +35,7 @@ export default class WaypointMarker extends PureComponent { return ( Date: Fri, 19 Apr 2019 00:44:24 -0700 Subject: [PATCH 102/152] Modified fixtures Updated logic to Orchestrator. UpdateVehiclesFixtures now sends notifications to Orchestrator instead of the container. Fixed bug with above where Orchestrator does not initialize the vehicle to "ready" status, and denies all messages since it is in a "disconnected" status. TODO: Add logic to send stop message if vehicle is in a mission and sends status = "running" or status = "paused". Moved acknowledging to MessageHandler. No need for it to be in Orchestrator, was thinking that MessageHandler needed vehicle access to send acknowledgement, but that's not needed. --- src/common/MessageHandler.ts | 23 +++-- src/common/Orchestrator.ts | 24 +++-- src/common/struct/Vehicle.ts | 8 +- src/renderer/fixtures/index.ts | 3 +- .../fixtures/updateBoundingBoxesFixtures.ts | 18 ++++ .../fixtures/updateVehiclesFixtures.ts | 90 +++++++++++++------ src/renderer/mainWindow/map/MapContainer.tsx | 4 +- .../mainWindow/map/marker/VehicleMarker.tsx | 5 +- .../mainWindow/map/marker/WaypointMarker.tsx | 5 +- .../mainWindow/vehicle/VehicleTable.tsx | 4 +- src/static/index.ts | 4 +- src/util/ipc.ts | 13 +-- 12 files changed, 132 insertions(+), 69 deletions(-) create mode 100644 src/renderer/fixtures/updateBoundingBoxesFixtures.ts diff --git a/src/common/MessageHandler.ts b/src/common/MessageHandler.ts index 98477508..7c5867cb 100644 --- a/src/common/MessageHandler.ts +++ b/src/common/MessageHandler.ts @@ -103,6 +103,17 @@ class MessageHandler { }); } + /** + * Acknowledges a message. All messages are passed here through the MessageHandler. + * Only messages that are no acknowledged are bad messages and acknowledgements. + */ + private sendAcknowledgeMessage(jsonMessage: Message.JSONMessage): void { + this.sendMessage(jsonMessage.sid, { + type: 'ack', + ackid: jsonMessage.id, + }); + } + /** * Send a bad message. * @@ -152,16 +163,18 @@ class MessageHandler { || this.receivedMessageId[jsonMessage.sid] as number < jsonMessage.id; if (Message.TypeGuard.isConnectMessage(jsonMessage)) { - ipc.postAcknowledgeMessage(jsonMessage); - if (newMessage) ipc.postConnectToVehicle(jsonMessage); + if (newMessage) { + this.sendMessage(jsonMessage.sid, { type: 'connectionAck' }); + ipc.postConnectToVehicle(jsonMessage); + } } else if (Message.TypeGuard.isCompleteMessage(jsonMessage)) { - ipc.postAcknowledgeMessage(jsonMessage); + this.sendAcknowledgeMessage(jsonMessage); if (newMessage) ipc.postHandleCompleteMessage(jsonMessage); } else if (Message.TypeGuard.isPOIMessage(jsonMessage)) { - ipc.postAcknowledgeMessage(jsonMessage); + this.sendAcknowledgeMessage(jsonMessage); if (newMessage) ipc.postHandlePOIMessage(jsonMessage); } else if (Message.TypeGuard.isUpdateMessage(jsonMessage)) { - ipc.postAcknowledgeMessage(jsonMessage); + this.sendAcknowledgeMessage(jsonMessage); if (newMessage) ipc.postHandleUpdateMessage(jsonMessage); } else if (Message.TypeGuard.isBadMessage(jsonMessage)) { if (newMessage) ipc.postHandleBadMessage(jsonMessage); diff --git a/src/common/Orchestrator.ts b/src/common/Orchestrator.ts index f334b7ca..63098d36 100644 --- a/src/common/Orchestrator.ts +++ b/src/common/Orchestrator.ts @@ -17,17 +17,6 @@ import Vehicle from './struct/Vehicle'; import './MessageHandler'; class Orchestrator { - /** - * Acknowledges a message. All messages are passed here through the MessageHandler. - * Only messages that are no acknowledged are bad messages and acknowledgements. - */ - private static acknowledgeMessage(jsonMessage: Message.JSONMessage): void { - ipc.postSendMessage(jsonMessage.sid, { - type: 'ack', - ackid: jsonMessage.id, - }); - } - /** * Posts error message when error happens in Orchestrator. */ @@ -76,8 +65,6 @@ class Orchestrator { ipcRenderer.on('connectToVehicle', (_: Event, jsonMessage: Message.JSONMessage): void => this.connectToVehicle(jsonMessage)); ipcRenderer.on('disconnectFromVehicle', (_: Event, vehicleId: number): void => this.disconnectFromVehicle(vehicleId)); - ipcRenderer.on('acknowledgeMessage', (_: Event, jsonMessage: Message.JSONMessage): void => Orchestrator.acknowledgeMessage(jsonMessage)); - ipcRenderer.on('handleAcknowledgementMessage', (_: Event, jsonMessage: Message.JSONMessage): void => this.handleAcknowledgementMessage(jsonMessage)); ipcRenderer.on('handleBadMessage', (_: Event, jsonMessage: Message.JSONMessage): void => this.handleBadMessage(jsonMessage)); ipcRenderer.on('handleUpdateMessage', (_: Event, jsonMessage: Message.JSONMessage): void => this.handleUpdateMessage(jsonMessage)); @@ -106,6 +93,12 @@ class Orchestrator { this.vehicles[jsonMessage.sid] = new Vehicle({ sid: jsonMessage.sid, jobs: connectMessage.jobsAvailable, + status: 'ready', + }); + + ipc.postLogMessages({ + type: 'success', + message: `${(vehicleConfig.vehicleInfos[jsonMessage.sid] as VehicleInfo).name} has connected`, }); } @@ -139,6 +132,11 @@ class Orchestrator { this.vehicles[vehicleId].disconnect(); ipc.postUpdateVehicles(this.vehicles[vehicleId].toObject()); + + ipc.postLogMessages({ + type: 'failure', + message: `${(vehicleConfig.vehicleInfos[vehicleId] as VehicleInfo).name} has disconnected`, + }); } /** diff --git a/src/common/struct/Vehicle.ts b/src/common/struct/Vehicle.ts index 0fdd740b..e682ca12 100644 --- a/src/common/struct/Vehicle.ts +++ b/src/common/struct/Vehicle.ts @@ -28,6 +28,11 @@ export interface VehicleOptions { * Jobs vehicle can handle. */ jobs: JobType[]; + + /** + * Status of vehicle. + */ + status: VehicleStatus; } /** @@ -51,7 +56,7 @@ export default class Vehicle { /** * Current status of the vehicle. */ - private status: VehicleStatus = 'disconnected'; + private status: VehicleStatus; /** * Jobs the vehicle has. These define the tasks the vehicle is capable of performing. @@ -102,6 +107,7 @@ export default class Vehicle { public constructor(options: VehicleOptions) { this.vehicleId = options.sid; this.jobs = options.jobs; + this.status = options.status; this.updateEventHandler.addHandler('status', (status, message): boolean => { this.status = status; diff --git a/src/renderer/fixtures/index.ts b/src/renderer/fixtures/index.ts index fd8a21e2..d80f60b1 100644 --- a/src/renderer/fixtures/index.ts +++ b/src/renderer/fixtures/index.ts @@ -3,5 +3,6 @@ /* eslint-disable multiline-comment-style */ // import './logMessagesFixtures'; -// import './updateVehiclesFixtures'; +import './updateBoundingBoxesFixtures'; +import './updateVehiclesFixtures'; import './updateWaypointsFixtures'; diff --git a/src/renderer/fixtures/updateBoundingBoxesFixtures.ts b/src/renderer/fixtures/updateBoundingBoxesFixtures.ts new file mode 100644 index 00000000..4ece7abb --- /dev/null +++ b/src/renderer/fixtures/updateBoundingBoxesFixtures.ts @@ -0,0 +1,18 @@ +import { locationConfig } from '../../static/index'; + +import ipc from '../../util/ipc'; + +const { lat, lng } = locationConfig.startLocation; + +const boundingBox = { + name: 'Test Bounding Box', + color: 'red', + bounds: { + top: lat, + bottom: lat - 0.001, + left: lng - 0.001, + right: lng, + }, +}; + +ipc.postUpdateBoundingBoxes(boundingBox); diff --git a/src/renderer/fixtures/updateVehiclesFixtures.ts b/src/renderer/fixtures/updateVehiclesFixtures.ts index b4a5e263..804e6935 100644 --- a/src/renderer/fixtures/updateVehiclesFixtures.ts +++ b/src/renderer/fixtures/updateVehiclesFixtures.ts @@ -1,12 +1,26 @@ -import Vehicle, { VehicleOptions } from '../../common/struct/Vehicle'; +/* + * This fixture bypasses the MessageHandler, and interacts directly with the Orchestrator + * to add vehicles and post update messages about them. + * + * In a real field test, all messages are sent through the MessageHandler (as it is received + * on an Xbee). + */ -import { locationConfig, vehicleConfig } from '../../static/index'; +import { JobType, locationConfig } from '../../static/index'; -import { VehicleObject, VehicleStatus } from '../../types/vehicle'; +import * as Message from '../../types/message'; +import { VehicleStatus } from '../../types/vehicle'; import ipc from '../../util/ipc'; -const fixtureOptions: VehicleOptions[] = [ +interface Fixture { + sid: number; + jobs: JobType[]; + lat: number; + lng: number; +} + +const fixtureOptions: { sid: number; jobs: JobType[] }[] = [ { sid: 100, jobs: ['isrSearch', 'payloadDrop'], @@ -37,33 +51,57 @@ const fixtureOptions: VehicleOptions[] = [ }, ]; -const status = Object.keys(vehicleConfig.vehicleStatuses); +let fixtures: Fixture[] = fixtureOptions.map( + (fixtureOption): Fixture => ({ ...fixtureOption, ...locationConfig.startLocation }), +); -const vehicles = fixtureOptions.map((options): Vehicle => new Vehicle(options)); -vehicles.forEach((vehicle): void => { - vehicle.getUpdateEventHandler().events({ - lat: locationConfig.startLocation.lat + (Math.random() / 1000) - 0.0005, - lng: locationConfig.startLocation.lng + (Math.random() / 1000) - 0.0005, - }); -}); +const status: VehicleStatus[] = ['ready', 'waiting', 'paused', 'running', 'error']; -const fixtures: VehicleObject[] = []; +let messageId = 0; -/** - * Sends an updateVehicle notification with random vehicle fixtures. - */ -function updateVehicles(): void { - for (let i = 0; i < vehicles.length; i += 1) { - vehicles[i].getUpdateEventHandler().events({ - lat: vehicles[i].getLat() + (Math.random() / 5000) - 0.0001, - lng: vehicles[i].getLng() + (Math.random() / 5000) - 0.0001, - status: status[Math.floor(Math.random() * status.length)] as VehicleStatus, - }); +function generateJSONMessage(vehicleId: number, message: Message.Message): Message.JSONMessage { + const jsonMessage = { + id: messageId, + sid: vehicleId, + tid: 0, + time: Date.now(), + ...message, + }; - fixtures[i] = vehicles[i].toObject(); - } + messageId += 1; + return jsonMessage; +} - ipc.postUpdateVehicles(...fixtures); +function randomCoordinate(location: { lat: number; lng: number }): { lat: number; lng: number } { + return { + lat: location.lat + (Math.random() / 5000) - 0.0001, + lng: location.lng + (Math.random() / 5000) - 0.0001, + }; +} + +function connectVehicles(): void { + fixtures.forEach((fixture): void => { + ipc.postConnectToVehicle(generateJSONMessage(fixture.sid, { + type: 'connect', + jobsAvailable: fixture.jobs, + })); + }); +} + +function updateVehicles(): void { + fixtures = fixtures.map( + (fixture): Fixture => ({ ...fixture, ...randomCoordinate(fixture) }), + ); + + fixtures.forEach((fixture): void => { + ipc.postHandleUpdateMessage(generateJSONMessage(fixture.sid, { + type: 'update', + lat: fixture.lat, + lng: fixture.lng, + status: status[Math.floor(Math.random() * status.length)], + })); + }); } +connectVehicles(); setInterval((): void => { updateVehicles(); }, 1000); diff --git a/src/renderer/mainWindow/map/MapContainer.tsx b/src/renderer/mainWindow/map/MapContainer.tsx index 3699e646..ceba9d03 100644 --- a/src/renderer/mainWindow/map/MapContainer.tsx +++ b/src/renderer/mainWindow/map/MapContainer.tsx @@ -231,7 +231,7 @@ export default class MapContainer extends Component { * Updates bounding boxes. */ private updateBoundingBoxes( - ...boundingBoxes: { name: string; bounds: BoundingBoxBounds }[] + ...boundingBoxes: { name: string; color?: string; bounds: BoundingBoxBounds }[] ): void { const { boundingBoxes: thisBoundingBoxes } = this.state; const currentBoundingBoxes = thisBoundingBoxes; @@ -239,6 +239,7 @@ export default class MapContainer extends Component { boundingBoxes.forEach((boundingBox): void => { if (!currentBoundingBoxes[boundingBox.name]) { currentBoundingBoxes[boundingBox.name] = { + color: boundingBox.color || '#000', bounds: boundingBox.bounds, locked: false, }; @@ -264,6 +265,7 @@ export default class MapContainer extends Component { { const { type: vehicleStatusTypeString, message, - } = vehicleConfig.vehicleStatuses[vehicle.status] as VehicleStatus; + } = vehicleConfig.vehicleStatuses[vehicle.status] as VehicleStatusStyle; const vehicleStatusType = vehicleStatusTypeString as MessageType; const vehicleType = `${type}${vehicleStatusType === 'failure' ? '_red' : ''}`; @@ -42,7 +42,6 @@ export default class VehicleMarker extends PureComponent { iconUrl: (vehicleIcons[vehicleType] || imageConfig.pin) as string, iconSize: [50, 50], iconAnchor: [25, 25], - popupAnchor: [0, -25], })} > { icon={Leaflet.icon({ iconUrl: imageConfig.pin as string, iconSize: [50, 50], - iconAnchor: [25, 25], - popupAnchor: [0, -25], + iconAnchor: [25, 50], })} >

{name}

diff --git a/src/renderer/mainWindow/vehicle/VehicleTable.tsx b/src/renderer/mainWindow/vehicle/VehicleTable.tsx index 2342db32..36e33966 100644 --- a/src/renderer/mainWindow/vehicle/VehicleTable.tsx +++ b/src/renderer/mainWindow/vehicle/VehicleTable.tsx @@ -11,7 +11,7 @@ import { import { vehicleConfig, VehicleInfo, - VehicleStatus, + VehicleStatusStyle, } from '../../../static/index'; import { ThemeProps } from '../../../types/componentStyle'; @@ -60,7 +60,7 @@ export default class VehicleTable extends Component { */ private static statusRenderer(props: TableCellProps): ReactNode { const { rowData }: { rowData: VehicleObject } = props; - const vehicleStatus = vehicleConfig.vehicleStatuses[rowData.status] as VehicleStatus; + const vehicleStatus = vehicleConfig.vehicleStatuses[rowData.status] as VehicleStatusStyle; return {vehicleStatus.message}; } diff --git a/src/static/index.ts b/src/static/index.ts index 7e4403cf..c888a796 100644 --- a/src/static/index.ts +++ b/src/static/index.ts @@ -78,13 +78,13 @@ const vehicleJobs: { /** * Object contained in vehicleStatuses in vehicle.json. */ -export interface VehicleStatus { +export interface VehicleStatusStyle { 'type': string; message: string; } const vehicleStatuses: { - [status: string]: VehicleStatus | undefined; + [status: string]: VehicleStatusStyle | undefined; } = vehicleStatusesObject; // Add logic to set startLocation. diff --git a/src/util/ipc.ts b/src/util/ipc.ts index c64d65b4..47e0d8cc 100644 --- a/src/util/ipc.ts +++ b/src/util/ipc.ts @@ -25,16 +25,6 @@ import { Information } from '../types/missionInformation'; import * as Task from '../types/task'; import { VehicleObject } from '../types/vehicle'; -/** - * Post "acknowledgeMessage" notification. - * - * Files that take this notification: - * - common/Orchestrator - */ -function postAcknowledgeMessage(jsonMessage: Message.JSONMessage): void { - ipcRenderer.send('post', 'acknowledgeMessage', jsonMessage); -} - /** * Post "centerMapToVehicle" notification. * @@ -313,7 +303,7 @@ function postToggleTheme(): void { * - renderer/mainWindow/map/MapContainer */ function postUpdateBoundingBoxes( - ...boundingBoxes: { name: string; bounds: BoundingBoxBounds }[] + ...boundingBoxes: { name: string; color?: string; bounds: BoundingBoxBounds }[] ): void { ipcRenderer.send('post', 'updateBoundingBoxes', ...boundingBoxes); } @@ -364,7 +354,6 @@ function postUpdateWaypoint( } export default { - postAcknowledgeMessage, postCenterMapToVehicle, postCompleteMission, postConfirmCompleteMission, From 9cc6e974dcc4a427a4f5e35950a592ce8475aef0 Mon Sep 17 00:00:00 2001 From: Paul Matthew Barrameda Date: Fri, 19 Apr 2019 01:01:06 -0700 Subject: [PATCH 103/152] Fixed Orchestrator vehicle disconnection --- src/common/Orchestrator.ts | 5 ++++- src/renderer/fixtures/updateVehiclesFixtures.ts | 2 +- src/renderer/index.ts | 6 +++--- src/static/index.ts | 8 ++++---- 4 files changed, 12 insertions(+), 9 deletions(-) diff --git a/src/common/Orchestrator.ts b/src/common/Orchestrator.ts index 63098d36..ae937482 100644 --- a/src/common/Orchestrator.ts +++ b/src/common/Orchestrator.ts @@ -100,6 +100,8 @@ class Orchestrator { type: 'success', message: `${(vehicleConfig.vehicleInfos[jsonMessage.sid] as VehicleInfo).name} has connected`, }); + + this.ping(this.vehicles[jsonMessage.sid]); } /** @@ -109,7 +111,8 @@ class Orchestrator { */ private ping(vehicle: Vehicle): void { const delta = Date.now() - vehicle.getLastConnectionTime(); - if (delta >= 0 && delta <= config.vehicleDisconnectionTime) { + + if (delta >= 0 && delta <= config.vehicleDisconnectionTime * 1000) { // Handler that expires and creates itself everytime it "pings" the vehicle. this.vehiclePinger.addHandler(`${vehicle.getVehicleId()}`, (): boolean => false, { callback: (): void => this.ping(vehicle), diff --git a/src/renderer/fixtures/updateVehiclesFixtures.ts b/src/renderer/fixtures/updateVehiclesFixtures.ts index 804e6935..3c103cd3 100644 --- a/src/renderer/fixtures/updateVehiclesFixtures.ts +++ b/src/renderer/fixtures/updateVehiclesFixtures.ts @@ -104,4 +104,4 @@ function updateVehicles(): void { } connectVehicles(); -setInterval((): void => { updateVehicles(); }, 1000); +setInterval((): void => { updateVehicles(); }, 1000); // Call updateVehicles once for disconnection. diff --git a/src/renderer/index.ts b/src/renderer/index.ts index 80dd644b..6a605150 100644 --- a/src/renderer/index.ts +++ b/src/renderer/index.ts @@ -3,7 +3,7 @@ import React from 'react'; import ReactDOM from 'react-dom'; -import statics from '../static/index'; +import { config } from '../static/index'; import ipc from '../util/ipc'; @@ -23,12 +23,12 @@ function runOnce(): void { require('../common/Orchestrator'); // Set up geolocation if geolocation is enabled in config. - if (statics.config.geolocation) { + if (config.geolocation) { ipc.postSetMapToUserLocation(); } // Sets up fixtures if in development and fixtures are enabled in config. - if (isDevelopment && statics.config.fixtures) { + if (isDevelopment && config.fixtures) { require('./fixtures/index'); } } diff --git a/src/static/index.ts b/src/static/index.ts index c888a796..5d75fea9 100644 --- a/src/static/index.ts +++ b/src/static/index.ts @@ -23,7 +23,7 @@ import arrow from './images/arrow.png'; import icon from './images/icon.png'; import pin from './images/pin.png'; -import config from './config.json'; +import configStatic from './config.json'; import { startLocation as startLocationString, locations as locationsObject } from './location.json'; import { vehicleIds as vehicleIdsObject, @@ -57,7 +57,7 @@ const locations: { [name: string]: Location | undefined; } = locationsObject; -export const vehicleIds: { +const vehicleIds: { [name: string]: number | undefined; } = vehicleIdsObject; @@ -121,6 +121,8 @@ function isValidTaskTypeForJob(taskType: string, jobType: JobType): boolean { return (vehicleJobs[jobType] as string[]).includes(taskType); } +export const config = configStatic; + export const locationConfig = { locations, startLocation, @@ -136,8 +138,6 @@ export const vehicleConfig = { vehicleJobs, }; -export { default as config } from './config.json'; - /** * A given key for an image will give either a string or an object with similar structure. */ From 579e86e9606f8b05a1a0e5e8b4011602f4284fed Mon Sep 17 00:00:00 2001 From: Paul Matthew Barrameda Date: Fri, 19 Apr 2019 10:32:32 -0700 Subject: [PATCH 104/152] Small change to missions --- src/common/missions/PayloadDrop.ts | 3 ++- src/common/missions/VTOLSearch.ts | 4 ++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/common/missions/PayloadDrop.ts b/src/common/missions/PayloadDrop.ts index 33ecca21..b62ef1d6 100644 --- a/src/common/missions/PayloadDrop.ts +++ b/src/common/missions/PayloadDrop.ts @@ -87,10 +87,11 @@ class PayloadDrop extends Mission { } return { - payloadDrop: { + retrieveTarget: { lat: this.missionData.lat, lng: this.missionData.lng, }, + // TODO: generate data for deliverTarget }; } } diff --git a/src/common/missions/VTOLSearch.ts b/src/common/missions/VTOLSearch.ts index ab6cd62b..8a633a5b 100644 --- a/src/common/missions/VTOLSearch.ts +++ b/src/common/missions/VTOLSearch.ts @@ -109,6 +109,10 @@ class VTOLSearch extends Mission { lat: this.missionData.lat, lng: this.missionData.lng, }, + /* + * TODO: ensure this data passes on correctly to payload drop, cannnot do automatically + * w/o calculations? Will require mission to be run separately for now. + */ }; } } From 323f2b02f1fd1199abf1ad678c9c4531c48b22c3 Mon Sep 17 00:00:00 2001 From: Paul Matthew Barrameda Date: Fri, 19 Apr 2019 13:25:08 -0700 Subject: [PATCH 105/152] Updated for testing with UGV Temporary stuff --- src/common/MessageHandler.ts | 2 +- src/common/Xbee.ts | 3 ++- src/static/config.json | 2 +- src/static/vehicle.json | 2 +- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/common/MessageHandler.ts b/src/common/MessageHandler.ts index 7c5867cb..15d5ea94 100644 --- a/src/common/MessageHandler.ts +++ b/src/common/MessageHandler.ts @@ -136,7 +136,7 @@ class MessageHandler { // Filter out all invalid messages. if (!isJSON(text)) { ipc.postLogMessages({ - message: `Received text from Xbee that is not a JSON, could not send bad message to sender ${text}`, + message: `Received text from Xbee that is not a JSON, could not send bad message to sender: ${text}`, }); return; } diff --git a/src/common/Xbee.ts b/src/common/Xbee.ts index 5736d1e0..d3ee1a7a 100644 --- a/src/common/Xbee.ts +++ b/src/common/Xbee.ts @@ -9,7 +9,7 @@ import { JSONMessage } from '../types/message'; import ipc from '../util/ipc'; // TODO: Add a feature with Vehicle container to change this dynamically and reconnect. -const port: string | undefined = 'COM5'; +const port: string | undefined = '/dev/tty.SLAB_USBtoUART'; const serialport = new SerialPort(port, { baudRate: 57600 }, (error): void => { if (error) { @@ -90,6 +90,7 @@ serialport.on('close', (): void => { xbeeAPI.parser.on('data', (frame: Frame): void => { if (frame.type !== C.FRAME_TYPE.ZIGBEE_RECEIVE_PACKET || !frame.data) return; + console.log(msgpack.decode(frame.data)); ipc.postReceiveMessage(msgpack.decode(frame.data)); }); diff --git a/src/static/config.json b/src/static/config.json index 51f9f420..41da663b 100644 --- a/src/static/config.json +++ b/src/static/config.json @@ -1,5 +1,5 @@ { - "fixtures": true, + "fixtures": false, "geolocation": true, "vehicleDisconnectionTime": 20, diff --git a/src/static/vehicle.json b/src/static/vehicle.json index 6988bca4..3b660ea4 100644 --- a/src/static/vehicle.json +++ b/src/static/vehicle.json @@ -20,7 +20,7 @@ "type": "plane" }, "200": { - "macAddress": "", + "macAddress": "0013A20041947790", "name": "UGV", "type": "rover" }, From c006d4bcffef30f685fddb7b4c53f7932e504de7 Mon Sep 17 00:00:00 2001 From: Paul Matthew Barrameda Date: Fri, 19 Apr 2019 21:00:52 -0700 Subject: [PATCH 106/152] (Re)added logic to the Orchestrator to only acknowledge if the message is valid (vehicle is connected, if mission related, vehicle is in mission, etc) --- src/common/MessageHandler.ts | 45 ++++++---------- src/common/Orchestrator.ts | 100 ++++++++++++++++++++++------------- src/common/struct/Vehicle.ts | 26 ++++----- src/util/ipc.ts | 34 ++++++++---- 4 files changed, 112 insertions(+), 93 deletions(-) diff --git a/src/common/MessageHandler.ts b/src/common/MessageHandler.ts index 15d5ea94..8356e2b5 100644 --- a/src/common/MessageHandler.ts +++ b/src/common/MessageHandler.ts @@ -42,6 +42,8 @@ class MessageHandler { public constructor() { ipcRenderer.on('sendMessage', (_: Event, vehicleId: number, message: Message.Message): void => this.sendMessage(vehicleId, message)); ipcRenderer.on('receiveMessage', (_: Event, text: string): void => this.receiveMessage(text)); + + ipcRenderer.on('stopSendingMessage', (_: Event, ackMessage: Message.JSONMessage): void => this.stopSendingMessage(ackMessage)); ipcRenderer.on('stopSendingMessages', this.stopSendingMessages); } @@ -103,17 +105,6 @@ class MessageHandler { }); } - /** - * Acknowledges a message. All messages are passed here through the MessageHandler. - * Only messages that are no acknowledged are bad messages and acknowledgements. - */ - private sendAcknowledgeMessage(jsonMessage: Message.JSONMessage): void { - this.sendMessage(jsonMessage.sid, { - type: 'ack', - ackid: jsonMessage.id, - }); - } - /** * Send a bad message. * @@ -163,29 +154,17 @@ class MessageHandler { || this.receivedMessageId[jsonMessage.sid] as number < jsonMessage.id; if (Message.TypeGuard.isConnectMessage(jsonMessage)) { - if (newMessage) { - this.sendMessage(jsonMessage.sid, { type: 'connectionAck' }); - ipc.postConnectToVehicle(jsonMessage); - } + ipc.postConnectToVehicle(jsonMessage); } else if (Message.TypeGuard.isCompleteMessage(jsonMessage)) { - this.sendAcknowledgeMessage(jsonMessage); - if (newMessage) ipc.postHandleCompleteMessage(jsonMessage); + ipc.postHandleCompleteMessage(jsonMessage, newMessage); } else if (Message.TypeGuard.isPOIMessage(jsonMessage)) { - this.sendAcknowledgeMessage(jsonMessage); - if (newMessage) ipc.postHandlePOIMessage(jsonMessage); + ipc.postHandlePOIMessage(jsonMessage, newMessage); } else if (Message.TypeGuard.isUpdateMessage(jsonMessage)) { - this.sendAcknowledgeMessage(jsonMessage); - if (newMessage) ipc.postHandleUpdateMessage(jsonMessage); + ipc.postHandleUpdateMessage(jsonMessage, newMessage); } else if (Message.TypeGuard.isBadMessage(jsonMessage)) { - if (newMessage) ipc.postHandleBadMessage(jsonMessage); + if (newMessage) ipc.postHandleBadMessage(jsonMessage, newMessage); } else if (Message.TypeGuard.isAcknowledgementMessage(jsonMessage)) { - const { ackid } = jsonMessage as Message.AcknowledgementMessage; - - if (newMessage) { - const hash = `${jsonMessage.sid}#${ackid}`; - this.updateHandler.event(hash, true); // Stop sending message that this message acknowleges. - ipc.postHandleAcknowledgementMessage(jsonMessage); - } + ipc.postHandleAcknowledgementMessage(jsonMessage, newMessage); } else { this.sendBadMessage(jsonMessage, `Message of type ${jsonMessage.type} is invalid or is not acceptable by GCS`); } @@ -197,6 +176,14 @@ class MessageHandler { } } + /** + * Stops sending certain message, specified by ackid and tid of message. + */ + private stopSendingMessage(ackMessage: Message.JSONMessage): void { + const hash = `${ackMessage.sid}#${(ackMessage as Message.AcknowledgementMessage).ackid}`; + this.updateHandler.event(hash, true); + } + /** * Stops sending all messages. */ diff --git a/src/common/Orchestrator.ts b/src/common/Orchestrator.ts index ae937482..fea517c1 100644 --- a/src/common/Orchestrator.ts +++ b/src/common/Orchestrator.ts @@ -27,6 +27,17 @@ class Orchestrator { }); } + /** + * Acknowledges a message. All messages are passed here through the MessageHandler. + * Only messages that are no acknowledged are bad messages and acknowledgements. + */ + private static acknowledgeMessage(jsonMessage: Message.JSONMessage): void { + ipc.postSendMessage(jsonMessage.sid, { + type: 'ack', + ackid: jsonMessage.id, + }); + } + /** * List of mission names and information for each mission. */ @@ -65,11 +76,11 @@ class Orchestrator { ipcRenderer.on('connectToVehicle', (_: Event, jsonMessage: Message.JSONMessage): void => this.connectToVehicle(jsonMessage)); ipcRenderer.on('disconnectFromVehicle', (_: Event, vehicleId: number): void => this.disconnectFromVehicle(vehicleId)); - ipcRenderer.on('handleAcknowledgementMessage', (_: Event, jsonMessage: Message.JSONMessage): void => this.handleAcknowledgementMessage(jsonMessage)); - ipcRenderer.on('handleBadMessage', (_: Event, jsonMessage: Message.JSONMessage): void => this.handleBadMessage(jsonMessage)); - ipcRenderer.on('handleUpdateMessage', (_: Event, jsonMessage: Message.JSONMessage): void => this.handleUpdateMessage(jsonMessage)); - ipcRenderer.on('handlePOIMessage', (_: Event, jsonMessage: Message.JSONMessage): void => this.handlePOIMessage(jsonMessage)); - ipcRenderer.on('handleCompleteMessage', (_: Event, jsonMessage: Message.JSONMessage): void => this.handleCompleteMessage(jsonMessage)); + ipcRenderer.on('handleAcknowledgementMessage', (_: Event, jsonMessage: Message.JSONMessage, newMessage: boolean): void => this.handleAcknowledgementMessage(jsonMessage, newMessage)); + ipcRenderer.on('handleBadMessage', (_: Event, jsonMessage: Message.JSONMessage, newMessage: boolean): void => this.handleBadMessage(jsonMessage, newMessage)); + ipcRenderer.on('handleUpdateMessage', (_: Event, jsonMessage: Message.JSONMessage, newMessage: boolean): void => this.handleUpdateMessage(jsonMessage, newMessage)); + ipcRenderer.on('handlePOIMessage', (_: Event, jsonMessage: Message.JSONMessage, newMessage: boolean): void => this.handlePOIMessage(jsonMessage, newMessage)); + ipcRenderer.on('handleCompleteMessage', (_: Event, jsonMessage: Message.JSONMessage, newMessage: boolean): void => this.handleCompleteMessage(jsonMessage, newMessage)); ipcRenderer.on('startMissions', ( _: Event, @@ -88,11 +99,13 @@ class Orchestrator { * @param jsonMessage The connect message. */ private connectToVehicle(jsonMessage: Message.JSONMessage): void { - const connectMessage = jsonMessage as Message.ConnectMessage; + if (this.vehicles[jsonMessage.sid] && this.vehicles[jsonMessage.sid].getStatus() !== 'disconnected') return; + + ipc.postSendMessage(jsonMessage.sid, { type: 'connectionAck' }); this.vehicles[jsonMessage.sid] = new Vehicle({ sid: jsonMessage.sid, - jobs: connectMessage.jobsAvailable, + jobs: (jsonMessage as Message.ConnectMessage).jobsAvailable, status: 'ready', }); @@ -146,25 +159,27 @@ class Orchestrator { * Handles acknowledgement messages. * @param jsonMessage Message from vehicle. */ - private handleAcknowledgementMessage(jsonMessage: Message.JSONMessage): void { - if (!this.vehicles[jsonMessage.sid] || this.vehicles[jsonMessage.sid].getStatus() === 'disconnected') { - Orchestrator.postOrchestratorError(`Received acknowledgement message from disconnected vehicle id ${jsonMessage.sid}`); - return; - } - this.vehicles[jsonMessage.sid].updateLastConnectionTime(jsonMessage); + private handleAcknowledgementMessage( + jsonMessage: Message.JSONMessage, + newMessage: boolean, + ): void { + if (!this.vehicles[jsonMessage.sid] || this.vehicles[jsonMessage.sid].getStatus() === 'disconnected') return; + if (!newMessage) return; + + this.vehicles[jsonMessage.sid].update(jsonMessage); + + ipc.postStopSendingMessage(jsonMessage); } /** * Handles bad messages. * @param jsonMessage Message from vehicle. */ - private handleBadMessage(jsonMessage: Message.JSONMessage): void { - if (!this.vehicles[jsonMessage.sid] || this.vehicles[jsonMessage.sid].getStatus() === 'disconnected') { - Orchestrator.postOrchestratorError(`Received bad message from disconnected vehicle id ${jsonMessage.sid}`); - return; - } + private handleBadMessage(jsonMessage: Message.JSONMessage, newMessage: boolean): void { + if (!this.vehicles[jsonMessage.sid] || this.vehicles[jsonMessage.sid].getStatus() === 'disconnected') return; + if (!newMessage) return; - this.vehicles[jsonMessage.sid].updateLastConnectionTime(jsonMessage); + this.vehicles[jsonMessage.sid].update(jsonMessage); const badMessage = jsonMessage as Message.BadMessage; ipc.postLogMessages({ @@ -177,12 +192,13 @@ class Orchestrator { * Handles update messages. * @param jsonMessage Message from vehicle. */ - private handleUpdateMessage(jsonMessage: Message.JSONMessage): void { - if (!this.vehicles[jsonMessage.sid] || this.vehicles[jsonMessage.sid].getStatus() === 'disconnected') { - Orchestrator.postOrchestratorError(`Received update message from disconnected vehicle id ${jsonMessage.sid}`); - return; - } + private handleUpdateMessage(jsonMessage: Message.JSONMessage, newMessage: boolean): void { + if (!this.vehicles[jsonMessage.sid] || this.vehicles[jsonMessage.sid].getStatus() === 'disconnected') return; + if (!newMessage) return; + + Orchestrator.acknowledgeMessage(jsonMessage); + // This function updates the lastConnectionTime variable of the vehicle too. this.vehicles[jsonMessage.sid].update(jsonMessage); if (this.currentMission && this.currentMission.getVehicles()[jsonMessage.sid]) { this.currentMission.update(jsonMessage); @@ -195,17 +211,21 @@ class Orchestrator { * Handles point of interest messages. * @param jsonMessage Message from vehicle. */ - private handlePOIMessage(jsonMessage: Message.JSONMessage): void { - if (!this.vehicles[jsonMessage.sid] || this.vehicles[jsonMessage.sid].getStatus() === 'disconnected') { - Orchestrator.postOrchestratorError(`Received point of interest message from ${vehicleConfig.vehicleInfos[jsonMessage.sid]} but it is currently disconnected`); - return; - } + private handlePOIMessage(jsonMessage: Message.JSONMessage, newMessage: boolean): void { + if (!this.vehicles[jsonMessage.sid] || this.vehicles[jsonMessage.sid].getStatus() === 'disconnected') return; + if (!newMessage) return; if (this.currentMission && !this.currentMission.getVehicles()[jsonMessage.sid]) { - Orchestrator.postOrchestratorError(`Received point of interest message from ${vehicleConfig.vehicleInfos[jsonMessage.sid]} while it is not assigned to mission`); + ipc.postLogMessages({ + type: 'failure', + message: `Received point of interest message from ${vehicleConfig.vehicleInfos[jsonMessage.sid]} while it is not assigned to mission`, + }); return; } + Orchestrator.acknowledgeMessage(jsonMessage); + + this.vehicles[jsonMessage.sid].update(jsonMessage); if (this.currentMission) this.currentMission.update(jsonMessage); } @@ -213,22 +233,28 @@ class Orchestrator { * Handles complete messages. * @param jsonMessage Message from vehicle. */ - private handleCompleteMessage(jsonMessage: Message.JSONMessage): void { - if (!this.vehicles[jsonMessage.sid] || this.vehicles[jsonMessage.sid].getStatus() === 'disconnected') { - Orchestrator.postOrchestratorError(`Received complete message from disconnected vehicle id ${jsonMessage.sid}`); - return; - } + private handleCompleteMessage(jsonMessage: Message.JSONMessage, newMessage: boolean): void { + if (!this.vehicles[jsonMessage.sid] || this.vehicles[jsonMessage.sid].getStatus() === 'disconnected') return; + if (!newMessage) return; if (!this.currentMission) { - Orchestrator.postOrchestratorError(`Received complete message from ${vehicleConfig.vehicleInfos[jsonMessage.sid]} while no mission is running`); + ipc.postLogMessages({ + type: 'failure', + message: `Received complete message from ${vehicleConfig.vehicleInfos[jsonMessage.sid]} while no mission is running`, + }); return; } if (!this.currentMission.getVehicles()[jsonMessage.sid]) { - Orchestrator.postOrchestratorError(`Received complete message from ${vehicleConfig.vehicleInfos[jsonMessage.sid]} while it is not assigned to the mission`); + ipc.postLogMessages({ + type: 'failure', + message: `Received complete message from ${vehicleConfig.vehicleInfos[jsonMessage.sid]} while it is not assigned to the mission`, + }); return; } + Orchestrator.acknowledgeMessage(jsonMessage); + this.currentMission.update(jsonMessage); } diff --git a/src/common/struct/Vehicle.ts b/src/common/struct/Vehicle.ts index e682ca12..34962cb2 100644 --- a/src/common/struct/Vehicle.ts +++ b/src/common/struct/Vehicle.ts @@ -4,7 +4,6 @@ import { JSONMessage, Message, StartMessage, - UpdateMessage, } from '../../types/message'; import * as Task from '../../types/task'; import { VehicleObject, VehicleStatus } from '../../types/vehicle'; @@ -117,6 +116,11 @@ export default class Vehicle { return false; }); + this.updateEventHandler.addHandler('time', (time): boolean => { + this.lastConnectionTime = time; + return false; + }); + this.updateEventHandler.addHandler('lat', (lat): boolean => { this.lat = lat; return false; @@ -190,15 +194,12 @@ export default class Vehicle { /** * Updates all variables in this vehicle to the variables in the message. Called - * by the Orchestrator when the GCS receives an update message from the vehicle. - * Also updates the lastConnectionTime accordingly. + * by the Orchestrator when the GCS receives a message from the vehicle. * * @param message The message from the vehicle itself. */ public update(jsonMessage: JSONMessage): void { - const updateMessage = jsonMessage as UpdateMessage; - - this.lastConnectionTime = jsonMessage.time; + const updateMessage = jsonMessage; this.updateEventHandler.events(updateMessage); } @@ -211,14 +212,6 @@ export default class Vehicle { this.status = 'disconnected'; } - /** - * Sets lastConnectionTime to current time when function is called. Called - * by the Orchestrator whenever GCS receives a message from the vehicle. - */ - public updateLastConnectionTime(jsonMessage: JSONMessage): void { - this.lastConnectionTime = jsonMessage.time; - } - /** * Forwards message to MessageHandler to send through Xbee. * @@ -254,12 +247,11 @@ export default class Vehicle { this.assignedJob = jobType; if (errorCallback) this.errorCallback = errorCallback; - const startMessage: StartMessage = { + this.sendMessage({ type: 'start', jobType, - }; + }); - this.sendMessage(startMessage); this.updateEventHandler.addHandler('status', (value): boolean => { if (value === 'waiting') { if (completionCallback) completionCallback(); diff --git a/src/util/ipc.ts b/src/util/ipc.ts index 47e0d8cc..d1b89171 100644 --- a/src/util/ipc.ts +++ b/src/util/ipc.ts @@ -93,8 +93,11 @@ function postFinishMissions(completionParameters: Task.TaskParameters[]): void { * Files that take this notification: * - common/Orchestrator */ -function postHandleAcknowledgementMessage(jsonMessage: Message.JSONMessage): void { - ipcRenderer.send('post', 'handleAcknowledgementMessage', jsonMessage); +function postHandleAcknowledgementMessage( + jsonMessage: Message.JSONMessage, + newMessage: boolean, +): void { + ipcRenderer.send('post', 'handleAcknowledgementMessage', jsonMessage, newMessage); } /** @@ -103,8 +106,8 @@ function postHandleAcknowledgementMessage(jsonMessage: Message.JSONMessage): voi * Files that take this notification: * - common/Orchestrator */ -function postHandleBadMessage(jsonMessage: Message.JSONMessage): void { - ipcRenderer.send('post', 'handleBadMessage', jsonMessage); +function postHandleBadMessage(jsonMessage: Message.JSONMessage, newMessage: boolean): void { + ipcRenderer.send('post', 'handleBadMessage', jsonMessage, newMessage); } /** @@ -113,8 +116,8 @@ function postHandleBadMessage(jsonMessage: Message.JSONMessage): void { * Files that take this notification: * - common/Orchestrator */ -function postHandleCompleteMessage(jsonMessage: Message.JSONMessage): void { - ipcRenderer.send('post', 'handleCompleteMessage', jsonMessage); +function postHandleCompleteMessage(jsonMessage: Message.JSONMessage, newMessage: boolean): void { + ipcRenderer.send('post', 'handleCompleteMessage', jsonMessage, newMessage); } /** @@ -123,8 +126,8 @@ function postHandleCompleteMessage(jsonMessage: Message.JSONMessage): void { * Files that take this notification: * - common/Orchestrator */ -function postHandlePOIMessage(jsonMessage: Message.JSONMessage): void { - ipcRenderer.send('post', 'handlePOIMessage', jsonMessage); +function postHandlePOIMessage(jsonMessage: Message.JSONMessage, newMessage: boolean): void { + ipcRenderer.send('post', 'handlePOIMessage', jsonMessage, newMessage); } /** @@ -133,8 +136,8 @@ function postHandlePOIMessage(jsonMessage: Message.JSONMessage): void { * Files that take this notification: * - common/Orchestrator */ -function postHandleUpdateMessage(jsonMessage: Message.JSONMessage): void { - ipcRenderer.send('post', 'handleUpdateMessage', jsonMessage); +function postHandleUpdateMessage(jsonMessage: Message.JSONMessage, newMessage: boolean): void { + ipcRenderer.send('post', 'handleUpdateMessage', jsonMessage, newMessage); } /** @@ -276,6 +279,16 @@ function postStopMissions(): void { ipcRenderer.send('post', 'stopMissions'); } +/** + * Post "stopSendingMessage" notification. + * + * Files that take this notification: + * - common/MessageHandler + */ +function postStopSendingMessage(ackMessage: Message.JSONMessage): void { + ipcRenderer.send('post', 'stopSendingMessage', ackMessage); +} + /** * Post "stopSendingMessages" notification. * @@ -376,6 +389,7 @@ export default { postStartMissions, postStartNextMission, postStopMissions, + postStopSendingMessage, postStopSendingMessages, postToggleTheme, postUpdateBoundingBoxes, From 6271c9a661047433f5bea6f4af589c7a28a02210 Mon Sep 17 00:00:00 2001 From: Paul Matthew Barrameda Date: Fri, 19 Apr 2019 21:03:01 -0700 Subject: [PATCH 107/152] Fixed broken ts tests Forgot to run `npm test` again before committing --- src/common/struct/Vehicle.ts | 1 - src/renderer/fixtures/updateVehiclesFixtures.ts | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/common/struct/Vehicle.ts b/src/common/struct/Vehicle.ts index 34962cb2..f2d8aacb 100644 --- a/src/common/struct/Vehicle.ts +++ b/src/common/struct/Vehicle.ts @@ -3,7 +3,6 @@ import { JobType, vehicleConfig } from '../../static/index'; import { JSONMessage, Message, - StartMessage, } from '../../types/message'; import * as Task from '../../types/task'; import { VehicleObject, VehicleStatus } from '../../types/vehicle'; diff --git a/src/renderer/fixtures/updateVehiclesFixtures.ts b/src/renderer/fixtures/updateVehiclesFixtures.ts index 3c103cd3..5686df97 100644 --- a/src/renderer/fixtures/updateVehiclesFixtures.ts +++ b/src/renderer/fixtures/updateVehiclesFixtures.ts @@ -99,7 +99,7 @@ function updateVehicles(): void { lat: fixture.lat, lng: fixture.lng, status: status[Math.floor(Math.random() * status.length)], - })); + }), true); }); } From 5ea0abc5b5425ba1ea2fb495aa139a46e7d91a16 Mon Sep 17 00:00:00 2001 From: Paul Matthew Barrameda Date: Sat, 20 Apr 2019 01:19:22 -0700 Subject: [PATCH 108/152] Update logic in Orchestrator's ping + ack/self change Moved logic that requires only new message in an if loop, and put the Orchestrator acknowledge outside of that. Simplified loop of creating new event handler every few seconds with delta timeout to a simple timeout. Prevented disconnectFromVehicle to happening when vehicle is already disconnected (due to recursive calls after the program ends). Fixed logic in vehicle to not create a new instance every time it is reconnected. Added logic in the Orchestrator to send "stop" message to vehicle if it is a mission-related state (waiting, running, paused) while it is not assigned a mission. Fixed logic in MessageHandler as msgpack.decode actually obtains the object itself, not the string. The object can vary from undefined to a valid message. Also fixed the jobTypes type guard (see src/static/index) --- src/common/MessageHandler.ts | 29 ++--- src/common/Orchestrator.ts | 119 ++++++++++-------- src/common/Xbee.ts | 3 +- src/common/struct/Vehicle.ts | 9 ++ .../fixtures/updateVehiclesFixtures.ts | 2 +- src/static/index.ts | 4 +- src/util/ipc.ts | 9 +- 7 files changed, 93 insertions(+), 82 deletions(-) diff --git a/src/common/MessageHandler.ts b/src/common/MessageHandler.ts index 8356e2b5..476941e6 100644 --- a/src/common/MessageHandler.ts +++ b/src/common/MessageHandler.ts @@ -5,7 +5,6 @@ import { config, vehicleConfig } from '../static/index'; import * as Message from '../types/message'; import ipc from '../util/ipc'; -import { isJSON } from '../util/util'; import DictionaryList from './struct/DictionaryList'; import UpdateHandler from './struct/UpdateHandler'; @@ -41,7 +40,7 @@ class MessageHandler { public constructor() { ipcRenderer.on('sendMessage', (_: Event, vehicleId: number, message: Message.Message): void => this.sendMessage(vehicleId, message)); - ipcRenderer.on('receiveMessage', (_: Event, text: string): void => this.receiveMessage(text)); + ipcRenderer.on('receiveMessage', (_: Event, message: any): void => this.receiveMessage(message)); // eslint-disable-line @typescript-eslint/no-explicit-any ipcRenderer.on('stopSendingMessage', (_: Event, ackMessage: Message.JSONMessage): void => this.stopSendingMessage(ackMessage)); ipcRenderer.on('stopSendingMessages', this.stopSendingMessages); @@ -123,29 +122,23 @@ class MessageHandler { * * @param text The raw string in the message. */ - private receiveMessage(text: string): void { - // Filter out all invalid messages. - if (!isJSON(text)) { - ipc.postLogMessages({ - message: `Received text from Xbee that is not a JSON, could not send bad message to sender: ${text}`, - }); - return; - } + // eslint-disable-next-line @typescript-eslint/no-explicit-any + private receiveMessage(message: any): void { + if (!message) return; - const json = JSON.parse(text); - if (!Message.TypeGuard.isMessage(json)) { - if (!Number.isNaN(json.sid) && vehicleConfig.isValidVehicleId(json.sid as number)) { - this.sendBadMessage(json as Message.JSONMessage, 'Invalid message, does not meet requirements for a message'); + if (!Message.TypeGuard.isJSONMessage(message)) { + if (!Number.isNaN(message.sid) && vehicleConfig.isValidVehicleId(message.sid as number)) { + this.sendBadMessage(message as Message.JSONMessage, 'Invalid message, does not meet requirements for a message'); } else { ipc.postLogMessages({ - message: `Received JSON from Xbee that is not a valid message, could not send bad message to sender: ${text}`, + message: `Received JSON from Xbee that is not a valid message, could not send bad message to sender: ${message}`, }); } return; } - json.type = (json.type as string).toLowerCase(); - const jsonMessage = json as Message.JSONMessage; + message.type = message.type.toLowerCase(); // eslint-disable-line no-param-reassign + const jsonMessage = message as Message.JSONMessage; // Ignore messages from unrecognized vehicles. if (!vehicleConfig.isValidVehicleId(jsonMessage.sid)) return; @@ -154,7 +147,7 @@ class MessageHandler { || this.receivedMessageId[jsonMessage.sid] as number < jsonMessage.id; if (Message.TypeGuard.isConnectMessage(jsonMessage)) { - ipc.postConnectToVehicle(jsonMessage); + ipc.postConnectToVehicle(jsonMessage, newMessage); } else if (Message.TypeGuard.isCompleteMessage(jsonMessage)) { ipc.postHandleCompleteMessage(jsonMessage, newMessage); } else if (Message.TypeGuard.isPOIMessage(jsonMessage)) { diff --git a/src/common/Orchestrator.ts b/src/common/Orchestrator.ts index fea517c1..fae11d00 100644 --- a/src/common/Orchestrator.ts +++ b/src/common/Orchestrator.ts @@ -11,7 +11,6 @@ import ipc from '../util/ipc'; import missionObject, { MissionObject } from './missions/index'; import Mission from './struct/Mission'; -import UpdateHandler from './struct/UpdateHandler'; import Vehicle from './struct/Vehicle'; import './MessageHandler'; @@ -67,13 +66,8 @@ class Orchestrator { private vehicles: { [vehicleId: number]: Vehicle } = {}; - /** - * Keeps track of the vehicle and ensures it is connected. - */ - private vehiclePinger = new UpdateHandler(); - public constructor() { - ipcRenderer.on('connectToVehicle', (_: Event, jsonMessage: Message.JSONMessage): void => this.connectToVehicle(jsonMessage)); + ipcRenderer.on('connectToVehicle', (_: Event, jsonMessage: Message.JSONMessage, newMessage: boolean): void => this.connectToVehicle(jsonMessage, newMessage)); ipcRenderer.on('disconnectFromVehicle', (_: Event, vehicleId: number): void => this.disconnectFromVehicle(vehicleId)); ipcRenderer.on('handleAcknowledgementMessage', (_: Event, jsonMessage: Message.JSONMessage, newMessage: boolean): void => this.handleAcknowledgementMessage(jsonMessage, newMessage)); @@ -98,23 +92,29 @@ class Orchestrator { * Connects to vehicle specified by the connect message. * @param jsonMessage The connect message. */ - private connectToVehicle(jsonMessage: Message.JSONMessage): void { - if (this.vehicles[jsonMessage.sid] && this.vehicles[jsonMessage.sid].getStatus() !== 'disconnected') return; + private connectToVehicle(jsonMessage: Message.JSONMessage, newMessage: boolean): void { + if (!this.vehicles[jsonMessage.sid] || this.vehicles[jsonMessage.sid].getStatus() === 'disconnected') return; + if (!newMessage) return; + // Put this after newMessage return since this message will be sent repeatedly. ipc.postSendMessage(jsonMessage.sid, { type: 'connectionAck' }); - this.vehicles[jsonMessage.sid] = new Vehicle({ - sid: jsonMessage.sid, - jobs: (jsonMessage as Message.ConnectMessage).jobsAvailable, - status: 'ready', - }); + if (!this.vehicles[jsonMessage.sid]) { + this.vehicles[jsonMessage.sid] = new Vehicle({ + sid: jsonMessage.sid, + jobs: (jsonMessage as Message.ConnectMessage).jobsAvailable, + status: 'ready', + }); + + this.ping(this.vehicles[jsonMessage.sid]); + } else { + this.vehicles[jsonMessage.sid].connect(); + } ipc.postLogMessages({ type: 'success', message: `${(vehicleConfig.vehicleInfos[jsonMessage.sid] as VehicleInfo).name} has connected`, }); - - this.ping(this.vehicles[jsonMessage.sid]); } /** @@ -127,11 +127,8 @@ class Orchestrator { if (delta >= 0 && delta <= config.vehicleDisconnectionTime * 1000) { // Handler that expires and creates itself everytime it "pings" the vehicle. - this.vehiclePinger.addHandler(`${vehicle.getVehicleId()}`, (): boolean => false, { - callback: (): void => this.ping(vehicle), - time: delta, - }); - } else { + setTimeout((): void => { this.ping(vehicle); }, delta); + } else if (vehicle.getStatus() !== 'disconnected') { this.disconnectFromVehicle(vehicle.getVehicleId()); } } @@ -146,6 +143,11 @@ class Orchestrator { return; } + if (this.vehicles[vehicleId].getStatus() === 'disconnected') { + Orchestrator.postOrchestratorError(`Tried to disconnect from disconnected vehicle id ${vehicleId}`); + return; + } + this.vehicles[vehicleId].disconnect(); ipc.postUpdateVehicles(this.vehicles[vehicleId].toObject()); @@ -194,17 +196,22 @@ class Orchestrator { */ private handleUpdateMessage(jsonMessage: Message.JSONMessage, newMessage: boolean): void { if (!this.vehicles[jsonMessage.sid] || this.vehicles[jsonMessage.sid].getStatus() === 'disconnected') return; - if (!newMessage) return; - Orchestrator.acknowledgeMessage(jsonMessage); - - // This function updates the lastConnectionTime variable of the vehicle too. - this.vehicles[jsonMessage.sid].update(jsonMessage); - if (this.currentMission && this.currentMission.getVehicles()[jsonMessage.sid]) { - this.currentMission.update(jsonMessage); + if (newMessage) { + this.vehicles[jsonMessage.sid].update(jsonMessage); + if (this.currentMission && this.currentMission.getVehicles()[jsonMessage.sid]) { + this.currentMission.update(jsonMessage); + } else { + const status = this.vehicles[jsonMessage.sid].getStatus(); + if (status === 'waiting' || status === 'running' || status === 'paused') { + this.vehicles[jsonMessage.sid].stop(); + } + } + + ipc.postUpdateVehicles(this.vehicles[jsonMessage.sid].toObject()); } - ipc.postUpdateVehicles(this.vehicles[jsonMessage.sid].toObject()); + Orchestrator.acknowledgeMessage(jsonMessage); } /** @@ -213,20 +220,21 @@ class Orchestrator { */ private handlePOIMessage(jsonMessage: Message.JSONMessage, newMessage: boolean): void { if (!this.vehicles[jsonMessage.sid] || this.vehicles[jsonMessage.sid].getStatus() === 'disconnected') return; - if (!newMessage) return; - if (this.currentMission && !this.currentMission.getVehicles()[jsonMessage.sid]) { - ipc.postLogMessages({ - type: 'failure', - message: `Received point of interest message from ${vehicleConfig.vehicleInfos[jsonMessage.sid]} while it is not assigned to mission`, - }); - return; + if (newMessage) { + if (this.currentMission && !this.currentMission.getVehicles()[jsonMessage.sid]) { + ipc.postLogMessages({ + type: 'failure', + message: `Received point of interest message from ${vehicleConfig.vehicleInfos[jsonMessage.sid]} while it is not assigned to mission`, + }); + return; + } + + this.vehicles[jsonMessage.sid].update(jsonMessage); + if (this.currentMission) this.currentMission.update(jsonMessage); } Orchestrator.acknowledgeMessage(jsonMessage); - - this.vehicles[jsonMessage.sid].update(jsonMessage); - if (this.currentMission) this.currentMission.update(jsonMessage); } /** @@ -235,27 +243,28 @@ class Orchestrator { */ private handleCompleteMessage(jsonMessage: Message.JSONMessage, newMessage: boolean): void { if (!this.vehicles[jsonMessage.sid] || this.vehicles[jsonMessage.sid].getStatus() === 'disconnected') return; - if (!newMessage) return; - if (!this.currentMission) { - ipc.postLogMessages({ - type: 'failure', - message: `Received complete message from ${vehicleConfig.vehicleInfos[jsonMessage.sid]} while no mission is running`, - }); - return; - } + if (newMessage) { + if (!this.currentMission) { + ipc.postLogMessages({ + type: 'failure', + message: `Received complete message from ${vehicleConfig.vehicleInfos[jsonMessage.sid]} while no mission is running`, + }); + return; + } + + if (!this.currentMission.getVehicles()[jsonMessage.sid]) { + ipc.postLogMessages({ + type: 'failure', + message: `Received complete message from ${vehicleConfig.vehicleInfos[jsonMessage.sid]} while it is not assigned to the mission`, + }); + return; + } - if (!this.currentMission.getVehicles()[jsonMessage.sid]) { - ipc.postLogMessages({ - type: 'failure', - message: `Received complete message from ${vehicleConfig.vehicleInfos[jsonMessage.sid]} while it is not assigned to the mission`, - }); - return; + this.currentMission.update(jsonMessage); } Orchestrator.acknowledgeMessage(jsonMessage); - - this.currentMission.update(jsonMessage); } /** diff --git a/src/common/Xbee.ts b/src/common/Xbee.ts index d3ee1a7a..5736d1e0 100644 --- a/src/common/Xbee.ts +++ b/src/common/Xbee.ts @@ -9,7 +9,7 @@ import { JSONMessage } from '../types/message'; import ipc from '../util/ipc'; // TODO: Add a feature with Vehicle container to change this dynamically and reconnect. -const port: string | undefined = '/dev/tty.SLAB_USBtoUART'; +const port: string | undefined = 'COM5'; const serialport = new SerialPort(port, { baudRate: 57600 }, (error): void => { if (error) { @@ -90,7 +90,6 @@ serialport.on('close', (): void => { xbeeAPI.parser.on('data', (frame: Frame): void => { if (frame.type !== C.FRAME_TYPE.ZIGBEE_RECEIVE_PACKET || !frame.data) return; - console.log(msgpack.decode(frame.data)); ipc.postReceiveMessage(msgpack.decode(frame.data)); }); diff --git a/src/common/struct/Vehicle.ts b/src/common/struct/Vehicle.ts index f2d8aacb..0d9e210f 100644 --- a/src/common/struct/Vehicle.ts +++ b/src/common/struct/Vehicle.ts @@ -202,6 +202,15 @@ export default class Vehicle { this.updateEventHandler.events(updateMessage); } + /** + * Sets the vehicle to connected. It will go back to "ready" status and + * its update messages will bring it to its real status. + */ + public connect(): void { + this.status = 'ready'; + this.lastConnectionTime = Date.now(); + } + /** * Sets the vehicle as disconnected by changing it status to "disconnected". * The Orchestrator is in charge of preventing any more updates to the vehicle diff --git a/src/renderer/fixtures/updateVehiclesFixtures.ts b/src/renderer/fixtures/updateVehiclesFixtures.ts index 5686df97..cd6b5023 100644 --- a/src/renderer/fixtures/updateVehiclesFixtures.ts +++ b/src/renderer/fixtures/updateVehiclesFixtures.ts @@ -84,7 +84,7 @@ function connectVehicles(): void { ipc.postConnectToVehicle(generateJSONMessage(fixture.sid, { type: 'connect', jobsAvailable: fixture.jobs, - })); + }), true); }); } diff --git a/src/static/index.ts b/src/static/index.ts index 5d75fea9..1a005fc9 100644 --- a/src/static/index.ts +++ b/src/static/index.ts @@ -108,8 +108,8 @@ function isValidVehicleId(vehicleId: number): boolean { function isValidJobType(jobType: string): boolean { return jobType === 'isrSearch' || jobType === 'payloadDrop' - || jobType === 'ugvRetrieve' - || jobType === 'uuvRetrieve' + || jobType === 'ugvRescue' + || jobType === 'uuvRescue' || jobType === 'quickScan' || jobType === 'detailedSearch'; } diff --git a/src/util/ipc.ts b/src/util/ipc.ts index d1b89171..fa8fa903 100644 --- a/src/util/ipc.ts +++ b/src/util/ipc.ts @@ -63,8 +63,8 @@ function postConfirmCompleteMission(): void { * Files that take this notification: * - common/Orchestrator */ -function postConnectToVehicle(jsonMessage: Message.JSONMessage): void { - ipcRenderer.send('post', 'connectToVehicle', jsonMessage); +function postConnectToVehicle(jsonMessage: Message.JSONMessage, newMessage: boolean): void { + ipcRenderer.send('post', 'connectToVehicle', jsonMessage, newMessage); } /** @@ -185,8 +185,9 @@ function postLogMessages(...messages: LogMessage[]): void { * Files that take this notification: * - common/MessageHandler */ -function postReceiveMessage(text: string): void { - ipcRenderer.send('post', 'receiveMessage', text); +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function postReceiveMessage(message: any): void { + ipcRenderer.send('post', 'receiveMessage', message); } /** From bf2de79364e784db45bcdcd30cca0e9ddf581c15 Mon Sep 17 00:00:00 2001 From: Paul Matthew Barrameda Date: Mon, 22 Apr 2019 00:24:40 -0700 Subject: [PATCH 109/152] Fixed frame type in Xbee Fixed connection logic in Orchestrator Fixed logic in sending message in MessaageHandler --- src/common/MessageHandler.ts | 2 +- src/common/Orchestrator.ts | 2 +- src/common/Xbee.ts | 2 +- src/static/vehicle.json | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/common/MessageHandler.ts b/src/common/MessageHandler.ts index 476941e6..0aa963f7 100644 --- a/src/common/MessageHandler.ts +++ b/src/common/MessageHandler.ts @@ -73,11 +73,11 @@ class MessageHandler { this.id += 1; + xbee.sendMessage(jsonMessage); this.messageDictionary.push('sent', jsonMessage); if (Message.TypeGuard.isAcknowledgementMessage(message) || Message.TypeGuard.isBadMessage(message)) { - xbee.sendMessage(jsonMessage); return; } diff --git a/src/common/Orchestrator.ts b/src/common/Orchestrator.ts index fae11d00..99912750 100644 --- a/src/common/Orchestrator.ts +++ b/src/common/Orchestrator.ts @@ -93,7 +93,7 @@ class Orchestrator { * @param jsonMessage The connect message. */ private connectToVehicle(jsonMessage: Message.JSONMessage, newMessage: boolean): void { - if (!this.vehicles[jsonMessage.sid] || this.vehicles[jsonMessage.sid].getStatus() === 'disconnected') return; + if (this.vehicles[jsonMessage.sid] && this.vehicles[jsonMessage.sid].getStatus() !== 'disconnected') return; if (!newMessage) return; // Put this after newMessage return since this message will be sent repeatedly. diff --git a/src/common/Xbee.ts b/src/common/Xbee.ts index 5736d1e0..107b02d7 100644 --- a/src/common/Xbee.ts +++ b/src/common/Xbee.ts @@ -44,7 +44,7 @@ function sendMessage(message: JSONMessage): void { const { macAddress } = vehicleInfoObject as VehicleInfo; xbeeAPI.builder.write({ - type: C.FRAME_TYPE.ZIGBEE_TRANSMIT_STATUS, + type: C.FRAME_TYPE.ZIGBEE_TRANSMIT_REQUEST, destination64: macAddress, data: msgpack.encode(message), }); diff --git a/src/static/vehicle.json b/src/static/vehicle.json index 3b660ea4..4a2efe24 100644 --- a/src/static/vehicle.json +++ b/src/static/vehicle.json @@ -20,7 +20,7 @@ "type": "plane" }, "200": { - "macAddress": "0013A20041947790", + "macAddress": "0013A2004194754E", "name": "UGV", "type": "rover" }, From 92183f792e574593a87c996ae9413c9dc5a8c892 Mon Sep 17 00:00:00 2001 From: Paul Matthew Barrameda Date: Tue, 23 Apr 2019 16:16:15 -0700 Subject: [PATCH 110/152] Added bounding box --- src/renderer/fixtures/index.ts | 2 +- .../fixtures/updateBoundingBoxesFixtures.ts | 2 +- src/renderer/mainWindow/map/MapContainer.tsx | 58 ++-- .../mainWindow/map/marker/WaypointMarker.tsx | 20 +- .../mainWindow/map/shape/BoundingBox.tsx | 247 ++++++++++++++++++ src/static/config.json | 4 +- .../images/markers/draggable_selector.png | Bin 0 -> 2105 bytes src/static/index.ts | 4 +- src/util/ipc.ts | 5 +- src/util/util.ts | 8 +- 10 files changed, 300 insertions(+), 50 deletions(-) create mode 100644 src/renderer/mainWindow/map/shape/BoundingBox.tsx create mode 100644 src/static/images/markers/draggable_selector.png diff --git a/src/renderer/fixtures/index.ts b/src/renderer/fixtures/index.ts index d80f60b1..a38297cb 100644 --- a/src/renderer/fixtures/index.ts +++ b/src/renderer/fixtures/index.ts @@ -4,5 +4,5 @@ // import './logMessagesFixtures'; import './updateBoundingBoxesFixtures'; -import './updateVehiclesFixtures'; +// import './updateVehiclesFixtures'; import './updateWaypointsFixtures'; diff --git a/src/renderer/fixtures/updateBoundingBoxesFixtures.ts b/src/renderer/fixtures/updateBoundingBoxesFixtures.ts index 4ece7abb..77ab523e 100644 --- a/src/renderer/fixtures/updateBoundingBoxesFixtures.ts +++ b/src/renderer/fixtures/updateBoundingBoxesFixtures.ts @@ -15,4 +15,4 @@ const boundingBox = { }, }; -ipc.postUpdateBoundingBoxes(boundingBox); +ipc.postUpdateBoundingBoxes(true, boundingBox); diff --git a/src/renderer/mainWindow/map/MapContainer.tsx b/src/renderer/mainWindow/map/MapContainer.tsx index ceba9d03..c70994d6 100644 --- a/src/renderer/mainWindow/map/MapContainer.tsx +++ b/src/renderer/mainWindow/map/MapContainer.tsx @@ -1,16 +1,9 @@ import { Event, ipcRenderer } from 'electron'; import fs from 'fs'; import Leaflet from 'leaflet'; -import React, { - Component, - createRef, - Fragment, - ReactNode, - RefObject, -} from 'react'; +import React, { Component, Fragment, ReactNode } from 'react'; import { Map, - Rectangle, TileLayer, Viewport, } from 'react-leaflet'; @@ -26,6 +19,8 @@ import { updateVehicles } from '../../../util/util'; import GeolocationControl from './control/GeolocationControl'; import ThemeControl from './control/ThemeControl'; +import BoundingBox from './shape/BoundingBox'; + // import CachedTileLayer from './CachedTileLayer'; import VehicleMarker from './marker/VehicleMarker'; import WaypointMarker from './marker/WaypointMarker'; @@ -113,7 +108,7 @@ export default class MapContainer extends Component { waypoints: {}, }; - this.ref = createRef(); + this.ref = React.createRef(); this.onlocationfound = this.onlocationfound.bind(this); this.onViewportChanged = this.onViewportChanged.bind(this); @@ -133,8 +128,8 @@ export default class MapContainer extends Component { ipcRenderer.on('setMapToUserLocation', this.setMapToUserLocation); ipcRenderer.on('updateMapLocation', (_: Event, location: Location): void => this.updateMapLocation(location)); ipcRenderer.on('updateVehicles', (_: Event, ...vehicles: VehicleObject[]): void => updateVehicles(this, ...vehicles)); - ipcRenderer.on('updateWaypoints', (_: Event, updateMap?: boolean, ...waypoints: { name: string; location: Location }[]): void => this.updateWaypoints(updateMap, ...waypoints)); - ipcRenderer.on('updateBoundingBoxes', (_: Event, ...boundingBoxes: { name: string; bounds: BoundingBoxBounds}[]): void => this.updateBoundingBoxes(...boundingBoxes)); + ipcRenderer.on('updateWaypoints', (_: Event, updateMap: boolean, ...waypoints: { name: string; location: Location }[]): void => this.updateWaypoints(updateMap, ...waypoints)); + ipcRenderer.on('updateBoundingBoxes', (_: Event, updateMap: boolean, ...boundingBoxes: { name: string; bounds: BoundingBoxBounds}[]): void => this.updateBoundingBoxes(updateMap, ...boundingBoxes)); } /** @@ -174,7 +169,7 @@ export default class MapContainer extends Component { /** * Reference to map (to access its leaflet element). */ - private ref: RefObject; + private ref: React.RefObject; /** * Callback whenever a "loadConfig" notification is received. @@ -210,45 +205,46 @@ export default class MapContainer extends Component { * Updates waypoints. */ private updateWaypoints( - updateMap?: boolean, + updateMap: boolean, ...waypoints: { name: string; location: Location }[] ): void { - const { waypoints: thisWaypoints } = this.state; - const currentWaypoints = thisWaypoints; + const { waypoints: currentWaypoints } = this.state; + const newWaypoints = currentWaypoints; waypoints.forEach((waypoint): void => { - if (!currentWaypoints[waypoint.name]) { - currentWaypoints[waypoint.name] = { location: waypoint.location, locked: false }; + if (!newWaypoints[waypoint.name]) { + newWaypoints[waypoint.name] = { location: waypoint.location, locked: false }; } else if (updateMap) { - currentWaypoints[waypoint.name].location = waypoint.location; + newWaypoints[waypoint.name].location = waypoint.location; } }); - this.setState({ waypoints: currentWaypoints }); + this.setState({ waypoints: newWaypoints }); } /** * Updates bounding boxes. */ private updateBoundingBoxes( + updateMap: boolean, ...boundingBoxes: { name: string; color?: string; bounds: BoundingBoxBounds }[] ): void { - const { boundingBoxes: thisBoundingBoxes } = this.state; - const currentBoundingBoxes = thisBoundingBoxes; + const { boundingBoxes: currentBoundingBoxes } = this.state; + const newBoundingBoxes = currentBoundingBoxes; boundingBoxes.forEach((boundingBox): void => { - if (!currentBoundingBoxes[boundingBox.name]) { - currentBoundingBoxes[boundingBox.name] = { + if (!newBoundingBoxes[boundingBox.name]) { + newBoundingBoxes[boundingBox.name] = { color: boundingBox.color || '#000', bounds: boundingBox.bounds, locked: false, }; - } else { - currentBoundingBoxes[boundingBox.name].bounds = boundingBox.bounds; + } else if (updateMap) { + newBoundingBoxes[boundingBox.name].bounds = boundingBox.bounds; } }); - this.setState({ boundingBoxes: currentBoundingBoxes }); + this.setState({ boundingBoxes: newBoundingBoxes }); } public render(): ReactNode { @@ -262,14 +258,12 @@ export default class MapContainer extends Component { const boundingBoxRectangles = Object.keys(boundingBoxes) .map((name): ReactNode => ( - )); diff --git a/src/renderer/mainWindow/map/marker/WaypointMarker.tsx b/src/renderer/mainWindow/map/marker/WaypointMarker.tsx index 6ed2adf2..1789660f 100644 --- a/src/renderer/mainWindow/map/marker/WaypointMarker.tsx +++ b/src/renderer/mainWindow/map/marker/WaypointMarker.tsx @@ -2,7 +2,7 @@ import Leaflet from 'leaflet'; import React, { PureComponent, ReactNode } from 'react'; import { Marker, Tooltip } from 'react-leaflet'; -import { imageConfig, Location } from '../../../../static/index'; +import { Location } from '../../../../static/index'; import ipc from '../../../../util/ipc'; @@ -12,6 +12,17 @@ export interface WaypointMarkerProps { locked: boolean; } +// @ts-ignore +delete Leaflet.Icon.Default.prototype._getIconUrl; // eslint-disable-line no-underscore-dangle + +/* eslint-disable global-require */ +Leaflet.Icon.Default.mergeOptions({ + iconRetinaUrl: require('leaflet/dist/images/marker-icon-2x.png'), + iconUrl: require('leaflet/dist/images/marker-icon.png'), + shadowUrl: require('leaflet/dist/images/marker-shadow.png'), +}); +/* eslint-disable global-require */ + export default class WaypointMarker extends PureComponent { public constructor(props: WaypointMarkerProps) { super(props); @@ -37,15 +48,10 @@ export default class WaypointMarker extends PureComponent { draggable={!locked} ondrag={this.ondrag} position={[location.lat, location.lng]} - icon={Leaflet.icon({ - iconUrl: imageConfig.pin as string, - iconSize: [50, 50], - iconAnchor: [25, 50], - })} >

{name}

diff --git a/src/renderer/mainWindow/map/shape/BoundingBox.tsx b/src/renderer/mainWindow/map/shape/BoundingBox.tsx new file mode 100644 index 00000000..740b6367 --- /dev/null +++ b/src/renderer/mainWindow/map/shape/BoundingBox.tsx @@ -0,0 +1,247 @@ +import Leaflet from 'leaflet'; +import React, { PureComponent, ReactNode } from 'react'; +import { Marker, Rectangle } from 'react-leaflet'; + +import { imageConfig, RecursiveImageSignature } from '../../../../static/index'; + +import { BoundingBoxBounds } from '../../../../types/componentStyle'; + +import ipc from '../../../../util/ipc'; + +const selectorIcon = Leaflet.icon({ + iconUrl: (imageConfig.markers as RecursiveImageSignature).draggable_selector as string, + iconSize: [7, 7], +}); + +export interface BoundingBoxProps { + name: string; + startingBounds: BoundingBoxBounds; + locked: boolean; + color?: string; +} + +interface State { + /** + * Current bounds of the bounding box. + */ + bounds: BoundingBoxBounds; +} + +export default class BoundingBox extends PureComponent { + public constructor(props: BoundingBoxProps) { + super(props); + + this.ref1 = React.createRef(); + this.ref2 = React.createRef(); + this.ref3 = React.createRef(); + this.ref4 = React.createRef(); + this.ref = React.createRef(); + + this.ondrag = this.ondrag.bind(this); + this.ondrag1 = this.ondrag1.bind(this); + this.ondrag2 = this.ondrag2.bind(this); + this.ondrag3 = this.ondrag3.bind(this); + this.ondrag4 = this.ondrag4.bind(this); + } + + private ondrag(): void { + const { name } = this.props; + + const marker1 = this.ref1.current; + const marker2 = this.ref2.current; + const marker3 = this.ref3.current; + const marker4 = this.ref4.current; + const rectangle = this.ref.current; + + if (!marker1 || !marker2 || !marker3 || !marker4 || !rectangle) return; + + const location1 = marker1.leafletElement.getLatLng(); + const location2 = marker2.leafletElement.getLatLng(); + const location3 = marker3.leafletElement.getLatLng(); + const location4 = marker4.leafletElement.getLatLng(); + + const bounds = { + top: Math.max(location1.lat, location2.lat, location3.lat, location4.lat), + bottom: Math.min(location1.lat, location2.lat, location3.lat, location4.lat), + left: Math.min(location1.lng, location2.lng, location3.lng, location4.lng), + right: Math.max(location1.lng, location2.lng, location3.lng, location4.lng), + }; + + rectangle.leafletElement.setBounds([ + [bounds.bottom, bounds.left], + [bounds.top, bounds.right], + ]); + + ipc.postUpdateBoundingBoxes(false, { name, bounds }); + } + + /** + * Triggers when marker 1 moves. + * Marker 3 will change vertically. + * Marker 2 will change horizontally. + */ + private ondrag1(event: Leaflet.LeafletMouseEvent): void { + const marker3 = this.ref3.current; + const marker2 = this.ref2.current; + + if (!marker3 || !marker2) return; + + marker3.leafletElement.setLatLng({ + lat: event.latlng.lat, + lng: marker3.leafletElement.getLatLng().lng, + }); + + marker2.leafletElement.setLatLng({ + lat: marker2.leafletElement.getLatLng().lat, + lng: event.latlng.lng, + }); + + this.ondrag(); + } + + /** + * Triggers when marker 2 moves. + * Marker 4 will change vertically. + * Marker 1 will change horizontally. + */ + private ondrag2(event: Leaflet.LeafletMouseEvent): void { + const marker4 = this.ref4.current; + const marker1 = this.ref1.current; + + if (!marker4 || !marker1) return; + + marker4.leafletElement.setLatLng({ + lat: event.latlng.lat, + lng: marker4.leafletElement.getLatLng().lng, + }); + + marker1.leafletElement.setLatLng({ + lat: marker1.leafletElement.getLatLng().lat, + lng: event.latlng.lng, + }); + + this.ondrag(); + } + + /** + * Triggers when marker 3 moves. + * Marker 1 will change vertically. + * Marker 4 will change horizontally. + */ + private ondrag3(event: Leaflet.LeafletMouseEvent): void { + const marker1 = this.ref1.current; + const marker4 = this.ref4.current; + + if (!marker1 || !marker4) return; + + marker1.leafletElement.setLatLng({ + lat: event.latlng.lat, + lng: marker1.leafletElement.getLatLng().lng, + }); + + marker4.leafletElement.setLatLng({ + lat: marker4.leafletElement.getLatLng().lat, + lng: event.latlng.lng, + }); + + + this.ondrag(); + } + + /** + * Triggers when marker 4 moves. + * Marker 2 will change vertically. + * Marker 3 will change horizontally. + */ + private ondrag4(event: Leaflet.LeafletMouseEvent): void { + const marker2 = this.ref2.current; + const marker3 = this.ref3.current; + + if (!marker2 || !marker3) return; + + marker2.leafletElement.setLatLng({ + lat: event.latlng.lat, + lng: marker2.leafletElement.getLatLng().lng, + }); + + marker3.leafletElement.setLatLng({ + lat: marker3.leafletElement.getLatLng().lat, + lng: event.latlng.lng, + }); + + + this.ondrag(); + } + + /** + * Reference to marker 1 (to access its leaflet element). + */ + private ref1: React.RefObject; + + /** + * Reference to marker 2 (to access its leaflet element). + */ + private ref2: React.RefObject; + + /** + * Reference to marker 3 (to access its leaflet element). + */ + private ref3: React.RefObject; + + /** + * Reference to marker 4 (to access its leaflet element). + */ + private ref4: React.RefObject; + + /** + * Reference to rectangle (to access its leaflet element). + */ + private ref: React.RefObject; + + public render(): ReactNode { + const { locked, color, startingBounds } = this.props; + + return ( + <> + + + + + + + ); + } +} diff --git a/src/static/config.json b/src/static/config.json index 41da663b..96e6ba2d 100644 --- a/src/static/config.json +++ b/src/static/config.json @@ -1,6 +1,6 @@ { - "fixtures": false, - "geolocation": true, + "fixtures": true, + "geolocation": false, "vehicleDisconnectionTime": 20, "messageSendRate": 10 diff --git a/src/static/images/markers/draggable_selector.png b/src/static/images/markers/draggable_selector.png new file mode 100644 index 0000000000000000000000000000000000000000..2b94c6727e901c779130113ba2e43d91f2ce0762 GIT binary patch literal 2105 zcmcIlU1%Id9N#F47Aii7559D}c~Nk7XZH5?Zr7YKy*rx&H^$_kNk#44?Cjl|+nsfH zle+}0R+QNKP>P5xM8OANL{6oklGch}_+UZbL@5!35op;I9d>fhMNHE{z4N}_A$q&;a1T0x%H z+|h-QPA-g<$-)dVY-#vjaj0o>0*}U6Y?0GOb8r8xqgtq>;p+IAx8C zc@|PJE$e_F4T>2ZShe~s>N|& zDoUf#kQ*tPg;fO^hM_=JQB}YZAe#4M+ys6!*kdTrh=gtsyUZ68MqFXDaZch&J1%%Z zpVp7MVd4Q(nmAC93=@}nK+D=3>Un*16c^{Xm9E}(iKFs-K$Qs^vDuK&;vDtk!JaXj z^!Em{VZH6CO%z(E9*0F-80wn}YAj|^jcwpZf4T2XVCF-LV-}VftM?K$-eVE-dGSsQ z6W32z?@APPH_!r(X-?u)IwIx}j4-0D`s(G?;Qg7HJq%QdoB|r5bXwZC!kSfsxrH ze98V!+1iY7j(&v9jGoEhRJsDPdKQ9=t*M}*>o$P8Y8W(?)~KfT*d7dBesXYqEo-96 z=8PJGI>DF%oHi;z(=!kl*;E>E!mOh?SqH0}$0nx3LJ#vgaXnn6O5j%|@n1E0R%fBb zY##H}+WAsGKOQp2t@DLwV)rhwRLtwh&~>27C@Fg`gS$KGMm)#!-GekqpXm1LuGp?l z6O-gnhm~-{K+}A^S}~+<#v_|Bjx7O zxo9rz8hL`^fZj zSG3PwJKlWy6=nI|qsL}{+OzWJ$6IdSe>MHrQ@{QB^+Rv%JN)bZ9Vdqt4^ADpM(x7t Zqbu*(kTD{ literal 0 HcmV?d00001 diff --git a/src/static/index.ts b/src/static/index.ts index 1a005fc9..09b84f79 100644 --- a/src/static/index.ts +++ b/src/static/index.ts @@ -8,6 +8,7 @@ import plane from './images/markers/vehicles/plane.png'; import rover_red from './images/markers/vehicles/rover_red.png'; import rover from './images/markers/vehicles/rover.png'; +import draggable_selector from './images/markers/draggable_selector.png'; import poi_fp from './images/markers/poi_fp.png'; import poi_unknwn from './images/markers/poi_unkwn.png'; import poi_vld from './images/markers/poi_vld.png'; @@ -99,7 +100,7 @@ const startLocation: Location = startLocationString && locations[startLocationSt * Checks if a number is a valid vehicle id. */ function isValidVehicleId(vehicleId: number): boolean { - return vehicleInfos[vehicleId] !== undefined; + return vehicleId !== 0 && vehicleInfos[vehicleId] !== undefined; } /** @@ -151,6 +152,7 @@ export const imageConfig: RecursiveImageSignature = { pin, logo: { ngcp_calpoly, ngcp_pomona }, markers: { + draggable_selector, poi_fp, poi_unknwn, poi_vld, diff --git a/src/util/ipc.ts b/src/util/ipc.ts index fa8fa903..4a25a40b 100644 --- a/src/util/ipc.ts +++ b/src/util/ipc.ts @@ -317,9 +317,10 @@ function postToggleTheme(): void { * - renderer/mainWindow/map/MapContainer */ function postUpdateBoundingBoxes( + updateMap: boolean, ...boundingBoxes: { name: string; color?: string; bounds: BoundingBoxBounds }[] ): void { - ipcRenderer.send('post', 'updateBoundingBoxes', ...boundingBoxes); + ipcRenderer.send('post', 'updateBoundingBoxes', updateMap, ...boundingBoxes); } /** @@ -361,7 +362,7 @@ function postUpdateVehicles(...vehicles: VehicleObject[]): void { * @param updateMap Set this to true, except from the marker's drag event. */ function postUpdateWaypoint( - updateMap?: boolean, + updateMap: boolean, ...waypoints: { name: string; location: Location }[] ): void { ipcRenderer.send('post', 'updateWaypoints', updateMap, ...waypoints); diff --git a/src/util/util.ts b/src/util/util.ts index e34c1181..a82cb39f 100644 --- a/src/util/util.ts +++ b/src/util/util.ts @@ -12,14 +12,14 @@ export function updateVehicles( component: Component<{}, { vehicles: { [vehicleId: string]: VehicleObject } }>, ...vehicles: VehicleObject[] ): void { - const { vehicles: thisVehicles } = component.state; - const currentVehicles = thisVehicles; + const { vehicles: currentVehicles } = component.state; + const newVehicles = currentVehicles; vehicles.forEach((vehicle): void => { - currentVehicles[vehicle.vehicleId] = vehicle; + newVehicles[vehicle.vehicleId] = vehicle; }); - component.setState({ vehicles: currentVehicles }); + component.setState({ vehicles: newVehicles }); } /** From 70d8bd86de3c63e3cfbe607bc542b6c21461ffb3 Mon Sep 17 00:00:00 2001 From: Paul Matthew Barrameda Date: Tue, 23 Apr 2019 21:09:18 -0700 Subject: [PATCH 111/152] Added logic to MissionWindow Will change the logic in MissionWindow as the other apps become built. The way the other apps will work is that the apps will not send information to the MissionWindow unless all their fields are filled out. Other changes: Changed how the bounding box looks if it is locked (the draggable markers will be gone) Changed commenting to the fix in leaflet's default marker in WaypointMarker. Fixed layout choosing in App.tsx (not using any type anymore). --- src/common/Orchestrator.ts | 4 +- src/renderer/App.tsx | 13 ++-- src/renderer/mainWindow/map/MapContainer.tsx | 9 ++- .../mainWindow/map/marker/WaypointMarker.tsx | 10 +-- .../mainWindow/map/shape/BoundingBox.tsx | 40 ++++++------ src/renderer/missionWindow/MissionWindow.tsx | 61 +++++++++++++------ 6 files changed, 90 insertions(+), 47 deletions(-) diff --git a/src/common/Orchestrator.ts b/src/common/Orchestrator.ts index 99912750..2e05fa73 100644 --- a/src/common/Orchestrator.ts +++ b/src/common/Orchestrator.ts @@ -8,7 +8,7 @@ import * as Task from '../types/task'; import ipc from '../util/ipc'; -import missionObject, { MissionObject } from './missions/index'; +import missionObject from './missions/index'; import Mission from './struct/Mission'; import Vehicle from './struct/Vehicle'; @@ -351,7 +351,7 @@ class Orchestrator { return; } - const mission = missionObj as MissionObject; + const mission = missionObj; this.currentMissionIndex += 1; this.currentMission = new mission.constructor( diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index 74633eec..94e73c88 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -11,8 +11,8 @@ import 'react-virtualized/styles.css'; import './app.css'; -const windows: { [hash: string]: ReactNode } = { - '#main': MainWindow, +const windows: { [hash: string]: React.ElementType } = { + '#main': MainWindow as React.ElementType, '#mission': MissionWindow, }; @@ -44,11 +44,14 @@ export default class App extends Component<{}, State> { // In the case another hash was somehow loaded. if (window.location.hash !== '#main' && window.location.hash !== '#mission') { - return
; + return ( +
+

404 Not Found

+
+ ); } - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const Window = windows[window.location.hash] as any; + const Window = windows[window.location.hash]; return ; } diff --git a/src/renderer/mainWindow/map/MapContainer.tsx b/src/renderer/mainWindow/map/MapContainer.tsx index c70994d6..4f1ce9cd 100644 --- a/src/renderer/mainWindow/map/MapContainer.tsx +++ b/src/renderer/mainWindow/map/MapContainer.tsx @@ -227,7 +227,12 @@ export default class MapContainer extends Component { */ private updateBoundingBoxes( updateMap: boolean, - ...boundingBoxes: { name: string; color?: string; bounds: BoundingBoxBounds }[] + ...boundingBoxes: { + name: string; + bounds: BoundingBoxBounds; + color?: string; + locked?: boolean; + }[] ): void { const { boundingBoxes: currentBoundingBoxes } = this.state; const newBoundingBoxes = currentBoundingBoxes; @@ -237,7 +242,7 @@ export default class MapContainer extends Component { newBoundingBoxes[boundingBox.name] = { color: boundingBox.color || '#000', bounds: boundingBox.bounds, - locked: false, + locked: boundingBox.locked || false, }; } else if (updateMap) { newBoundingBoxes[boundingBox.name].bounds = boundingBox.bounds; diff --git a/src/renderer/mainWindow/map/marker/WaypointMarker.tsx b/src/renderer/mainWindow/map/marker/WaypointMarker.tsx index 1789660f..948c71c6 100644 --- a/src/renderer/mainWindow/map/marker/WaypointMarker.tsx +++ b/src/renderer/mainWindow/map/marker/WaypointMarker.tsx @@ -12,16 +12,18 @@ export interface WaypointMarkerProps { locked: boolean; } -// @ts-ignore -delete Leaflet.Icon.Default.prototype._getIconUrl; // eslint-disable-line no-underscore-dangle +/* eslint-disable */ + +// See this issue for the following logic: https://github.com/PaulLeCam/react-leaflet/issues/255#issuecomment-269750542. +delete (Leaflet.Icon.Default.prototype as any)._getIconUrl; -/* eslint-disable global-require */ Leaflet.Icon.Default.mergeOptions({ iconRetinaUrl: require('leaflet/dist/images/marker-icon-2x.png'), iconUrl: require('leaflet/dist/images/marker-icon.png'), shadowUrl: require('leaflet/dist/images/marker-shadow.png'), }); -/* eslint-disable global-require */ + +/* eslint-disable */ export default class WaypointMarker extends PureComponent { public constructor(props: WaypointMarkerProps) { diff --git a/src/renderer/mainWindow/map/shape/BoundingBox.tsx b/src/renderer/mainWindow/map/shape/BoundingBox.tsx index 740b6367..db736070 100644 --- a/src/renderer/mainWindow/map/shape/BoundingBox.tsx +++ b/src/renderer/mainWindow/map/shape/BoundingBox.tsx @@ -201,47 +201,53 @@ export default class BoundingBox extends PureComponent public render(): ReactNode { const { locked, color, startingBounds } = this.props; - return ( - <> - + const boundingMarkers = ( +
- +
+ ); + + return ( +
+ + {!locked && boundingMarkers} +
); } } diff --git a/src/renderer/missionWindow/MissionWindow.tsx b/src/renderer/missionWindow/MissionWindow.tsx index 5652d9de..3d5882f6 100644 --- a/src/renderer/missionWindow/MissionWindow.tsx +++ b/src/renderer/missionWindow/MissionWindow.tsx @@ -1,27 +1,28 @@ import React, { Component, ReactNode } from 'react'; import { ThemeProps } from '../../types/componentStyle'; -// import * as MissionInformation from '../../types/missionInformation'; +import * as MissionInformation from '../../types/missionInformation'; import ISRSearch from './parameters/ISRSearch'; -import PayLoadDrop from './parameters/PayloadDrop'; +import PayloadDrop from './parameters/PayloadDrop'; import UGVRetrieveTarget from './parameters/UGVRetrieveTarget'; import UUVRetreiveTarget from './parameters/UUVRetrieveTarget'; import VTOLSearch from './parameters/VTOLSearch'; -const layouts: { [missionName: string]: () => ReactNode } = { - payloadDrop: PayLoadDrop, - isrSearch: ISRSearch, - ugvRescue: UGVRetrieveTarget, - uuvRescue: UUVRetreiveTarget, - vtolSearch: VTOLSearch, -}; +import ipc from '../../util/ipc'; -type MissionName = 'isrSearch' | 'vtolSearch' | 'payloadDrop' | 'ugvRetrieve' | 'uuvRetrieve'; +const layoutsLandMission = [ISRSearch, VTOLSearch, PayloadDrop, UGVRetrieveTarget]; +const layoutsUnderwaterMission = [ISRSearch, VTOLSearch, PayloadDrop, UUVRetreiveTarget]; interface State { - firstMission: MissionName; - // information: Information; + missionType: 'land' | 'underwater'; + requireConfirmation: boolean; + startMissionIndex: number; + endMissionIndex: number; + information: { + landMission: MissionInformation.Information[]; + underwaterMission: MissionInformation.Information[]; + }; } /** @@ -32,21 +33,47 @@ export default class MissionWindow extends Component { super(props); this.state = { - firstMission: 'isrSearch', + missionType: 'land', + requireConfirmation: true, + startMissionIndex: 0, + endMissionIndex: 0, + information: { + landMission: [], + underwaterMission: [], + }, }; + + this.postStartMissions = this.postStartMissions.bind(this); + } + + private postStartMissions(): void { + const { + missionType, + requireConfirmation, + startMissionIndex, + endMissionIndex, + information, + } = this.state; + + const missionInformation = missionType === 'land' ? information.landMission : information.underwaterMission; + + ipc.postStartMissions( + missionInformation.slice(startMissionIndex, endMissionIndex + 1), + requireConfirmation, + ); } public render(): ReactNode { const { theme } = this.props; - const { firstMission } = this.state; + const { missionType, startMissionIndex } = this.state; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const Layout = layouts[firstMission] as any; + const Layout = (missionType === 'land' ? layoutsLandMission[startMissionIndex] + : layoutsUnderwaterMission[startMissionIndex]) as React.ElementType; return (
- +
); } From d8b4300d47bc75209eba7d7160c8d9ce37975be5 Mon Sep 17 00:00:00 2001 From: Paul Matthew Barrameda Date: Tue, 23 Apr 2019 22:07:37 -0700 Subject: [PATCH 112/152] Added slider to drag as well as styling Other edits: Bumped version to 0.7 since we're finally implementing mission UI. Named the mission parameter files properly so that they look nice in the meantime. Exported all variables in common mission files (common/missions) so that the mission UI files can use them without importing default. Change starting theme to dark to reduce stress to my eyes whenever I make changes to code. --- package.json | 4 +- src/common/missions/ISRSearch.ts | 6 +- src/common/missions/PayloadDrop.ts | 6 +- src/common/missions/UGVRescue.ts | 6 +- src/common/missions/UUVRescue.ts | 6 +- src/common/missions/VTOLSearch.ts | 6 +- src/renderer/App.tsx | 3 +- src/renderer/missionWindow/MissionWindow.tsx | 57 ++++++++++++++++--- src/renderer/missionWindow/mission.css | 24 +++++--- .../missionWindow/parameters/ISRSearch.tsx | 5 +- .../missionWindow/parameters/PayloadDrop.tsx | 5 +- .../missionWindow/parameters/UGVRescue.tsx | 8 +++ .../parameters/UGVRetrieveTarget.tsx | 9 --- .../missionWindow/parameters/UUVRescue.tsx | 8 +++ .../parameters/UUVRetrieveTarget.tsx | 9 --- .../missionWindow/parameters/VTOLSearch.tsx | 7 +-- .../missionWindow/parameters/parameters.css | 0 17 files changed, 108 insertions(+), 61 deletions(-) create mode 100644 src/renderer/missionWindow/parameters/UGVRescue.tsx delete mode 100644 src/renderer/missionWindow/parameters/UGVRetrieveTarget.tsx create mode 100644 src/renderer/missionWindow/parameters/UUVRescue.tsx delete mode 100644 src/renderer/missionWindow/parameters/UUVRetrieveTarget.tsx create mode 100644 src/renderer/missionWindow/parameters/parameters.css diff --git a/package.json b/package.json index 37af15bf..bd48e10b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "GCS", - "version": "0.6.5", + "version": "0.7.0", "license": "MIT", "author": "Northrop Grumman Collaboration Project", "description": "Ground Control Station for autonomous vehicle platforms in NGCP", @@ -22,6 +22,7 @@ "msgpack-lite": "^0.1.26", "pouchdb": "^7.0.0", "prop-types": "^15.7.2", + "rc-slider": "^8.6.9", "react": "^16.8.6", "react-dom": "^16.8.6", "react-leaflet": "^2.2.1", @@ -38,6 +39,7 @@ "@types/msgpack-lite": "^0.1.6", "@types/node": "^11.13.5", "@types/pouchdb": "^6.3.3", + "@types/rc-slider": "^8.6.3", "@types/react": "^16.8.13", "@types/react-dom": "^16.8.4", "@types/react-leaflet": "^2.2.1", diff --git a/src/common/missions/ISRSearch.ts b/src/common/missions/ISRSearch.ts index ff1821d0..a814466b 100644 --- a/src/common/missions/ISRSearch.ts +++ b/src/common/missions/ISRSearch.ts @@ -12,11 +12,11 @@ import { getBoundingBox } from '../../util/util'; import Vehicle from '../struct/Vehicle'; -const missionName: MissionInformation.MissionName = 'isrSearch'; +export const missionName: MissionInformation.MissionName = 'isrSearch'; -const jobTypes: JobType[] = ['isrSearch']; +export const jobTypes: JobType[] = ['isrSearch']; -class ISRSearch extends Mission { +export class ISRSearch extends Mission { protected missionName = missionName; protected jobTypes = new Set(jobTypes); diff --git a/src/common/missions/PayloadDrop.ts b/src/common/missions/PayloadDrop.ts index b62ef1d6..035d7334 100644 --- a/src/common/missions/PayloadDrop.ts +++ b/src/common/missions/PayloadDrop.ts @@ -11,11 +11,11 @@ import ipc from '../../util/ipc'; import Vehicle from '../struct/Vehicle'; -const missionName: MissionInformation.MissionName = 'payloadDrop'; +export const missionName: MissionInformation.MissionName = 'payloadDrop'; -const jobTypes: JobType[] = ['payloadDrop']; +export const jobTypes: JobType[] = ['payloadDrop']; -class PayloadDrop extends Mission { +export class PayloadDrop extends Mission { protected missionName = missionName; protected jobTypes = new Set(jobTypes); diff --git a/src/common/missions/UGVRescue.ts b/src/common/missions/UGVRescue.ts index d4282632..6cc8a647 100644 --- a/src/common/missions/UGVRescue.ts +++ b/src/common/missions/UGVRescue.ts @@ -8,11 +8,11 @@ import { Task, TaskParameters } from '../../types/task'; import Vehicle from '../struct/Vehicle'; -const missionName: MissionInformation.MissionName = 'ugvRescue'; +export const missionName: MissionInformation.MissionName = 'ugvRescue'; -const jobTypes: JobType[] = ['ugvRescue']; +export const jobTypes: JobType[] = ['ugvRescue']; -class UGVRescue extends Mission { +export class UGVRescue extends Mission { protected missionName = missionName; protected jobTypes = new Set(jobTypes); diff --git a/src/common/missions/UUVRescue.ts b/src/common/missions/UUVRescue.ts index dfaa3487..1887e5b2 100644 --- a/src/common/missions/UUVRescue.ts +++ b/src/common/missions/UUVRescue.ts @@ -8,11 +8,11 @@ import { Task, TaskParameters } from '../../types/task'; import Vehicle from '../struct/Vehicle'; -const missionName: MissionInformation.MissionName = 'uuvRescue'; +export const missionName: MissionInformation.MissionName = 'uuvRescue'; -const jobTypes: JobType[] = ['uuvRescue']; +export const jobTypes: JobType[] = ['uuvRescue']; -class UUVRescue extends Mission { +export class UUVRescue extends Mission { protected missionName = missionName; protected jobTypes = new Set(jobTypes); diff --git a/src/common/missions/VTOLSearch.ts b/src/common/missions/VTOLSearch.ts index 8a633a5b..56351b80 100644 --- a/src/common/missions/VTOLSearch.ts +++ b/src/common/missions/VTOLSearch.ts @@ -12,11 +12,11 @@ import { getDistance } from '../../util/util'; import Vehicle from '../struct/Vehicle'; -const missionName: MissionInformation.MissionName = 'vtolSearch'; +export const missionName: MissionInformation.MissionName = 'vtolSearch'; -const jobTypes: JobType[] = ['quickScan', 'detailedSearch']; +export const jobTypes: JobType[] = ['quickScan', 'detailedSearch']; -class VTOLSearch extends Mission { +export class VTOLSearch extends Mission { protected missionName = missionName; protected jobTypes = new Set(jobTypes); diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index 94e73c88..2aa128f1 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -8,6 +8,7 @@ import MissionWindow from './missionWindow/MissionWindow'; import 'leaflet/dist/leaflet.css'; import 'react-virtualized/styles.css'; +import 'rc-slider/assets/index.css'; import './app.css'; @@ -23,7 +24,7 @@ export default class App extends Component<{}, State> { super(props); this.state = { - theme: 'light', + theme: 'dark', }; this.toggleTheme = this.toggleTheme.bind(this); diff --git a/src/renderer/missionWindow/MissionWindow.tsx b/src/renderer/missionWindow/MissionWindow.tsx index 3d5882f6..4c465545 100644 --- a/src/renderer/missionWindow/MissionWindow.tsx +++ b/src/renderer/missionWindow/MissionWindow.tsx @@ -1,18 +1,21 @@ import React, { Component, ReactNode } from 'react'; +import { Range } from 'rc-slider'; import { ThemeProps } from '../../types/componentStyle'; import * as MissionInformation from '../../types/missionInformation'; import ISRSearch from './parameters/ISRSearch'; import PayloadDrop from './parameters/PayloadDrop'; -import UGVRetrieveTarget from './parameters/UGVRetrieveTarget'; -import UUVRetreiveTarget from './parameters/UUVRetrieveTarget'; +import UGVRescue from './parameters/UGVRescue'; +import UUVRescue from './parameters/UUVRescue'; import VTOLSearch from './parameters/VTOLSearch'; import ipc from '../../util/ipc'; -const layoutsLandMission = [ISRSearch, VTOLSearch, PayloadDrop, UGVRetrieveTarget]; -const layoutsUnderwaterMission = [ISRSearch, VTOLSearch, PayloadDrop, UUVRetreiveTarget]; +import './mission.css'; + +const layoutsLandMission = [ISRSearch, VTOLSearch, PayloadDrop, UGVRescue]; +const layoutsUnderwaterMission = [ISRSearch, VTOLSearch, PayloadDrop, UUVRescue]; interface State { missionType: 'land' | 'underwater'; @@ -43,7 +46,16 @@ export default class MissionWindow extends Component { }, }; + this.onSliderChange = this.onSliderChange.bind(this); this.postStartMissions = this.postStartMissions.bind(this); + this.toggleMissionType = this.toggleMissionType.bind(this); + } + + private onSliderChange(value: [number, number]): void { + this.setState({ + startMissionIndex: value[0], + endMissionIndex: value[1], + }); } private postStartMissions(): void { @@ -63,17 +75,44 @@ export default class MissionWindow extends Component { ); } + private toggleMissionType(): void { + const { missionType } = this.state; + + this.setState({ missionType: missionType === 'land' ? 'underwater' : 'land' }); + } + public render(): ReactNode { const { theme } = this.props; const { missionType, startMissionIndex } = this.state; - const Layout = (missionType === 'land' ? layoutsLandMission[startMissionIndex] - : layoutsUnderwaterMission[startMissionIndex]) as React.ElementType; + const missionTypeText = missionType === 'land' ? 'Land Missions' : 'Underwater Missions'; + + const layouts = missionType === 'land' ? layoutsLandMission : layoutsUnderwaterMission; + + if (startMissionIndex >= layouts.length) { + throw new RangeError('Layout chosen is out of range'); + } + + const Layout = layouts[startMissionIndex] as React.ElementType; + return ( -
- - +
+
+ + +
+
+ +
+
+ +
); } diff --git a/src/renderer/missionWindow/mission.css b/src/renderer/missionWindow/mission.css index dd9662c3..0f8ab08a 100644 --- a/src/renderer/missionWindow/mission.css +++ b/src/renderer/missionWindow/mission.css @@ -1,19 +1,19 @@ /* Defined styles for MissionWindow.jsx */ .missionWrapper, -.missionWrapper_dark, -.mission, -.job { +.missionWrapper_dark { display: grid; height: 100%; width: 100; -} - -.job { grid-template-areas: + 'selector' 'info' 'button'; - grid-template-rows: 7fr 1fr; + grid-template-rows: 25px auto 25px; +} + +.selectorContainer { + grid-area: selector; } .infoContainer { @@ -37,3 +37,13 @@ background-color: #212121; color: #fff; } + +.selectorButton { + margin-right: 10px; +} + +.selectorSlider, +.selectorSlider_dark { + width: 200px !important; + display: inline-block; +} diff --git a/src/renderer/missionWindow/parameters/ISRSearch.tsx b/src/renderer/missionWindow/parameters/ISRSearch.tsx index acd43fdb..e793bfcd 100644 --- a/src/renderer/missionWindow/parameters/ISRSearch.tsx +++ b/src/renderer/missionWindow/parameters/ISRSearch.tsx @@ -1,9 +1,8 @@ import React, { ReactNode } from 'react'; - -export const jobs = ['isrSearch']; +// import { jobTypes } from '../../../common/missions/ISRSearch'; export default function ISRSearch(): ReactNode { return ( -
ISRSearch
+
ISR Search
); } diff --git a/src/renderer/missionWindow/parameters/PayloadDrop.tsx b/src/renderer/missionWindow/parameters/PayloadDrop.tsx index e12654aa..d429d072 100644 --- a/src/renderer/missionWindow/parameters/PayloadDrop.tsx +++ b/src/renderer/missionWindow/parameters/PayloadDrop.tsx @@ -1,8 +1,7 @@ import React, { ReactNode } from 'react'; +// import { jobTypes } from '../../../common/missions/PayloadDrop'; -export const jobs = ['payloadDrop']; - -export default function ISRSearch(): ReactNode { +export default function PayloadDrop(): ReactNode { return (
Payload Drop
); diff --git a/src/renderer/missionWindow/parameters/UGVRescue.tsx b/src/renderer/missionWindow/parameters/UGVRescue.tsx new file mode 100644 index 00000000..7bd2fa7f --- /dev/null +++ b/src/renderer/missionWindow/parameters/UGVRescue.tsx @@ -0,0 +1,8 @@ +import React, { ReactNode } from 'react'; +// import { jobTypes } from '../../../common/missions/UGVRescue'; + +export default function UGVRescue(): ReactNode { + return ( +
UGV Rescue
+ ); +} diff --git a/src/renderer/missionWindow/parameters/UGVRetrieveTarget.tsx b/src/renderer/missionWindow/parameters/UGVRetrieveTarget.tsx deleted file mode 100644 index e22e1c97..00000000 --- a/src/renderer/missionWindow/parameters/UGVRetrieveTarget.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import React, { ReactNode } from 'react'; - -export const jobs = ['ugvRetrieve']; - -export default function ISRSearch(): ReactNode { - return ( -
ISRSearch
- ); -} diff --git a/src/renderer/missionWindow/parameters/UUVRescue.tsx b/src/renderer/missionWindow/parameters/UUVRescue.tsx new file mode 100644 index 00000000..7f2b2d59 --- /dev/null +++ b/src/renderer/missionWindow/parameters/UUVRescue.tsx @@ -0,0 +1,8 @@ +import React, { ReactNode } from 'react'; +// import { jobTypes } from '../../../common/missions/UUVRescue'; + +export default function UUVRescue(): ReactNode { + return ( +
UUV Rescue
+ ); +} diff --git a/src/renderer/missionWindow/parameters/UUVRetrieveTarget.tsx b/src/renderer/missionWindow/parameters/UUVRetrieveTarget.tsx deleted file mode 100644 index 7023ae0d..00000000 --- a/src/renderer/missionWindow/parameters/UUVRetrieveTarget.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import React, { ReactNode } from 'react'; - -export const jobs = ['uuvRetrieve']; - -export default function ISRSearch(): ReactNode { - return ( -
ISRSearch
- ); -} diff --git a/src/renderer/missionWindow/parameters/VTOLSearch.tsx b/src/renderer/missionWindow/parameters/VTOLSearch.tsx index c7c5a903..b0c03ba2 100644 --- a/src/renderer/missionWindow/parameters/VTOLSearch.tsx +++ b/src/renderer/missionWindow/parameters/VTOLSearch.tsx @@ -1,9 +1,8 @@ import React, { ReactNode } from 'react'; +// import { jobTypes } from '../../../common/missions/VTOLSearch'; -export const jobs = ['quickScan', 'detailedSearch']; - -export default function ISRSearch(): ReactNode { +export default function VTOLSearch(): ReactNode { return ( -
ISRSearch
+
VTOL Search
); } diff --git a/src/renderer/missionWindow/parameters/parameters.css b/src/renderer/missionWindow/parameters/parameters.css new file mode 100644 index 00000000..e69de29b From f94f97d234b2d67a195df9a06c4fa64e886cf873 Mon Sep 17 00:00:00 2001 From: Paul Matthew Barrameda Date: Tue, 23 Apr 2019 22:09:05 -0700 Subject: [PATCH 113/152] Fixed eslint issue I keep forgetting to run `npm test` before pushing --- src/common/missions/index.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/common/missions/index.ts b/src/common/missions/index.ts index e7d78a28..931e8901 100644 --- a/src/common/missions/index.ts +++ b/src/common/missions/index.ts @@ -1,3 +1,5 @@ +/* eslint-disable import/no-named-as-default */ + import { JobType } from '../../static/index'; import { MissionName } from '../../types/missionInformation'; From 574f61905227bc9d20d53cd3d1a12a8f556960a8 Mon Sep 17 00:00:00 2001 From: Paul Matthew Barrameda Date: Tue, 23 Apr 2019 22:51:48 -0700 Subject: [PATCH 114/152] Added "tip" to scroll --- src/renderer/missionWindow/MissionWindow.tsx | 55 ++++++++++++++++---- 1 file changed, 44 insertions(+), 11 deletions(-) diff --git a/src/renderer/missionWindow/MissionWindow.tsx b/src/renderer/missionWindow/MissionWindow.tsx index 4c465545..e950ea52 100644 --- a/src/renderer/missionWindow/MissionWindow.tsx +++ b/src/renderer/missionWindow/MissionWindow.tsx @@ -1,5 +1,5 @@ import React, { Component, ReactNode } from 'react'; -import { Range } from 'rc-slider'; +import * as Slider from 'rc-slider'; import { ThemeProps } from '../../types/componentStyle'; import * as MissionInformation from '../../types/missionInformation'; @@ -14,8 +14,40 @@ import ipc from '../../util/ipc'; import './mission.css'; -const layoutsLandMission = [ISRSearch, VTOLSearch, PayloadDrop, UGVRescue]; -const layoutsUnderwaterMission = [ISRSearch, VTOLSearch, PayloadDrop, UUVRescue]; +const Range = Slider.createSliderWithTooltip(Slider.Range); + +interface MissionLayout { + name: string; + layout: React.ElementType; +} + +const layoutsLandMission: MissionLayout[] = [{ + name: 'ISR Search', + layout: ISRSearch as React.ElementType, +}, { + name: 'VTOL Search', + layout: VTOLSearch as React.ElementType, +}, { + name: 'Payload Drop', + layout: PayloadDrop as React.ElementType, +}, { + name: 'UGV Rescue', + layout: UGVRescue as React.ElementType, +}]; + +const layoutsUnderwaterMission: MissionLayout[] = [{ + name: 'ISR Search', + layout: ISRSearch as React.ElementType, +}, { + name: 'VTOL Search', + layout: VTOLSearch as React.ElementType, +}, { + name: 'Payload Drop', + layout: PayloadDrop as React.ElementType, +}, { + name: 'UUV Rescue', + layout: UUVRescue as React.ElementType, +}]; interface State { missionType: 'land' | 'underwater'; @@ -49,6 +81,7 @@ export default class MissionWindow extends Component { this.onSliderChange = this.onSliderChange.bind(this); this.postStartMissions = this.postStartMissions.bind(this); this.toggleMissionType = this.toggleMissionType.bind(this); + this.tipFormatter = this.tipFormatter.bind(this); } private onSliderChange(value: [number, number]): void { @@ -77,10 +110,15 @@ export default class MissionWindow extends Component { private toggleMissionType(): void { const { missionType } = this.state; - this.setState({ missionType: missionType === 'land' ? 'underwater' : 'land' }); } + private tipFormatter(value: number): string { + const { missionType } = this.state; + + return missionType === 'land' ? layoutsLandMission[value].name : layoutsUnderwaterMission[value].name; + } + public render(): ReactNode { const { theme } = this.props; const { missionType, startMissionIndex } = this.state; @@ -88,13 +126,7 @@ export default class MissionWindow extends Component { const missionTypeText = missionType === 'land' ? 'Land Missions' : 'Underwater Missions'; const layouts = missionType === 'land' ? layoutsLandMission : layoutsUnderwaterMission; - - if (startMissionIndex >= layouts.length) { - throw new RangeError('Layout chosen is out of range'); - } - - const Layout = layouts[startMissionIndex] as React.ElementType; - + const Layout = layouts[startMissionIndex].layout; return (
@@ -105,6 +137,7 @@ export default class MissionWindow extends Component { min={0} max={layouts.length - 1} onChange={this.onSliderChange} + tipFormatter={this.tipFormatter} />
From 08a3fcd73a3819ac9210002a4e18cf792e392bbc Mon Sep 17 00:00:00 2001 From: Paul Matthew Barrameda Date: Tue, 23 Apr 2019 23:53:48 -0700 Subject: [PATCH 115/152] Modified logic + styling --- src/renderer/missionWindow/MissionWindow.tsx | 105 +++++++++++------- src/renderer/missionWindow/mission.css | 5 +- .../missionWindow/parameters/ISRSearch.tsx | 13 ++- .../missionWindow/parameters/PayloadDrop.tsx | 13 ++- .../missionWindow/parameters/UGVRescue.tsx | 13 ++- .../missionWindow/parameters/UUVRescue.tsx | 13 ++- .../missionWindow/parameters/VTOLSearch.tsx | 13 ++- 7 files changed, 124 insertions(+), 51 deletions(-) diff --git a/src/renderer/missionWindow/MissionWindow.tsx b/src/renderer/missionWindow/MissionWindow.tsx index e950ea52..7fe6cfe5 100644 --- a/src/renderer/missionWindow/MissionWindow.tsx +++ b/src/renderer/missionWindow/MissionWindow.tsx @@ -1,8 +1,13 @@ +/* eslint-disable import/no-named-as-default */ + import React, { Component, ReactNode } from 'react'; import * as Slider from 'rc-slider'; +import { JobType } from '../../static/index'; + import { ThemeProps } from '../../types/componentStyle'; import * as MissionInformation from '../../types/missionInformation'; +import { VehicleStatus } from '../../types/vehicle'; import ISRSearch from './parameters/ISRSearch'; import PayloadDrop from './parameters/PayloadDrop'; @@ -18,45 +23,52 @@ const Range = Slider.createSliderWithTooltip(Slider.Range); interface MissionLayout { name: string; + jobTypes: JobType[]; layout: React.ElementType; } -const layoutsLandMission: MissionLayout[] = [{ - name: 'ISR Search', - layout: ISRSearch as React.ElementType, -}, { - name: 'VTOL Search', - layout: VTOLSearch as React.ElementType, -}, { - name: 'Payload Drop', - layout: PayloadDrop as React.ElementType, -}, { - name: 'UGV Rescue', - layout: UGVRescue as React.ElementType, -}]; - -const layoutsUnderwaterMission: MissionLayout[] = [{ - name: 'ISR Search', - layout: ISRSearch as React.ElementType, -}, { - name: 'VTOL Search', - layout: VTOLSearch as React.ElementType, -}, { - name: 'Payload Drop', - layout: PayloadDrop as React.ElementType, -}, { - name: 'UUV Rescue', - layout: UUVRescue as React.ElementType, -}]; +const layouts: { [key: string]: MissionLayout[] } = { + land: [ISRSearch, VTOLSearch, PayloadDrop, UGVRescue], + underwater: [ISRSearch, VTOLSearch, PayloadDrop, UUVRescue], +}; interface State { + /** + * Land missionType means we are performing UGV mission. + * Underwater missionType means we are performing UUV mission. + */ missionType: 'land' | 'underwater'; + + /** + * True if user should need to confirm before starting next mission. + */ requireConfirmation: boolean; + + /** + * First mission to perform. + */ startMissionIndex: number; + + /** + * Last mission to perform. + */ endMissionIndex: number; + + /** + * Current status of mission. All statuses will apply to mission except for + * "waiting", "error", and "disconnected". + * + * A little different since when the first mission finishes, the status will go + * to "next" instead of back to ready. From "next", it goes to "running", etc... + */ + status: VehicleStatus | 'next'; + + /** + * Information passed to Orchestrator for Mission to start. + */ information: { - landMission: MissionInformation.Information[]; - underwaterMission: MissionInformation.Information[]; + land: MissionInformation.Information[]; + underwater: MissionInformation.Information[]; }; } @@ -72,9 +84,10 @@ export default class MissionWindow extends Component { requireConfirmation: true, startMissionIndex: 0, endMissionIndex: 0, + status: 'ready', information: { - landMission: [], - underwaterMission: [], + land: [], + underwater: [], }, }; @@ -100,10 +113,8 @@ export default class MissionWindow extends Component { information, } = this.state; - const missionInformation = missionType === 'land' ? information.landMission : information.underwaterMission; - ipc.postStartMissions( - missionInformation.slice(startMissionIndex, endMissionIndex + 1), + information[missionType].slice(startMissionIndex, endMissionIndex + 1), requireConfirmation, ); } @@ -116,17 +127,27 @@ export default class MissionWindow extends Component { private tipFormatter(value: number): string { const { missionType } = this.state; - return missionType === 'land' ? layoutsLandMission[value].name : layoutsUnderwaterMission[value].name; + return layouts[missionType][value].name; } public render(): ReactNode { const { theme } = this.props; - const { missionType, startMissionIndex } = this.state; + const { + information, + missionType, + status, + startMissionIndex, + } = this.state; const missionTypeText = missionType === 'land' ? 'Land Missions' : 'Underwater Missions'; - const layouts = missionType === 'land' ? layoutsLandMission : layoutsUnderwaterMission; - const Layout = layouts[startMissionIndex].layout; + const Layout = layouts[missionType][startMissionIndex].layout; + + /* + * Start button will not appear unless all mission information is filled out + * (and no mission is running). + */ + const readyToStart = information[missionType][startMissionIndex] !== undefined; return (
@@ -135,7 +156,7 @@ export default class MissionWindow extends Component { @@ -144,7 +165,11 @@ export default class MissionWindow extends Component {
- + {status === 'ready' && } + {status !== 'ready' && } + {status === 'running' && } + {status === 'paused' && } + {status === 'next' && }
); diff --git a/src/renderer/missionWindow/mission.css b/src/renderer/missionWindow/mission.css index 0f8ab08a..fb9a91a5 100644 --- a/src/renderer/missionWindow/mission.css +++ b/src/renderer/missionWindow/mission.css @@ -14,6 +14,7 @@ .selectorContainer { grid-area: selector; + margin: auto; } .infoContainer { @@ -22,6 +23,7 @@ .buttonContainer { grid-area: button; + margin: auto; } /* Light theme */ @@ -39,7 +41,8 @@ } .selectorButton { - margin-right: 10px; + min-width: 150px; + margin-right: 20px; } .selectorSlider, diff --git a/src/renderer/missionWindow/parameters/ISRSearch.tsx b/src/renderer/missionWindow/parameters/ISRSearch.tsx index e793bfcd..b63ddfb3 100644 --- a/src/renderer/missionWindow/parameters/ISRSearch.tsx +++ b/src/renderer/missionWindow/parameters/ISRSearch.tsx @@ -1,8 +1,17 @@ import React, { ReactNode } from 'react'; -// import { jobTypes } from '../../../common/missions/ISRSearch'; -export default function ISRSearch(): ReactNode { +import { jobTypes } from '../../../common/missions/ISRSearch'; + +export const name = 'ISR Search'; + +export function ISRSearch(): ReactNode { return (
ISR Search
); } + +export default { + name, + jobTypes, + layout: ISRSearch as React.ElementType, +}; diff --git a/src/renderer/missionWindow/parameters/PayloadDrop.tsx b/src/renderer/missionWindow/parameters/PayloadDrop.tsx index d429d072..b1844255 100644 --- a/src/renderer/missionWindow/parameters/PayloadDrop.tsx +++ b/src/renderer/missionWindow/parameters/PayloadDrop.tsx @@ -1,8 +1,17 @@ import React, { ReactNode } from 'react'; -// import { jobTypes } from '../../../common/missions/PayloadDrop'; -export default function PayloadDrop(): ReactNode { +import { jobTypes } from '../../../common/missions/PayloadDrop'; + +export const name = 'Payload Drop'; + +export function PayloadDrop(): ReactNode { return (
Payload Drop
); } + +export default { + name, + jobTypes, + layout: PayloadDrop as React.ElementType, +}; diff --git a/src/renderer/missionWindow/parameters/UGVRescue.tsx b/src/renderer/missionWindow/parameters/UGVRescue.tsx index 7bd2fa7f..6b5415fc 100644 --- a/src/renderer/missionWindow/parameters/UGVRescue.tsx +++ b/src/renderer/missionWindow/parameters/UGVRescue.tsx @@ -1,8 +1,17 @@ import React, { ReactNode } from 'react'; -// import { jobTypes } from '../../../common/missions/UGVRescue'; -export default function UGVRescue(): ReactNode { +import { jobTypes } from '../../../common/missions/UGVRescue'; + +export const name = 'UGV Rescue'; + +export function UGVRescue(): ReactNode { return (
UGV Rescue
); } + +export default { + name, + jobTypes, + layout: UGVRescue as React.ElementType, +}; diff --git a/src/renderer/missionWindow/parameters/UUVRescue.tsx b/src/renderer/missionWindow/parameters/UUVRescue.tsx index 7f2b2d59..acef9959 100644 --- a/src/renderer/missionWindow/parameters/UUVRescue.tsx +++ b/src/renderer/missionWindow/parameters/UUVRescue.tsx @@ -1,8 +1,17 @@ import React, { ReactNode } from 'react'; -// import { jobTypes } from '../../../common/missions/UUVRescue'; -export default function UUVRescue(): ReactNode { +import { jobTypes } from '../../../common/missions/UUVRescue'; + +export const name = 'UUV Rescue'; + +export function UUVRescue(): ReactNode { return (
UUV Rescue
); } + +export default { + name, + jobTypes, + layout: UUVRescue as React.ElementType, +}; diff --git a/src/renderer/missionWindow/parameters/VTOLSearch.tsx b/src/renderer/missionWindow/parameters/VTOLSearch.tsx index b0c03ba2..b8684633 100644 --- a/src/renderer/missionWindow/parameters/VTOLSearch.tsx +++ b/src/renderer/missionWindow/parameters/VTOLSearch.tsx @@ -1,8 +1,17 @@ import React, { ReactNode } from 'react'; -// import { jobTypes } from '../../../common/missions/VTOLSearch'; -export default function VTOLSearch(): ReactNode { +import { jobTypes } from '../../../common/missions/VTOLSearch'; + +export const name = 'VTOL Search'; + +export function VTOLSearch(): ReactNode { return (
VTOL Search
); } + +export default { + name, + jobTypes, + layout: VTOLSearch as React.ElementType, +}; From d28f0a54a198c6edd4b4dd128f7f22532effdb41 Mon Sep 17 00:00:00 2001 From: Paul Matthew Barrameda Date: Wed, 24 Apr 2019 00:27:58 -0700 Subject: [PATCH 116/152] Added pause/resume logic + cache information Cache information is to store the information when user puts information to the Mission UI but the user changes window afterwards. We do not want the information to be lost and make the user reinput the information. Not sure if having a cache will work, but if it doesnt, will remove. --- src/common/Orchestrator.ts | 30 ++++++++++++++++-- src/renderer/missionWindow/MissionWindow.tsx | 32 ++++++++++++++++---- src/util/ipc.ts | 24 +++++++++++++++ 3 files changed, 78 insertions(+), 8 deletions(-) diff --git a/src/common/Orchestrator.ts b/src/common/Orchestrator.ts index 2e05fa73..31117b1e 100644 --- a/src/common/Orchestrator.ts +++ b/src/common/Orchestrator.ts @@ -81,8 +81,8 @@ class Orchestrator { missions: MissionInformation.Information[], requireConfirmation: boolean, ): void => this.startMissions(missions, requireConfirmation)); - - + ipcRenderer.on('pauseMission', this.pauseMission); + ipcRenderer.on('resumeMission', this.resumeMission); ipcRenderer.on('startNextMission', this.startNextMission); ipcRenderer.on('completeMission', (_: Event, missionName: string, completionParameters: Task.TaskParameters[]): void => this.completeMission(missionName, completionParameters)); ipcRenderer.on('stopMissions', this.stopMissions); @@ -296,6 +296,32 @@ class Orchestrator { this.startMission(); } + /** + * Sends pause message to all vehicles (regardless if they're running a mission or not). + */ + private pauseMission(): void { + if (!this.running) return; + + Object.values(this.vehicles).forEach((vehicle): void => { + ipc.postSendMessage(vehicle.getVehicleId(), { + type: 'pause', + }); + }); + } + + /** + * Sends pause message to all vehicles (regardless if they're running a mission or not). + */ + private resumeMission(): void { + if (!this.running) return; + + Object.values(this.vehicles).forEach((vehicle): void => { + ipc.postSendMessage(vehicle.getVehicleId(), { + type: 'resume', + }); + }); + } + /** * Happens when a mission completes. Either starts the next mission automatically, * ask user for confirmation before starting next mission, or ends missions. diff --git a/src/renderer/missionWindow/MissionWindow.tsx b/src/renderer/missionWindow/MissionWindow.tsx index 7fe6cfe5..94d8f49b 100644 --- a/src/renderer/missionWindow/MissionWindow.tsx +++ b/src/renderer/missionWindow/MissionWindow.tsx @@ -1,5 +1,6 @@ /* eslint-disable import/no-named-as-default */ +import { ipcRenderer } from 'electron'; import React, { Component, ReactNode } from 'react'; import * as Slider from 'rc-slider'; @@ -70,6 +71,15 @@ interface State { land: MissionInformation.Information[]; underwater: MissionInformation.Information[]; }; + + /** + * Information passed from missions as their information gets filled up. This is to + * prevent old information from being lost when user switches start mission. + */ + informationCache: { + land: MissionInformation.Information[]; + underwater: MissionInformation.Information[]; + }; } /** @@ -89,6 +99,10 @@ export default class MissionWindow extends Component { land: [], underwater: [], }, + informationCache: { + land: [], + underwater: [], + }, }; this.onSliderChange = this.onSliderChange.bind(this); @@ -97,6 +111,11 @@ export default class MissionWindow extends Component { this.tipFormatter = this.tipFormatter.bind(this); } + public componentDidMount(): void { + ipcRenderer.on('confirmCompleteMission', (): void => { this.setState({ status: 'next' }); }); + ipcRenderer.on('stopMissions', (): void => { this.setState({ status: 'ready' }); }); + } + private onSliderChange(value: [number, number]): void { this.setState({ startMissionIndex: value[0], @@ -134,6 +153,7 @@ export default class MissionWindow extends Component { const { theme } = this.props; const { information, + informationCache, missionType, status, startMissionIndex, @@ -162,14 +182,14 @@ export default class MissionWindow extends Component { />
- +
- {status === 'ready' && } - {status !== 'ready' && } - {status === 'running' && } - {status === 'paused' && } - {status === 'next' && } + {status === 'ready' && } + {status !== 'ready' && } + {status === 'running' && } + {status === 'paused' && } + {status === 'next' && }
); diff --git a/src/util/ipc.ts b/src/util/ipc.ts index 4a25a40b..17e267dd 100644 --- a/src/util/ipc.ts +++ b/src/util/ipc.ts @@ -179,6 +179,16 @@ function postLogMessages(...messages: LogMessage[]): void { ipcRenderer.send('post', 'logMessages', ...messages); } +/** + * Post "pauseMission" notification. + * + * Files that take this notification: + * - common/Orchestrator + */ +function postPauseMission(): void { + ipcRenderer.send('post', 'pauseMission'); +} + /** * Post "receiveMessage" notification. * @@ -190,6 +200,16 @@ function postReceiveMessage(message: any): void { ipcRenderer.send('post', 'receiveMessage', message); } +/** + * Post "resumeMission" notification. + * + * Files that take this notification: + * - common/Orchestrator + */ +function postResumeMission(): void { + ipcRenderer.send('post', 'resumeMission'); +} + /** * Post "saveConfig" notification. * @@ -265,6 +285,7 @@ function postStartMissions( * * Files that take this notification: * - common/Orchestrator + * - renderer/missionWindow/MissionWindow */ function postStartNextMission(): void { ipcRenderer.send('post', 'startNextMission'); @@ -275,6 +296,7 @@ function postStartNextMission(): void { * * Files that take this notification: * - common/Orchestrator + * - renderer/missionWindow/MissionWindow */ function postStopMissions(): void { ipcRenderer.send('post', 'stopMissions'); @@ -383,7 +405,9 @@ export default { postHideMissionWindow, postLoadConfig, postLogMessages, + postPauseMission, postReceiveMessage, + postResumeMission, postSaveConfig, postSendMessage, postSetMapToUserLocation, From 7225b9a8c9a9648de3ecf821ddbb49a79d04a39d Mon Sep 17 00:00:00 2001 From: Paul Matthew Barrameda Date: Wed, 24 Apr 2019 04:51:59 -0700 Subject: [PATCH 117/152] Progress for the night Test will fail but I need sleep so I dont really care at this point lol --- src/common/Orchestrator.ts | 23 +- src/main/index.ts | 4 +- .../fixtures/updateBoundingBoxesFixtures.ts | 4 +- .../fixtures/updateWaypointsFixtures.ts | 5 +- src/renderer/mainWindow/map/MapContainer.tsx | 82 +++++- .../mainWindow/map/marker/WaypointMarker.tsx | 2 +- src/renderer/missionWindow/MissionWindow.tsx | 83 ++++-- src/renderer/missionWindow/mission.css | 15 +- .../missionWindow/parameters/ISRSearch.tsx | 274 +++++++++++++++++- .../missionWindow/parameters/PayloadDrop.tsx | 3 +- .../missionWindow/parameters/UGVRescue.tsx | 3 +- .../missionWindow/parameters/UUVRescue.tsx | 3 +- .../missionWindow/parameters/VTOLSearch.tsx | 3 +- src/static/config.json | 2 +- src/types/missionInformation.ts | 7 - src/util/ipc.ts | 40 ++- 16 files changed, 475 insertions(+), 78 deletions(-) diff --git a/src/common/Orchestrator.ts b/src/common/Orchestrator.ts index 31117b1e..d9bf3d14 100644 --- a/src/common/Orchestrator.ts +++ b/src/common/Orchestrator.ts @@ -1,6 +1,11 @@ import { ipcRenderer, Event } from 'electron'; -import { config, vehicleConfig, VehicleInfo } from '../static/index'; +import { + config, + JobType, + vehicleConfig, + VehicleInfo, +} from '../static/index'; import * as Message from '../types/message'; import * as MissionInformation from '../types/missionInformation'; @@ -64,8 +69,16 @@ class Orchestrator { */ private currentMission: Mission | null = null; + /** + * All vehicles that have connected to the GCS. + */ private vehicles: { [vehicleId: number]: Vehicle } = {}; + /** + * Used for a mission to process which vehicle is doing which job. + */ + private activeVehicleMapping: { [vehicleId: number]: JobType } = {}; + public constructor() { ipcRenderer.on('connectToVehicle', (_: Event, jsonMessage: Message.JSONMessage, newMessage: boolean): void => this.connectToVehicle(jsonMessage, newMessage)); ipcRenderer.on('disconnectFromVehicle', (_: Event, vehicleId: number): void => this.disconnectFromVehicle(vehicleId)); @@ -79,8 +92,9 @@ class Orchestrator { ipcRenderer.on('startMissions', ( _: Event, missions: MissionInformation.Information[], + activeVehicleMapping: { [vehicleId: number]: JobType }, requireConfirmation: boolean, - ): void => this.startMissions(missions, requireConfirmation)); + ): void => this.startMissions(missions, activeVehicleMapping, requireConfirmation)); ipcRenderer.on('pauseMission', this.pauseMission); ipcRenderer.on('resumeMission', this.resumeMission); ipcRenderer.on('startNextMission', this.startNextMission); @@ -276,6 +290,7 @@ class Orchestrator { */ private startMissions( missions: MissionInformation.Information[], + activeVehicleMapping: { [vehicleId: number]: JobType }, requireConfirmation: boolean, ): void { if (this.running) { @@ -290,6 +305,7 @@ class Orchestrator { this.running = true; this.missions = missions; + this.activeVehicleMapping = activeVehicleMapping; this.requireConfirmation = requireConfirmation; this.currentMissionIndex = 0; @@ -383,7 +399,7 @@ class Orchestrator { this.currentMission = new mission.constructor( this.vehicles, this.missions[this.currentMissionIndex], - this.missions[this.currentMissionIndex].activeVehicleMapping, + this.activeVehicleMapping, ); } @@ -397,6 +413,7 @@ class Orchestrator { this.missions = []; this.running = false; this.requireConfirmation = false; + this.activeVehicleMapping = {}; } } diff --git a/src/main/index.ts b/src/main/index.ts index 0273afcf..e54c45bc 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -219,8 +219,8 @@ function createMissionWindow(): void { title: 'NGCP Mission User Interface', icon, show: false, - width: Math.floor(WIDTH * 2 / 3), - minWidth: Math.floor(WIDTH * 2 / 3), + width: Math.floor(WIDTH * 2.5 / 3), + minWidth: Math.floor(WIDTH * 2.5 / 3), height: HEIGHT, autoHideMenuBar: true, minHeight: HEIGHT, diff --git a/src/renderer/fixtures/updateBoundingBoxesFixtures.ts b/src/renderer/fixtures/updateBoundingBoxesFixtures.ts index 77ab523e..cf783fc7 100644 --- a/src/renderer/fixtures/updateBoundingBoxesFixtures.ts +++ b/src/renderer/fixtures/updateBoundingBoxesFixtures.ts @@ -6,7 +6,7 @@ const { lat, lng } = locationConfig.startLocation; const boundingBox = { name: 'Test Bounding Box', - color: 'red', + color: 'blue', bounds: { top: lat, bottom: lat - 0.001, @@ -15,4 +15,4 @@ const boundingBox = { }, }; -ipc.postUpdateBoundingBoxes(true, boundingBox); +ipc.postCreateBoundingBoxes(boundingBox); diff --git a/src/renderer/fixtures/updateWaypointsFixtures.ts b/src/renderer/fixtures/updateWaypointsFixtures.ts index eb629bed..54b1d6d3 100644 --- a/src/renderer/fixtures/updateWaypointsFixtures.ts +++ b/src/renderer/fixtures/updateWaypointsFixtures.ts @@ -1,10 +1,7 @@ -import { locationConfig } from '../../static/index'; - import ipc from '../../util/ipc'; const waypoint = { name: 'Test Marker', - location: locationConfig.startLocation, }; -ipc.postUpdateWaypoint(true, waypoint); +ipc.postCreateWaypoints(waypoint); diff --git a/src/renderer/mainWindow/map/MapContainer.tsx b/src/renderer/mainWindow/map/MapContainer.tsx index 4f1ce9cd..22dc647b 100644 --- a/src/renderer/mainWindow/map/MapContainer.tsx +++ b/src/renderer/mainWindow/map/MapContainer.tsx @@ -14,6 +14,7 @@ import { BoundingBoxBounds, ThemeProps } from '../../../types/componentStyle'; import { FileLoadOptions, FileSaveOptions } from '../../../types/fileOption'; import { VehicleObject } from '../../../types/vehicle'; +import ipc from '../../../util/ipc'; import { updateVehicles } from '../../../util/util'; import GeolocationControl from './control/GeolocationControl'; @@ -116,7 +117,9 @@ export default class MapContainer extends Component { this.loadConfig = this.loadConfig.bind(this); this.saveConfig = this.saveConfig.bind(this); this.centerMapToVehicle = this.centerMapToVehicle.bind(this); + this.createWaypoints = this.createWaypoints.bind(this); this.updateWaypoints = this.updateWaypoints.bind(this); + this.createBoundingBoxes = this.createBoundingBoxes.bind(this); this.updateBoundingBoxes = this.updateBoundingBoxes.bind(this); } @@ -128,8 +131,11 @@ export default class MapContainer extends Component { ipcRenderer.on('setMapToUserLocation', this.setMapToUserLocation); ipcRenderer.on('updateMapLocation', (_: Event, location: Location): void => this.updateMapLocation(location)); ipcRenderer.on('updateVehicles', (_: Event, ...vehicles: VehicleObject[]): void => updateVehicles(this, ...vehicles)); + + ipcRenderer.on('createWaypoints', (_: Event, ...waypoints: { name: string; location: Location }[]): void => this.createWaypoints(...waypoints)); ipcRenderer.on('updateWaypoints', (_: Event, updateMap: boolean, ...waypoints: { name: string; location: Location }[]): void => this.updateWaypoints(updateMap, ...waypoints)); - ipcRenderer.on('updateBoundingBoxes', (_: Event, updateMap: boolean, ...boundingBoxes: { name: string; bounds: BoundingBoxBounds}[]): void => this.updateBoundingBoxes(updateMap, ...boundingBoxes)); + ipcRenderer.on('createBoundingBoxes', (_: Event, ...boundingBoxes: { name: string; color?: string; bounds: BoundingBoxBounds}[]): void => this.createBoundingBoxes(...boundingBoxes)); + ipcRenderer.on('updateBoundingBoxes', (_: Event, updateMap: boolean, ...boundingBoxes: { name: string; color?: string; bounds: BoundingBoxBounds}[]): void => this.updateBoundingBoxes(updateMap, ...boundingBoxes)); } /** @@ -201,6 +207,30 @@ export default class MapContainer extends Component { this.updateMapLocation(vehicle); } + /** + * Creates waypoints. + */ + private createWaypoints(...waypoints: { name: string; location?: Location }[]): void { + const { viewport, waypoints: currentWaypoints } = this.state; + const newWaypoints = currentWaypoints; + + waypoints.forEach((waypoint): void => { + if (!newWaypoints[waypoint.name]) { + newWaypoints[waypoint.name] = { + locked: false, + location: waypoint.location || (viewport.center && { + lat: viewport.center[0], + lng: viewport.center[1], + }) || locationConfig.startLocation, + }; + + ipc.postUpdateWaypoints(false, { name: waypoint.name, ...newWaypoints[waypoint.name] }); + } + }); + + this.setState({ waypoints: newWaypoints }); + } + /** * Updates waypoints. */ @@ -212,9 +242,7 @@ export default class MapContainer extends Component { const newWaypoints = currentWaypoints; waypoints.forEach((waypoint): void => { - if (!newWaypoints[waypoint.name]) { - newWaypoints[waypoint.name] = { location: waypoint.location, locked: false }; - } else if (updateMap) { + if (newWaypoints[waypoint.name] && updateMap) { newWaypoints[waypoint.name].location = waypoint.location; } }); @@ -222,6 +250,42 @@ export default class MapContainer extends Component { this.setState({ waypoints: newWaypoints }); } + /** + * Creates bounding boxes. + */ + private createBoundingBoxes( + ...boundingBoxes: { + name: string; + bounds?: BoundingBoxBounds; + color?: string; + }[] + ): void { + const { boundingBoxes: currentBoundingBoxes, viewport } = this.state; + const newBoundingBoxes = currentBoundingBoxes; + + boundingBoxes.forEach((boundingBox): void => { + if (!newBoundingBoxes[boundingBox.name]) { + newBoundingBoxes[boundingBox.name] = { + color: boundingBox.color || 'red', + bounds: boundingBox.bounds || (viewport.center && { + top: viewport.center[0], + bottom: viewport.center[0], + left: viewport.center[1], + right: viewport.center[1], + }) || { + top: locationConfig.startLocation.lat, + bottom: locationConfig.startLocation.lat, + left: locationConfig.startLocation.lng, + right: locationConfig.startLocation.lng, + }, + locked: false, + }; + } + }); + + this.setState({ boundingBoxes: newBoundingBoxes }); + } + /** * Updates bounding boxes. */ @@ -231,21 +295,15 @@ export default class MapContainer extends Component { name: string; bounds: BoundingBoxBounds; color?: string; - locked?: boolean; }[] ): void { const { boundingBoxes: currentBoundingBoxes } = this.state; const newBoundingBoxes = currentBoundingBoxes; boundingBoxes.forEach((boundingBox): void => { - if (!newBoundingBoxes[boundingBox.name]) { - newBoundingBoxes[boundingBox.name] = { - color: boundingBox.color || '#000', - bounds: boundingBox.bounds, - locked: boundingBox.locked || false, - }; - } else if (updateMap) { + if (newBoundingBoxes[boundingBox.name] && updateMap) { newBoundingBoxes[boundingBox.name].bounds = boundingBox.bounds; + if (boundingBox.color) newBoundingBoxes[boundingBox.name].color = boundingBox.color; } }); diff --git a/src/renderer/mainWindow/map/marker/WaypointMarker.tsx b/src/renderer/mainWindow/map/marker/WaypointMarker.tsx index 948c71c6..ef9f46ea 100644 --- a/src/renderer/mainWindow/map/marker/WaypointMarker.tsx +++ b/src/renderer/mainWindow/map/marker/WaypointMarker.tsx @@ -35,7 +35,7 @@ export default class WaypointMarker extends PureComponent { private ondrag(event: Leaflet.LeafletMouseEvent): void { const { name } = this.props; - ipc.postUpdateWaypoint(false, { name, location: event.latlng }); + ipc.postUpdateWaypoints(false, { name, location: event.latlng }); } public render(): ReactNode { diff --git a/src/renderer/missionWindow/MissionWindow.tsx b/src/renderer/missionWindow/MissionWindow.tsx index 94d8f49b..85112711 100644 --- a/src/renderer/missionWindow/MissionWindow.tsx +++ b/src/renderer/missionWindow/MissionWindow.tsx @@ -8,7 +8,9 @@ import { JobType } from '../../static/index'; import { ThemeProps } from '../../types/componentStyle'; import * as MissionInformation from '../../types/missionInformation'; -import { VehicleStatus } from '../../types/vehicle'; +import { VehicleObject, VehicleStatus } from '../../types/vehicle'; + +import { updateVehicles } from '../../util/util'; import ISRSearch from './parameters/ISRSearch'; import PayloadDrop from './parameters/PayloadDrop'; @@ -24,6 +26,7 @@ const Range = Slider.createSliderWithTooltip(Slider.Range); interface MissionLayout { name: string; + missionName: string; jobTypes: JobType[]; layout: React.ElementType; } @@ -65,21 +68,19 @@ interface State { status: VehicleStatus | 'next'; /** - * Information passed to Orchestrator for Mission to start. + * Current vehicles connected. */ - information: { - land: MissionInformation.Information[]; - underwater: MissionInformation.Information[]; - }; + vehicles: { [vehicleId: number]: VehicleObject }; /** - * Information passed from missions as their information gets filled up. This is to - * prevent old information from being lost when user switches start mission. + * Mapping for all missions selected. */ - informationCache: { - land: MissionInformation.Information[]; - underwater: MissionInformation.Information[]; - }; + activeVehicleMapping: { [vehicleId: number]: JobType }; + + /** + * Information passed to Orchestrator for Mission to start. + */ + information: { [missionName: string]: undefined | MissionInformation.Information }; } /** @@ -95,23 +96,22 @@ export default class MissionWindow extends Component { startMissionIndex: 0, endMissionIndex: 0, status: 'ready', - information: { - land: [], - underwater: [], - }, - informationCache: { - land: [], - underwater: [], - }, + vehicles: {}, + activeVehicleMapping: {}, + information: {}, }; this.onSliderChange = this.onSliderChange.bind(this); + this.updateInformation = this.updateInformation.bind(this); this.postStartMissions = this.postStartMissions.bind(this); this.toggleMissionType = this.toggleMissionType.bind(this); this.tipFormatter = this.tipFormatter.bind(this); } public componentDidMount(): void { + ipcRenderer.on('updateVehicles', (_: Event, ...vehicles: VehicleObject[]): void => updateVehicles(this, ...vehicles)); + ipcRenderer.on('updateInformation', (_event: Event, information: MissionInformation.Information): void => this.updateInformation(information)); + ipcRenderer.on('confirmCompleteMission', (): void => { this.setState({ status: 'next' }); }); ipcRenderer.on('stopMissions', (): void => { this.setState({ status: 'ready' }); }); } @@ -123,6 +123,14 @@ export default class MissionWindow extends Component { }); } + private updateInformation(information: MissionInformation.Information): void { + const { information: currentInformation } = this.state; + const newInformation = currentInformation; + + newInformation[information.missionName] = information; + this.setState({ information: newInformation }); + } + private postStartMissions(): void { const { missionType, @@ -132,10 +140,16 @@ export default class MissionWindow extends Component { information, } = this.state; - ipc.postStartMissions( - information[missionType].slice(startMissionIndex, endMissionIndex + 1), - requireConfirmation, - ); + // Gets all relevant mission information for the missions being performed for the mission type. + const missionInformation = layouts[missionType].slice(startMissionIndex, endMissionIndex + 1) + .map(({ missionName }): MissionInformation.Information => { + if (!information[missionName]) { + throw new Error(`Undefined mission information for ${missionName}`); + } + return information[missionName] as MissionInformation.Information; + }); + + ipc.postStartMissions(missionInformation, requireConfirmation); } private toggleMissionType(): void { @@ -153,21 +167,29 @@ export default class MissionWindow extends Component { const { theme } = this.props; const { information, - informationCache, missionType, status, startMissionIndex, + endMissionIndex, } = this.state; + // Text displayed on toggle button. const missionTypeText = missionType === 'land' ? 'Land Missions' : 'Underwater Missions'; - const Layout = layouts[missionType][startMissionIndex].layout; + /* + * Used to render mission UI layout as well as determine which mission layout + * corresponds to which mission information. + */ + const { layout: Layout, missionName } = layouts[missionType][startMissionIndex]; + + // Used to render all the required jobs and options. + const selectedLayouts = layouts[missionType].slice(startMissionIndex, endMissionIndex + 1); /* * Start button will not appear unless all mission information is filled out * (and no mission is running). */ - const readyToStart = information[missionType][startMissionIndex] !== undefined; + const readyToStart = information[missionName] !== undefined; return (
@@ -181,8 +203,11 @@ export default class MissionWindow extends Component { tipFormatter={this.tipFormatter} />
-
- +
+ {status === 'ready' && } + {status !== 'ready' &&

:)

} +
+
{status === 'ready' && } diff --git a/src/renderer/missionWindow/mission.css b/src/renderer/missionWindow/mission.css index fb9a91a5..929bf8ef 100644 --- a/src/renderer/missionWindow/mission.css +++ b/src/renderer/missionWindow/mission.css @@ -6,10 +6,11 @@ height: 100%; width: 100; grid-template-areas: - 'selector' - 'info' - 'button'; + 'selector selector selector selector' + 'parameter parameter parameter mapping' + 'button button button button'; grid-template-rows: 25px auto 25px; + grid-template-columns: repeat(4, 1fr); } .selectorContainer { @@ -17,8 +18,12 @@ margin: auto; } -.infoContainer { - grid-area: info; +.parameterContainer { + grid-area: parameter; +} + +.mappingContainer { + grid-area: mapping; } .buttonContainer { diff --git a/src/renderer/missionWindow/parameters/ISRSearch.tsx b/src/renderer/missionWindow/parameters/ISRSearch.tsx index b63ddfb3..0be2f44b 100644 --- a/src/renderer/missionWindow/parameters/ISRSearch.tsx +++ b/src/renderer/missionWindow/parameters/ISRSearch.tsx @@ -1,17 +1,279 @@ -import React, { ReactNode } from 'react'; +import { Event, ipcRenderer } from 'electron'; +import React, { Component, ReactNode } from 'react'; -import { jobTypes } from '../../../common/missions/ISRSearch'; +import { missionName, jobTypes } from '../../../common/missions/ISRSearch'; + +import { Location } from '../../../static/index'; + +import { ISRSearchInformation } from '../../../types/missionInformation'; +import { VehicleObject } from '../../../types/vehicle'; + +import ipc from '../../../util/ipc'; export const name = 'ISR Search'; -export function ISRSearch(): ReactNode { - return ( -
ISR Search
- ); +const information: ISRSearchInformation = { + missionName: 'isrSearch', + parameters: { + takeoff: { + lat: 0, + lng: 0, + alt: 0, + loiter: { + lat: 0, + lng: 0, + alt: 0, + radius: 0, + direction: 0, + }, + }, + isrSearch: { + alt: 0, + waypoints: [ + { + lat: 0, + lng: 0, + }, + { + lat: 0, + lng: 0, + }, + { + lat: 0, + lng: 0, + }, + ], + }, + land: { + waypoints: [ + { + lat: 0, + lng: 0, + alt: 0, + }, + { + lat: 0, + lng: 0, + alt: 0, + }, + ], + }, + }, + // TODO: Move options as a separate entity for Orchestrator to take to make Mission UI easier. + options: { + noTakeoff: false, + noLand: false, + }, +}; + +const checklist: { [check: string]: number | undefined } = { + // Parameter checks. + takeoffLat: undefined, + takeoffLng: undefined, + takeoffAlt: undefined, + loiterLat: undefined, + loiterLng: undefined, + loiterRadius: undefined, + loiterDirection: undefined, + isrSearchAlt: undefined, + isrSearchLat1: undefined, + isrSearchLng1: undefined, + isrSearchLat2: undefined, + isrSearchLng2: undefined, + isrSearchLat3: undefined, + isrSearchLng3: undefined, + landLat1: undefined, + landLng1: undefined, + landAlt1: undefined, + landLat2: undefined, + landLng2: undefined, + landAlt2: undefined, +}; + +// eslint-disable-next-line @typescript-eslint/interface-name-prefix +export interface ISRSearchProps { + vehicles: { [vehicleId: number]: VehicleObject }; +} + +export class ISRSearch extends Component { + private static onChange(event: React.ChangeEvent): void { + ISRSearch.updateChecklist({ + [event.target.name]: parseInt(event.target.value, 10), + }); + } + + private static updateWaypoints(...waypoints: { name: string; location: Location }[]): void { + const checks: { [checkName: string]: number } = {}; + + waypoints.forEach((waypoint): void => { + switch (waypoint.name) { + case 'Takeoff': + checks.takeoffLat = waypoint.location.lat; + checks.takeoffLng = waypoint.location.lng; + break; + + case 'Loiter': + checks.loiterLat = waypoint.location.lat; + checks.loiterLng = waypoint.location.lng; + break; + + case 'ISR Search 1': + checks.isrSearchLat1 = waypoint.location.lat; + checks.isrSearchLng1 = waypoint.location.lng; + break; + + case 'ISR Search 2': + checks.isrSearchLat2 = waypoint.location.lat; + checks.isrSearchLng2 = waypoint.location.lng; + break; + + case 'ISR Search 3': + checks.isrSearchLat3 = waypoint.location.lat; + checks.isrSearchLng4 = waypoint.location.lng; + break; + + case 'Land 1': + checks.landLat1 = waypoint.location.lat; + checks.landLng1 = waypoint.location.lng; + break; + + case 'Land 2': + checks.landLat2 = waypoint.location.lat; + checks.landLng2 = waypoint.location.lng; + break; + + default: break; + } + }); + + ISRSearch.updateChecklist(checks); + } + + private static updateChecklist(checks: { [checkName: string]: number }): void { + Object.keys(checks).forEach((checkName): void => { + const value = checks[checkName]; + /* + * Following check is to ensure that we have all the "name" fields in the + * form correctly put in. Delete the following check when we ensure it works. + */ + if (!(checkName in checklist)) { + throw new RangeError('Wrong value provided'); + } + + checklist[checkName] = value; + }); + + if (ISRSearch.isReadyToStart) { + ipc.postUpdateInformation(information); + } + } + + private static isReadyToStart(): boolean { + return Object.values(checklist).every((value): boolean => value !== undefined); + } + + private static onClickTakeoff(): void { + ipc.postCreateWaypoints({ name: 'Takeoff' }); + } + + private static onClickLoiter(): void { + ipc.postCreateWaypoints({ name: 'Loiter' }); + } + + private static onClickSearch1(): void { + ipc.postCreateWaypoints({ name: 'ISR Search 1' }); + } + + private static onClickSearch2(): void { + ipc.postCreateWaypoints({ name: 'ISR Search 2' }); + } + + private static onClickSearch3(): void { + ipc.postCreateWaypoints({ name: 'ISR Search 3' }); + } + + private static onClickLand1(): void { + ipc.postCreateWaypoints({ name: 'Land 1' }); + } + + private static onClickLand2(): void { + ipc.postCreateWaypoints({ name: 'Land 2' }); + } + + public componentDidMount(): void { + ipcRenderer.on('updateWaypoints', (__: Event, _: boolean, ...waypoints: { name: string; location: Location }[]): void => ISRSearch.updateWaypoints(...waypoints)); + } + + public render(): ReactNode { + const { parameters } = information; + + return ( +
+

ISR Search

+ +

Takeoff Coordinates

+ + + +
+ + +

Loiter Coordinates

+ + + +
+ + + + +

ISR Search Waypoints

+ + + +
+ + + +
+ + + +
+ + +

Land Waypoints

+ + + +
+ +
+ + + +
+ + +
+ + +
+ +
+ ); + } } export default { name, + missionName, jobTypes, layout: ISRSearch as React.ElementType, }; diff --git a/src/renderer/missionWindow/parameters/PayloadDrop.tsx b/src/renderer/missionWindow/parameters/PayloadDrop.tsx index b1844255..5588b255 100644 --- a/src/renderer/missionWindow/parameters/PayloadDrop.tsx +++ b/src/renderer/missionWindow/parameters/PayloadDrop.tsx @@ -1,6 +1,6 @@ import React, { ReactNode } from 'react'; -import { jobTypes } from '../../../common/missions/PayloadDrop'; +import { missionName, jobTypes } from '../../../common/missions/PayloadDrop'; export const name = 'Payload Drop'; @@ -12,6 +12,7 @@ export function PayloadDrop(): ReactNode { export default { name, + missionName, jobTypes, layout: PayloadDrop as React.ElementType, }; diff --git a/src/renderer/missionWindow/parameters/UGVRescue.tsx b/src/renderer/missionWindow/parameters/UGVRescue.tsx index 6b5415fc..6c1418d6 100644 --- a/src/renderer/missionWindow/parameters/UGVRescue.tsx +++ b/src/renderer/missionWindow/parameters/UGVRescue.tsx @@ -1,6 +1,6 @@ import React, { ReactNode } from 'react'; -import { jobTypes } from '../../../common/missions/UGVRescue'; +import { missionName, jobTypes } from '../../../common/missions/UGVRescue'; export const name = 'UGV Rescue'; @@ -12,6 +12,7 @@ export function UGVRescue(): ReactNode { export default { name, + missionName, jobTypes, layout: UGVRescue as React.ElementType, }; diff --git a/src/renderer/missionWindow/parameters/UUVRescue.tsx b/src/renderer/missionWindow/parameters/UUVRescue.tsx index acef9959..59bbbb62 100644 --- a/src/renderer/missionWindow/parameters/UUVRescue.tsx +++ b/src/renderer/missionWindow/parameters/UUVRescue.tsx @@ -1,6 +1,6 @@ import React, { ReactNode } from 'react'; -import { jobTypes } from '../../../common/missions/UUVRescue'; +import { missionName, jobTypes } from '../../../common/missions/UUVRescue'; export const name = 'UUV Rescue'; @@ -12,6 +12,7 @@ export function UUVRescue(): ReactNode { export default { name, + missionName, jobTypes, layout: UUVRescue as React.ElementType, }; diff --git a/src/renderer/missionWindow/parameters/VTOLSearch.tsx b/src/renderer/missionWindow/parameters/VTOLSearch.tsx index b8684633..d5628724 100644 --- a/src/renderer/missionWindow/parameters/VTOLSearch.tsx +++ b/src/renderer/missionWindow/parameters/VTOLSearch.tsx @@ -1,6 +1,6 @@ import React, { ReactNode } from 'react'; -import { jobTypes } from '../../../common/missions/VTOLSearch'; +import { missionName, jobTypes } from '../../../common/missions/VTOLSearch'; export const name = 'VTOL Search'; @@ -12,6 +12,7 @@ export function VTOLSearch(): ReactNode { export default { name, + missionName, jobTypes, layout: VTOLSearch as React.ElementType, }; diff --git a/src/static/config.json b/src/static/config.json index 96e6ba2d..1781e843 100644 --- a/src/static/config.json +++ b/src/static/config.json @@ -1,5 +1,5 @@ { - "fixtures": true, + "fixtures": false, "geolocation": false, "vehicleDisconnectionTime": 20, diff --git a/src/types/missionInformation.ts b/src/types/missionInformation.ts index e8b926ba..15e11ecf 100644 --- a/src/types/missionInformation.ts +++ b/src/types/missionInformation.ts @@ -1,5 +1,3 @@ -import { JobType } from '../static/index'; - import * as Task from './task'; export type MissionName = 'isrSearch' | 'vtolSearch' | 'payloadDrop' | 'ugvRescue' | 'uuvRescue'; @@ -19,11 +17,6 @@ interface InformationBase { * Options for the mission. User generated. */ options: {}; - - /** - * Vehicle mapping for specific mission. User generated. - */ - activeVehicleMapping: { [vehicleId: number]: JobType }; } // eslint-disable-next-line @typescript-eslint/interface-name-prefix diff --git a/src/util/ipc.ts b/src/util/ipc.ts index 17e267dd..96494b81 100644 --- a/src/util/ipc.ts +++ b/src/util/ipc.ts @@ -67,6 +67,28 @@ function postConnectToVehicle(jsonMessage: Message.JSONMessage, newMessage: bool ipcRenderer.send('post', 'connectToVehicle', jsonMessage, newMessage); } +/** + * Post "createBoundingBoxes" notification. + * + * Files that take this notification: + * - renderer/mainWindow/map/MapContainer + */ +function postCreateBoundingBoxes( + ...boundingBoxes: { name: string; color?: string; bounds?: BoundingBoxBounds}[] +): void { + ipcRenderer.send('post', 'createBoundingBoxes', ...boundingBoxes); +} + +/** + * Post "createWaypoints" notification. + * + * Files that take this notification: + * - renderer/mainWindow/map/MapContainer + */ +function postCreateWaypoints(...waypoints: { name: string; location?: Location }[]): void { + ipcRenderer.send('post', 'createWaypoints', ...waypoints); +} + /** * Post "disconnectFromVehicle" notification. * @@ -345,6 +367,16 @@ function postUpdateBoundingBoxes( ipcRenderer.send('post', 'updateBoundingBoxes', updateMap, ...boundingBoxes); } +/** + * Post "updateInformation" notification. + * + * Files that take this notification: + * - renderer/missionWindow/MissionWindow + */ +function postUpdateInformation(information: Information): void { + ipcRenderer.send('post', 'updateInformation', information); +} + /** * Post "updateMapLocation" notification. * @@ -380,10 +412,11 @@ function postUpdateVehicles(...vehicles: VehicleObject[]): void { * * Files that take this notification: * - renderer/mainWindow/map/MapContainer + * - renderer/missionWindow/parameters/ISRSearch * * @param updateMap Set this to true, except from the marker's drag event. */ -function postUpdateWaypoint( +function postUpdateWaypoints( updateMap: boolean, ...waypoints: { name: string; location: Location }[] ): void { @@ -395,6 +428,8 @@ export default { postCompleteMission, postConfirmCompleteMission, postConnectToVehicle, + postCreateBoundingBoxes, + postCreateWaypoints, postDisconnectFromVehicle, postFinishMissions, postHandleAcknowledgementMessage, @@ -419,7 +454,8 @@ export default { postStopSendingMessages, postToggleTheme, postUpdateBoundingBoxes, + postUpdateInformation, postUpdateMapLocation, postUpdateVehicles, - postUpdateWaypoint, + postUpdateWaypoints, }; From 3de9fcc8c9cc2d7f64bcce33d7b4c486dc74dd37 Mon Sep 17 00:00:00 2001 From: Paul Matthew Barrameda Date: Thu, 25 Apr 2019 02:08:43 -0700 Subject: [PATCH 118/152] Finish Mission Window UI Logic Separated options and activeVehicleMapping from MissionInformation to make it easier to set up Mission Window UI. Added logic to set options. Added logic to set vehicle mapping. User can start mission whether or not vehicle mapping is done or not, but the Mission backend will throw an error and stop mission if vehicle mapping is not correct. And other stuff... Getting late so I need rest, will finish up tomorrow. --- src/common/MessageHandler.ts | 2 +- src/common/Orchestrator.ts | 26 ++-- src/common/missions/ISRSearch.ts | 11 +- src/common/missions/PayloadDrop.ts | 11 +- src/common/missions/UGVRescue.ts | 5 +- src/common/missions/UUVRescue.ts | 5 +- src/common/missions/VTOLSearch.ts | 11 +- src/common/missions/index.ts | 2 +- src/common/struct/Mission.ts | 43 ++++--- src/main/index.ts | 8 +- src/renderer/fixtures/index.ts | 6 +- src/renderer/form/Checkbox.tsx | 22 ++++ src/renderer/form/Select.tsx | 32 +++++ src/renderer/mainWindow/log/LogContainer.tsx | 30 +++-- src/renderer/missionWindow/MissionWindow.tsx | 119 +++++++++++++++--- .../mapping/ActiveVehicleMapping.tsx | 75 +++++++++++ .../missionWindow/mapping/MissionOptions.tsx | 88 +++++++++++++ src/renderer/missionWindow/mission.css | 7 +- .../{parameters => parameter}/ISRSearch.tsx | 21 +--- .../{parameters => parameter}/PayloadDrop.tsx | 4 +- .../{parameters => parameter}/UGVRescue.tsx | 4 +- .../{parameters => parameter}/UUVRescue.tsx | 4 +- .../{parameters => parameter}/VTOLSearch.tsx | 4 +- .../{parameters => parameter}/parameters.css | 0 src/static/config.json | 4 +- src/types/missionInformation.ts | 72 +++++++---- src/util/ipc.ts | 42 ++++++- 27 files changed, 509 insertions(+), 149 deletions(-) create mode 100644 src/renderer/form/Checkbox.tsx create mode 100644 src/renderer/form/Select.tsx create mode 100644 src/renderer/missionWindow/mapping/ActiveVehicleMapping.tsx create mode 100644 src/renderer/missionWindow/mapping/MissionOptions.tsx rename src/renderer/missionWindow/{parameters => parameter}/ISRSearch.tsx (94%) rename src/renderer/missionWindow/{parameters => parameter}/PayloadDrop.tsx (71%) rename src/renderer/missionWindow/{parameters => parameter}/UGVRescue.tsx (71%) rename src/renderer/missionWindow/{parameters => parameter}/UUVRescue.tsx (71%) rename src/renderer/missionWindow/{parameters => parameter}/VTOLSearch.tsx (71%) rename src/renderer/missionWindow/{parameters => parameter}/parameters.css (100%) diff --git a/src/common/MessageHandler.ts b/src/common/MessageHandler.ts index 0aa963f7..f5a1cab5 100644 --- a/src/common/MessageHandler.ts +++ b/src/common/MessageHandler.ts @@ -36,7 +36,7 @@ class MessageHandler { /** * Map of last message ID received from specific vehicle. */ - private receivedMessageId: { [key: string]: number | undefined } = {}; + private receivedMessageId: { [vehicleId: number]: number | undefined } = {}; public constructor() { ipcRenderer.on('sendMessage', (_: Event, vehicleId: number, message: Message.Message): void => this.sendMessage(vehicleId, message)); diff --git a/src/common/Orchestrator.ts b/src/common/Orchestrator.ts index d9bf3d14..cecca54b 100644 --- a/src/common/Orchestrator.ts +++ b/src/common/Orchestrator.ts @@ -2,7 +2,6 @@ import { ipcRenderer, Event } from 'electron'; import { config, - JobType, vehicleConfig, VehicleInfo, } from '../static/index'; @@ -77,7 +76,12 @@ class Orchestrator { /** * Used for a mission to process which vehicle is doing which job. */ - private activeVehicleMapping: { [vehicleId: number]: JobType } = {}; + private activeVehicleMapping: MissionInformation.ActiveVehicleMapping | null = null; + + /** + * Used for a mission to know options for specific missions. + */ + private options: MissionInformation.MissionOptions | null = null; public constructor() { ipcRenderer.on('connectToVehicle', (_: Event, jsonMessage: Message.JSONMessage, newMessage: boolean): void => this.connectToVehicle(jsonMessage, newMessage)); @@ -92,9 +96,10 @@ class Orchestrator { ipcRenderer.on('startMissions', ( _: Event, missions: MissionInformation.Information[], - activeVehicleMapping: { [vehicleId: number]: JobType }, + options: MissionInformation.MissionOptions, + activeVehicleMapping: MissionInformation.ActiveVehicleMapping, requireConfirmation: boolean, - ): void => this.startMissions(missions, activeVehicleMapping, requireConfirmation)); + ): void => this.startMissions(missions, activeVehicleMapping, options, requireConfirmation)); ipcRenderer.on('pauseMission', this.pauseMission); ipcRenderer.on('resumeMission', this.resumeMission); ipcRenderer.on('startNextMission', this.startNextMission); @@ -290,7 +295,8 @@ class Orchestrator { */ private startMissions( missions: MissionInformation.Information[], - activeVehicleMapping: { [vehicleId: number]: JobType }, + activeVehicleMapping: MissionInformation.ActiveVehicleMapping, + options: MissionInformation.MissionOptions, requireConfirmation: boolean, ): void { if (this.running) { @@ -306,6 +312,7 @@ class Orchestrator { this.running = true; this.missions = missions; this.activeVehicleMapping = activeVehicleMapping; + this.options = options; this.requireConfirmation = requireConfirmation; this.currentMissionIndex = 0; @@ -388,8 +395,9 @@ class Orchestrator { */ private startMission(): void { const missionObj = missionObject[this.missions[this.currentMissionIndex].missionName]; - if (!missionObj) { - Orchestrator.postOrchestratorError('Tried to start mission with unknown mission name'); + + if (!this.options || !this.activeVehicleMapping) { + Orchestrator.postOrchestratorError('Tried to start mission while options/mapping are null'); return; } @@ -400,6 +408,7 @@ class Orchestrator { this.vehicles, this.missions[this.currentMissionIndex], this.activeVehicleMapping, + this.options, ); } @@ -413,7 +422,8 @@ class Orchestrator { this.missions = []; this.running = false; this.requireConfirmation = false; - this.activeVehicleMapping = {}; + this.activeVehicleMapping = null; + this.options = null; } } diff --git a/src/common/missions/ISRSearch.ts b/src/common/missions/ISRSearch.ts index a814466b..4241cda3 100644 --- a/src/common/missions/ISRSearch.ts +++ b/src/common/missions/ISRSearch.ts @@ -33,9 +33,10 @@ export class ISRSearch extends Mission { public constructor( vehicles: { [vehicleId: number]: Vehicle }, information: MissionInformation.ISRSearchInformation, - activeVehicleMapping: { [vehicleId: number]: JobType }, + activeVehicleMapping: MissionInformation.ActiveVehicleMapping, + options: MissionInformation.MissionOptions, ) { - super(vehicles, information, activeVehicleMapping); + super(vehicles, information, activeVehicleMapping, options); this.information = information; } @@ -43,7 +44,7 @@ export class ISRSearch extends Mission { const missionParameters = this.information.parameters; const tasks = new DictionaryList(); - if (!this.information.options.noTakeoff) { + if (!this.options.isrSearch.noTakeoff) { tasks.push('isrSearch', { taskType: 'takeoff', ...missionParameters.takeoff, @@ -55,7 +56,7 @@ export class ISRSearch extends Mission { ...missionParameters.isrSearch, }); - if (!this.information.options.noLand) { + if (!this.options.isrSearch.noLand) { tasks.push('isrSearch', { taskType: 'land', ...missionParameters.land, @@ -69,7 +70,7 @@ export class ISRSearch extends Mission { super.update(jsonMessage); if (Message.TypeGuard.isPOIMessage(jsonMessage) - && this.activeVehicleMapping[jsonMessage.sid] === 'isrSearch') { + && this.activeVehicleMapping[this.missionName][jsonMessage.sid] === 'isrSearch') { const poiMessage = jsonMessage as Message.POIMessage; this.missionData.push({ lat: poiMessage.lat, diff --git a/src/common/missions/PayloadDrop.ts b/src/common/missions/PayloadDrop.ts index 035d7334..1b7bec6b 100644 --- a/src/common/missions/PayloadDrop.ts +++ b/src/common/missions/PayloadDrop.ts @@ -32,9 +32,10 @@ export class PayloadDrop extends Mission { public constructor( vehicles: { [vehicleId: number]: Vehicle }, information: MissionInformation.PayloadDropInformation, - activeVehicleMapping: { [vehicleId: number]: JobType }, + activeVehicleMapping: MissionInformation.ActiveVehicleMapping, + options: MissionInformation.MissionOptions, ) { - super(vehicles, information, activeVehicleMapping); + super(vehicles, information, activeVehicleMapping, options); this.information = information; } @@ -42,7 +43,7 @@ export class PayloadDrop extends Mission { const missionParameters = this.information.parameters; const tasks = new DictionaryList(); - if (!this.information.options.noTakeoff) { + if (!this.options.payloadDrop.noTakeoff) { tasks.push('payloadDrop', { taskType: 'takeoff', ...missionParameters.takeoff, @@ -54,7 +55,7 @@ export class PayloadDrop extends Mission { ...missionParameters.payloadDrop, }); - if (!this.information.options.noLand) { + if (!this.options.payloadDrop.noLand) { tasks.push('payloadDrop', { taskType: 'land', ...missionParameters.land, @@ -68,7 +69,7 @@ export class PayloadDrop extends Mission { super.update(jsonMessage); if (Message.TypeGuard.isPOIMessage(jsonMessage) - && this.activeVehicleMapping[jsonMessage.sid] === 'payloadDrop') { + && this.activeVehicleMapping[this.missionName][jsonMessage.sid] === 'payloadDrop') { const poiMessage = jsonMessage as Message.POIMessage; this.missionData = { lat: poiMessage.lat, diff --git a/src/common/missions/UGVRescue.ts b/src/common/missions/UGVRescue.ts index 6cc8a647..bc2d19ab 100644 --- a/src/common/missions/UGVRescue.ts +++ b/src/common/missions/UGVRescue.ts @@ -24,9 +24,10 @@ export class UGVRescue extends Mission { public constructor( vehicles: { [vehicleId: number]: Vehicle }, information: MissionInformation.UGVRescueInformation, - activeVehicleMapping: { [vehicleId: number]: JobType }, + activeVehicleMapping: MissionInformation.ActiveVehicleMapping, + options: MissionInformation.MissionOptions, ) { - super(vehicles, information, activeVehicleMapping); + super(vehicles, information, activeVehicleMapping, options); this.information = information; } diff --git a/src/common/missions/UUVRescue.ts b/src/common/missions/UUVRescue.ts index 1887e5b2..b74a3acf 100644 --- a/src/common/missions/UUVRescue.ts +++ b/src/common/missions/UUVRescue.ts @@ -24,9 +24,10 @@ export class UUVRescue extends Mission { public constructor( vehicles: { [vehicleId: number]: Vehicle }, information: MissionInformation.UUVRescueInformation, - activeVehicleMapping: { [vehicleId: number]: JobType }, + activeVehicleMapping: MissionInformation.ActiveVehicleMapping, + options: MissionInformation.MissionOptions, ) { - super(vehicles, information, activeVehicleMapping); + super(vehicles, information, activeVehicleMapping, options); this.information = information; } diff --git a/src/common/missions/VTOLSearch.ts b/src/common/missions/VTOLSearch.ts index 56351b80..c494228f 100644 --- a/src/common/missions/VTOLSearch.ts +++ b/src/common/missions/VTOLSearch.ts @@ -47,9 +47,10 @@ export class VTOLSearch extends Mission { public constructor( vehicles: { [vehicleId: number]: Vehicle }, information: MissionInformation.VTOLSearchInformation, - activeVehicleMapping: { [vehicleId: number]: JobType }, + activeVehicleMapping: MissionInformation.ActiveVehicleMapping, + options: MissionInformation.MissionOptions, ) { - super(vehicles, information, activeVehicleMapping); + super(vehicles, information, activeVehicleMapping, options); this.information = information; } @@ -57,7 +58,7 @@ export class VTOLSearch extends Mission { const vehicleIdString = Object.keys(this.activeVehicleMapping) .find((idString): boolean => { const id = parseInt(idString, 10); - return this.activeVehicleMapping[id] === 'detailedSearch'; + return this.activeVehicleMapping[this.missionName][id] === 'detailedSearch'; }); if (!vehicleIdString) return undefined; @@ -83,13 +84,13 @@ export class VTOLSearch extends Mission { if (Message.TypeGuard.isPOIMessage(jsonMessage)) { const poiMessage = jsonMessage as Message.POIMessage; - if (this.activeVehicleMapping[jsonMessage.sid] === 'quickScan') { + if (this.activeVehicleMapping[this.missionName][jsonMessage.sid] === 'quickScan') { this.addTask('detailedSearch', { taskType: 'detailedSearch', lat: poiMessage.lat, lng: poiMessage.lng, }); - } else if (this.activeVehicleMapping[jsonMessage.sid] === 'detailedSearch') { + } else if (this.activeVehicleMapping[this.missionName][jsonMessage.sid] === 'detailedSearch') { this.missionData = { lat: poiMessage.lat, lng: poiMessage.lng }; } } diff --git a/src/common/missions/index.ts b/src/common/missions/index.ts index 931e8901..bd6ee6d6 100644 --- a/src/common/missions/index.ts +++ b/src/common/missions/index.ts @@ -37,7 +37,7 @@ export interface MissionObject { constructor: MissionWrapper; } -const missionObject: { [missionName: string]: MissionObject | undefined } = { +const missionObject: { [missionName in MissionName]: MissionObject } = { isrSearch: ISRSearch, vtolSearch: VTOLSearch, payloadDrop: PayloadDrop, diff --git a/src/common/struct/Mission.ts b/src/common/struct/Mission.ts index c1de5ff2..29fb2a38 100644 --- a/src/common/struct/Mission.ts +++ b/src/common/struct/Mission.ts @@ -57,7 +57,12 @@ export default abstract class Mission { * * There will be no job types in this mapping that are irrelevant to the mission. */ - protected activeVehicleMapping: { [vehicleId: number]: JobType } = {}; + protected activeVehicleMapping: MissionInformation.ActiveVehicleMapping; + + /** + * Options to generate tasks for the mission. + */ + protected options: MissionInformation.MissionOptions; /** * Comparison to how tasks are added to the waiting task list. If there is no @@ -94,19 +99,13 @@ export default abstract class Mission { public constructor( vehicles: { [vehicleId: number]: Vehicle }, information: MissionInformation.Information, - activeVehicleMapping: { [vehicleId: number]: JobType }, + activeVehicleMapping: MissionInformation.ActiveVehicleMapping, + options: MissionInformation.MissionOptions, ) { this.vehicles = vehicles; this.information = information; - - // Filters out any irrelevant jobs when adding to activeVehicleMapping. - Object.keys(activeVehicleMapping).forEach((vehicleIdString): void => { - const vehicleId = parseInt(vehicleIdString, 10); - const jobType = activeVehicleMapping[vehicleId]; - if (this.jobTypes.has(jobType)) { - this.activeVehicleMapping[vehicleId] = jobType; - } - }); + this.options = options; + this.activeVehicleMapping = activeVehicleMapping; /* * Create event handler that will handle all changes of mission status: @@ -224,7 +223,7 @@ export default abstract class Mission { }; pendingAssignVehicleIds.forEach((vehicleId): void => { - const jobType = this.activeVehicleMapping[vehicleId]; + const jobType = this.activeVehicleMapping[this.missionName][vehicleId]; this.vehicles[vehicleId].assignJob(jobType, (): void => onSuccess(vehicleId), @@ -291,7 +290,7 @@ export default abstract class Mission { // Puts vehicles in activeVehicleMapping to waitingVehicles. pendingAssignVehicleIds.forEach((vehicleId): void => { - const jobType = this.activeVehicleMapping[vehicleId]; + const jobType = this.activeVehicleMapping[this.missionName][vehicleId]; const vehicle = this.vehicles[vehicleId]; this.waitingVehicles.push(jobType, vehicle); @@ -339,7 +338,7 @@ export default abstract class Mission { } if (Message.TypeGuard.isCompleteMessage(jsonMessage)) { - const jobType = this.activeVehicleMapping[jsonMessage.sid]; + const jobType = this.activeVehicleMapping[this.missionName][jsonMessage.sid]; /* * Mission is not yet finished, continue to assign tasks. One of the following will happen: @@ -483,7 +482,9 @@ export default abstract class Mission { * @param activeVehicleMapping User provided map of vehicle to their job type. */ private checkActiveVehicleMapping(): boolean { - const providedJobTypes = new Set(Object.values(this.activeVehicleMapping)); + const providedJobTypes = new Set( + Object.values(this.activeVehicleMapping[this.missionName]), + ); const hasRequiredJobTypes = Array.from(this.jobTypes).every( (requiredJobType): boolean => providedJobTypes.has(requiredJobType), @@ -495,7 +496,9 @@ export default abstract class Mission { (vehicleIdString): number => parseInt(vehicleIdString, 10), ).every((vehicleId): boolean => vehicleConfig.isValidVehicleId(vehicleId) && this.vehicles[vehicleId] - && this.vehicles[vehicleId].getJobs().includes(this.activeVehicleMapping[vehicleId])); + && this.vehicles[vehicleId] + .getJobs() + .includes(this.activeVehicleMapping[this.missionName][vehicleId])); if (!hasValidVehicleAssigned) return false; @@ -509,7 +512,7 @@ export default abstract class Mission { */ private handleUnresponsiveVehicle(vehicleId: number): void { // Ignore vehicles that are not related to the mission. - if (!this.activeVehicleMapping[vehicleId]) return; + if (!this.activeVehicleMapping[this.missionName][vehicleId]) return; if (this.status === 'waiting') { this.stop(false, `Take manual control over ${(vehicleConfig.vehicleInfos[vehicleId] as VehicleInfo).name}`); @@ -517,9 +520,9 @@ export default abstract class Mission { ipc.postStopSendingMessages(); this.stop(false, `Take manual control over ${(vehicleConfig.vehicleInfos[vehicleId] as VehicleInfo).name}`); } else { - const jobType = this.activeVehicleMapping[vehicleId]; + const jobType = this.activeVehicleMapping[this.missionName][vehicleId]; - delete this.activeVehicleMapping[vehicleId]; + delete this.activeVehicleMapping[this.missionName][vehicleId]; // Put the task vehicle is performing back to the front of waitingTasks. if (this.activeTasks.has(vehicleId)) { @@ -532,7 +535,7 @@ export default abstract class Mission { const newVehicle = this.waitingVehicles.shift(jobType); const task = this.waitingTasks.shift(jobType); if (newVehicle && task) { - this.activeVehicleMapping[newVehicle.getVehicleId()] = jobType; + this.activeVehicleMapping[this.missionName][newVehicle.getVehicleId()] = jobType; if (!this.assignTask(newVehicle, task)) return; this.activeTasks.set(newVehicle.getVehicleId(), task); diff --git a/src/main/index.ts b/src/main/index.ts index e54c45bc..27147421 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -219,11 +219,11 @@ function createMissionWindow(): void { title: 'NGCP Mission User Interface', icon, show: false, - width: Math.floor(WIDTH * 2.5 / 3), - minWidth: Math.floor(WIDTH * 2.5 / 3), - height: HEIGHT, + width: Math.floor(WIDTH * 4 / 3), + minWidth: Math.floor(WIDTH * 4 / 3), + height: Math.floor(HEIGHT * 4 / 3), autoHideMenuBar: true, - minHeight: HEIGHT, + minHeight: Math.floor(HEIGHT * 4 / 3), }); if (isDevelopment) { diff --git a/src/renderer/fixtures/index.ts b/src/renderer/fixtures/index.ts index a38297cb..c04c5e2c 100644 --- a/src/renderer/fixtures/index.ts +++ b/src/renderer/fixtures/index.ts @@ -3,6 +3,6 @@ /* eslint-disable multiline-comment-style */ // import './logMessagesFixtures'; -import './updateBoundingBoxesFixtures'; -// import './updateVehiclesFixtures'; -import './updateWaypointsFixtures'; +// import './updateBoundingBoxesFixtures'; +import './updateVehiclesFixtures'; +// import './updateWaypointsFixtures'; diff --git a/src/renderer/form/Checkbox.tsx b/src/renderer/form/Checkbox.tsx new file mode 100644 index 00000000..42ff8bf3 --- /dev/null +++ b/src/renderer/form/Checkbox.tsx @@ -0,0 +1,22 @@ +import React, { PureComponent, ReactNode } from 'react'; + +export interface CheckboxProps { + checked: boolean; + onChange: (event: React.ChangeEvent) => void; + label: string; +} + +export default class Checkbox extends PureComponent { + public render(): ReactNode { + const { checked, label, onChange } = this.props; + + return ( +
+ +
+ ); + } +} diff --git a/src/renderer/form/Select.tsx b/src/renderer/form/Select.tsx new file mode 100644 index 00000000..574e00e2 --- /dev/null +++ b/src/renderer/form/Select.tsx @@ -0,0 +1,32 @@ +import React, { PureComponent, ReactNode } from 'react'; + +export interface SelectProps { + defaultOptionValue: { value: string; title?: ReactNode }; + optionValues: { value: string; title?: ReactNode }[]; + onChange: (event: React.ChangeEvent) => void; +} + +export default class Select extends PureComponent { + public constructor(props: SelectProps) { + super(props); + } + + public render(): ReactNode { + const { defaultOptionValue, onChange, optionValues } = this.props; + + return ( + + ); + } +} diff --git a/src/renderer/mainWindow/log/LogContainer.tsx b/src/renderer/mainWindow/log/LogContainer.tsx index e45a0239..cdbc98d8 100644 --- a/src/renderer/mainWindow/log/LogContainer.tsx +++ b/src/renderer/mainWindow/log/LogContainer.tsx @@ -3,7 +3,6 @@ import moment, { Moment } from 'moment'; import React, { Component, createRef, - FormEvent, ReactNode, RefObject, } from 'react'; @@ -17,6 +16,8 @@ import { import * as ComponentStyle from '../../../types/componentStyle'; +import Select from '../../form/Select'; + import './log.css'; interface State { @@ -181,7 +182,7 @@ export default class LogContainer extends Component): void { + private updateFilter(event: React.ChangeEvent): void { const { messages } = this.state; this.heightCache.clearAll(); @@ -229,7 +230,7 @@ export default class LogContainer extends Component @@ -251,12 +252,23 @@ export default class LogContainer extends Component
- + ): void => { + ipc.postUpdateActiveVehicleMapping( + missionName, + jobType, + parseInt(event.target.value, 10), + ); + }} + /> +
+ ); + }); + + return ( +
+

{title[missionName]}

+ {jobMappingComponents} +
+ ); + }); + + return {mappingComponents}; + } +} diff --git a/src/renderer/missionWindow/mapping/MissionOptions.tsx b/src/renderer/missionWindow/mapping/MissionOptions.tsx new file mode 100644 index 00000000..3a15cf71 --- /dev/null +++ b/src/renderer/missionWindow/mapping/MissionOptions.tsx @@ -0,0 +1,88 @@ +import React, { PureComponent, Fragment, ReactNode } from 'react'; + +import * as MissionInformation from '../../../types/missionInformation'; + +import ipc from '../../../util/ipc'; + +import Checkbox from '../../form/Checkbox'; + +export interface MissionOptionsProps { + title: { [missionName in MissionInformation.MissionName]: string }; + missionNames: MissionInformation.MissionName[]; + options: MissionInformation.MissionOptions; +} + +export default class MissionOptions extends PureComponent { + private static onChangeISRSearchNoTakeoff(event: React.ChangeEvent): void { + ipc.postUpdateOptions('isrSearch', 'noTakeoff', event.target.checked); + } + + private static onChangeISRSearchNoLand(event: React.ChangeEvent): void { + ipc.postUpdateOptions('isrSearch', 'noLand', event.target.checked); + } + + private static onChangePayloadDropNoTakeoff(event: React.ChangeEvent): void { + ipc.postUpdateOptions('payloadDrop', 'noTakeoff', event.target.checked); + } + + private static onChangePayloadDropNoLand(event: React.ChangeEvent): void { + ipc.postUpdateOptions('payloadDrop', 'noLand', event.target.checked); + } + + public render(): ReactNode { + const { missionNames, options, title } = this.props; + + const optionComponents = missionNames.filter((missionName): boolean => missionName in options) + .map((missionName): ReactNode => { + let checkboxes: ReactNode; + + switch (missionName) { + case 'isrSearch': + checkboxes = ( + + + + + ); + break; + + case 'payloadDrop': + checkboxes = ( + + + + + ); + break; + + default: + throw new RangeError(`Tried to make mission option component for ${missionName}`); + } + + return ( +
+

{title[missionName]}

+ {checkboxes} +
+ ); + }); + + return {optionComponents}; + } +} diff --git a/src/renderer/missionWindow/mission.css b/src/renderer/missionWindow/mission.css index 929bf8ef..71bc39aa 100644 --- a/src/renderer/missionWindow/mission.css +++ b/src/renderer/missionWindow/mission.css @@ -8,8 +8,9 @@ grid-template-areas: 'selector selector selector selector' 'parameter parameter parameter mapping' + 'parameter parameter parameter options' 'button button button button'; - grid-template-rows: 25px auto 25px; + grid-template-rows: 25px 1fr 1fr 25px; grid-template-columns: repeat(4, 1fr); } @@ -26,6 +27,10 @@ grid-area: mapping; } +.optionsContainer { + grid-area: options; +} + .buttonContainer { grid-area: button; margin: auto; diff --git a/src/renderer/missionWindow/parameters/ISRSearch.tsx b/src/renderer/missionWindow/parameter/ISRSearch.tsx similarity index 94% rename from src/renderer/missionWindow/parameters/ISRSearch.tsx rename to src/renderer/missionWindow/parameter/ISRSearch.tsx index 0be2f44b..0f8ce197 100644 --- a/src/renderer/missionWindow/parameters/ISRSearch.tsx +++ b/src/renderer/missionWindow/parameter/ISRSearch.tsx @@ -1,7 +1,7 @@ import { Event, ipcRenderer } from 'electron'; import React, { Component, ReactNode } from 'react'; -import { missionName, jobTypes } from '../../../common/missions/ISRSearch'; +import { missionName } from '../../../common/missions/ISRSearch'; import { Location } from '../../../static/index'; @@ -59,11 +59,6 @@ const information: ISRSearchInformation = { ], }, }, - // TODO: Move options as a separate entity for Orchestrator to take to make Mission UI easier. - options: { - noTakeoff: false, - noLand: false, - }, }; const checklist: { [check: string]: number | undefined } = { @@ -254,26 +249,12 @@ export class ISRSearch extends Component {
- -
- - -
-
); } } export default { - name, missionName, - jobTypes, layout: ISRSearch as React.ElementType, }; diff --git a/src/renderer/missionWindow/parameters/PayloadDrop.tsx b/src/renderer/missionWindow/parameter/PayloadDrop.tsx similarity index 71% rename from src/renderer/missionWindow/parameters/PayloadDrop.tsx rename to src/renderer/missionWindow/parameter/PayloadDrop.tsx index 5588b255..e908730b 100644 --- a/src/renderer/missionWindow/parameters/PayloadDrop.tsx +++ b/src/renderer/missionWindow/parameter/PayloadDrop.tsx @@ -1,6 +1,6 @@ import React, { ReactNode } from 'react'; -import { missionName, jobTypes } from '../../../common/missions/PayloadDrop'; +import { missionName } from '../../../common/missions/PayloadDrop'; export const name = 'Payload Drop'; @@ -11,8 +11,6 @@ export function PayloadDrop(): ReactNode { } export default { - name, missionName, - jobTypes, layout: PayloadDrop as React.ElementType, }; diff --git a/src/renderer/missionWindow/parameters/UGVRescue.tsx b/src/renderer/missionWindow/parameter/UGVRescue.tsx similarity index 71% rename from src/renderer/missionWindow/parameters/UGVRescue.tsx rename to src/renderer/missionWindow/parameter/UGVRescue.tsx index 6c1418d6..e9e4911b 100644 --- a/src/renderer/missionWindow/parameters/UGVRescue.tsx +++ b/src/renderer/missionWindow/parameter/UGVRescue.tsx @@ -1,6 +1,6 @@ import React, { ReactNode } from 'react'; -import { missionName, jobTypes } from '../../../common/missions/UGVRescue'; +import { missionName } from '../../../common/missions/UGVRescue'; export const name = 'UGV Rescue'; @@ -11,8 +11,6 @@ export function UGVRescue(): ReactNode { } export default { - name, missionName, - jobTypes, layout: UGVRescue as React.ElementType, }; diff --git a/src/renderer/missionWindow/parameters/UUVRescue.tsx b/src/renderer/missionWindow/parameter/UUVRescue.tsx similarity index 71% rename from src/renderer/missionWindow/parameters/UUVRescue.tsx rename to src/renderer/missionWindow/parameter/UUVRescue.tsx index 59bbbb62..ebe3af20 100644 --- a/src/renderer/missionWindow/parameters/UUVRescue.tsx +++ b/src/renderer/missionWindow/parameter/UUVRescue.tsx @@ -1,6 +1,6 @@ import React, { ReactNode } from 'react'; -import { missionName, jobTypes } from '../../../common/missions/UUVRescue'; +import { missionName } from '../../../common/missions/UUVRescue'; export const name = 'UUV Rescue'; @@ -11,8 +11,6 @@ export function UUVRescue(): ReactNode { } export default { - name, missionName, - jobTypes, layout: UUVRescue as React.ElementType, }; diff --git a/src/renderer/missionWindow/parameters/VTOLSearch.tsx b/src/renderer/missionWindow/parameter/VTOLSearch.tsx similarity index 71% rename from src/renderer/missionWindow/parameters/VTOLSearch.tsx rename to src/renderer/missionWindow/parameter/VTOLSearch.tsx index d5628724..e9cf5401 100644 --- a/src/renderer/missionWindow/parameters/VTOLSearch.tsx +++ b/src/renderer/missionWindow/parameter/VTOLSearch.tsx @@ -1,6 +1,6 @@ import React, { ReactNode } from 'react'; -import { missionName, jobTypes } from '../../../common/missions/VTOLSearch'; +import { missionName } from '../../../common/missions/VTOLSearch'; export const name = 'VTOL Search'; @@ -11,8 +11,6 @@ export function VTOLSearch(): ReactNode { } export default { - name, missionName, - jobTypes, layout: VTOLSearch as React.ElementType, }; diff --git a/src/renderer/missionWindow/parameters/parameters.css b/src/renderer/missionWindow/parameter/parameters.css similarity index 100% rename from src/renderer/missionWindow/parameters/parameters.css rename to src/renderer/missionWindow/parameter/parameters.css diff --git a/src/static/config.json b/src/static/config.json index 1781e843..e63f3126 100644 --- a/src/static/config.json +++ b/src/static/config.json @@ -1,7 +1,7 @@ { - "fixtures": false, + "fixtures": true, "geolocation": false, - "vehicleDisconnectionTime": 20, + "vehicleDisconnectionTime": 3, "messageSendRate": 10 } diff --git a/src/types/missionInformation.ts b/src/types/missionInformation.ts index 15e11ecf..2d952de7 100644 --- a/src/types/missionInformation.ts +++ b/src/types/missionInformation.ts @@ -1,7 +1,52 @@ +import { JobType } from '../static/index'; + import * as Task from './task'; export type MissionName = 'isrSearch' | 'vtolSearch' | 'payloadDrop' | 'ugvRescue' | 'uuvRescue'; +type ActiveVehicleMappingSignature = { + [missionName in MissionName]: { [vehicleId: number]: JobType }; +} + +/** + * Mapping of vehicle to job to perform for all missions. + */ +export interface ActiveVehicleMapping extends ActiveVehicleMappingSignature { + isrSearch: { [vehicleId: number]: JobType }; + vtolSearch: { [vehicleId: number]: JobType }; + payloadDrop: { [vehicleId: number]: JobType }; + ugvRescue: { [vehicleId: number]: JobType }; + uuvRescue: { [vehicleId: number]: JobType }; +} + +/** + * Options for all missions run. + */ +export interface MissionOptions { + isrSearch: { + /** + * Will not require takeoff task if true. + */ + noTakeoff: boolean; + + /** + * Will not require land task if true. + */ + noLand: boolean; + }; + payloadDrop: { + /** + * Will not require takeoff task if true. + */ + noTakeoff: boolean; + + /** + * Will not require land task if true. + */ + noLand: boolean; + }; +} + interface InformationBase { /** * Name of mission. @@ -12,11 +57,6 @@ interface InformationBase { * Parameters for the mission. Both user and mission generated. */ parameters: {}; - - /** - * Options for the mission. User generated. - */ - options: {}; } // eslint-disable-next-line @typescript-eslint/interface-name-prefix @@ -27,17 +67,6 @@ export interface ISRSearchInformation extends InformationBase { isrSearch: Task.ISRSearchTaskParameters; land: Task.LandTaskParameters; }; - options: { - /** - * Will not require takeoff task if true. - */ - noTakeoff: boolean; - - /** - * Will not require land task if true. - */ - noLand: boolean; - }; } /** @@ -71,17 +100,6 @@ export interface PayloadDropInformation extends InformationBase { payloadDrop: Task.PayloadDropTaskParameters; land: Task.LandTaskParameters; }; - options: { - /** - * Will not require takeoff task if true. - */ - noTakeoff: boolean; - - /** - * Will not require land task if true. - */ - noLand: boolean; - }; } function isPayloadDropInformation(information: Information): boolean { diff --git a/src/util/ipc.ts b/src/util/ipc.ts index 96494b81..b31f60d6 100644 --- a/src/util/ipc.ts +++ b/src/util/ipc.ts @@ -16,12 +16,12 @@ import { BrowserWindow, ipcRenderer } from 'electron'; -import { Location } from '../static/index'; +import { JobType, Location } from '../static/index'; import { BoundingBoxBounds, LogMessage } from '../types/componentStyle'; import * as FileOptions from '../types/fileOption'; import * as Message from '../types/message'; -import { Information } from '../types/missionInformation'; +import * as MissionInformation from '../types/missionInformation'; import * as Task from '../types/task'; import { VehicleObject } from '../types/vehicle'; @@ -296,10 +296,12 @@ function postShowMissionWindow(): void { * - common/Orchestrator */ function postStartMissions( - missions: Information[], + missions: MissionInformation.Information[], + activeVehicleMapping: MissionInformation.ActiveVehicleMapping, + options: MissionInformation.MissionOptions, requireConfirmation: boolean, ): void { - ipcRenderer.send('post', 'startMissions', missions, requireConfirmation); + ipcRenderer.send('post', 'startMissions', missions, activeVehicleMapping, options, requireConfirmation); } /** @@ -354,6 +356,20 @@ function postToggleTheme(): void { ipcRenderer.send('post', 'toggleTheme'); } +/** + * Post "updateActiveVehicleMapping" notification. + * + * Files that take this notification: + * - renderer/missionWindow/MissionWindow + */ +function postUpdateActiveVehicleMapping( + missionName: MissionInformation.MissionName, + jobType: JobType, + vehicleId: number, +): void { + ipcRenderer.send('post', 'updateActiveVehicleMapping', missionName, jobType, vehicleId); +} + /** * Post "updateBoundingBoxes" notification. * @@ -373,7 +389,7 @@ function postUpdateBoundingBoxes( * Files that take this notification: * - renderer/missionWindow/MissionWindow */ -function postUpdateInformation(information: Information): void { +function postUpdateInformation(information: MissionInformation.Information): void { ipcRenderer.send('post', 'updateInformation', information); } @@ -396,6 +412,20 @@ function postUpdateMapLocation( } } +/** + * Post "updateOptions" notification. + * + * Files that take this notification: + * - renderer/missionWindow/MissionWindow + */ +function postUpdateOptions( + missionName: MissionInformation.MissionName, + option: string, + value: boolean, +): void { + ipcRenderer.send('post', 'updateOptions', missionName, option, value); +} + /** * Post "updateVehicles" notification. * @@ -453,9 +483,11 @@ export default { postStopSendingMessage, postStopSendingMessages, postToggleTheme, + postUpdateActiveVehicleMapping, postUpdateBoundingBoxes, postUpdateInformation, postUpdateMapLocation, + postUpdateOptions, postUpdateVehicles, postUpdateWaypoints, }; From e9fb1021cb55f818bf9b9565e10d50ab3ce01823 Mon Sep 17 00:00:00 2001 From: Paul Matthew Barrameda Date: Thu, 25 Apr 2019 15:28:07 -0700 Subject: [PATCH 119/152] Fixed logic for unknown disconnection Source of disconnection from fixtures came from the following: 1. No ack from connectionAck message 2. No ack from stop message - job fixture set vehicle status to (running/paused/waiting) while no mission running Added logic to Orchestrator to not send these messages if fixtures are enabled, as well as simply exit on the same conditions instead of logging in Orchestrator.disconnectFromVehicle. --- src/common/Orchestrator.ts | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/src/common/Orchestrator.ts b/src/common/Orchestrator.ts index cecca54b..0da525da 100644 --- a/src/common/Orchestrator.ts +++ b/src/common/Orchestrator.ts @@ -116,7 +116,7 @@ class Orchestrator { if (!newMessage) return; // Put this after newMessage return since this message will be sent repeatedly. - ipc.postSendMessage(jsonMessage.sid, { type: 'connectionAck' }); + if (!config.fixtures) ipc.postSendMessage(jsonMessage.sid, { type: 'connectionAck' }); if (!this.vehicles[jsonMessage.sid]) { this.vehicles[jsonMessage.sid] = new Vehicle({ @@ -147,7 +147,7 @@ class Orchestrator { if (delta >= 0 && delta <= config.vehicleDisconnectionTime * 1000) { // Handler that expires and creates itself everytime it "pings" the vehicle. setTimeout((): void => { this.ping(vehicle); }, delta); - } else if (vehicle.getStatus() !== 'disconnected') { + } else { this.disconnectFromVehicle(vehicle.getVehicleId()); } } @@ -157,15 +157,7 @@ class Orchestrator { * @param vehicleId ID of vehicle to disconnect from. */ private disconnectFromVehicle(vehicleId: number): void { - if (!this.vehicles[vehicleId]) { - Orchestrator.postOrchestratorError(`Tried to disconnect from nonexisting vehicle id ${vehicleId}`); - return; - } - - if (this.vehicles[vehicleId].getStatus() === 'disconnected') { - Orchestrator.postOrchestratorError(`Tried to disconnect from disconnected vehicle id ${vehicleId}`); - return; - } + if (!this.vehicles[vehicleId] || this.vehicles[vehicleId].getStatus() === 'disconnected') return; this.vehicles[vehicleId].disconnect(); ipc.postUpdateVehicles(this.vehicles[vehicleId].toObject()); @@ -222,7 +214,7 @@ class Orchestrator { this.currentMission.update(jsonMessage); } else { const status = this.vehicles[jsonMessage.sid].getStatus(); - if (status === 'waiting' || status === 'running' || status === 'paused') { + if (!config.fixtures && (status === 'waiting' || status === 'running' || status === 'paused')) { this.vehicles[jsonMessage.sid].stop(); } } From 8e68a779bf92a14b6da13d430bcf81cf1f1b82a1 Mon Sep 17 00:00:00 2001 From: Paul Matthew Barrameda Date: Thu, 25 Apr 2019 18:08:50 -0700 Subject: [PATCH 120/152] Finished Mission UI Done. No joke this time. Only thing that needs to be done is to write line 246 of ISRSearch (pass the information, just build the ISRSearchInformation using the checklist. TODO: Maintain a cache on unload --- src/renderer/{form => common}/Select.tsx | 0 src/renderer/mainWindow/log/LogContainer.tsx | 2 +- src/renderer/mainWindow/map/MapContainer.tsx | 5 +- src/renderer/missionWindow/MissionWindow.tsx | 4 +- .../ActiveVehicleMapping.tsx | 4 +- .../extra}/Checkbox.tsx | 0 .../extra/CreateWaypointButton.tsx | 37 ++ .../{mapping => extra}/MissionOptions.tsx | 2 +- .../missionWindow/parameter/ISRSearch.tsx | 335 +++++++++++------- src/util/ipc.ts | 11 + src/util/parameter.ts | 17 + 11 files changed, 275 insertions(+), 142 deletions(-) rename src/renderer/{form => common}/Select.tsx (100%) rename src/renderer/missionWindow/{mapping => extra}/ActiveVehicleMapping.tsx (94%) rename src/renderer/{form => missionWindow/extra}/Checkbox.tsx (100%) create mode 100644 src/renderer/missionWindow/extra/CreateWaypointButton.tsx rename src/renderer/missionWindow/{mapping => extra}/MissionOptions.tsx (98%) create mode 100644 src/util/parameter.ts diff --git a/src/renderer/form/Select.tsx b/src/renderer/common/Select.tsx similarity index 100% rename from src/renderer/form/Select.tsx rename to src/renderer/common/Select.tsx diff --git a/src/renderer/mainWindow/log/LogContainer.tsx b/src/renderer/mainWindow/log/LogContainer.tsx index cdbc98d8..370ea358 100644 --- a/src/renderer/mainWindow/log/LogContainer.tsx +++ b/src/renderer/mainWindow/log/LogContainer.tsx @@ -16,7 +16,7 @@ import { import * as ComponentStyle from '../../../types/componentStyle'; -import Select from '../../form/Select'; +import Select from '../../common/Select'; import './log.css'; diff --git a/src/renderer/mainWindow/map/MapContainer.tsx b/src/renderer/mainWindow/map/MapContainer.tsx index 22dc647b..75be75b1 100644 --- a/src/renderer/mainWindow/map/MapContainer.tsx +++ b/src/renderer/mainWindow/map/MapContainer.tsx @@ -224,7 +224,10 @@ export default class MapContainer extends Component { }) || locationConfig.startLocation, }; - ipc.postUpdateWaypoints(false, { name: waypoint.name, ...newWaypoints[waypoint.name] }); + ipc.postUpdateWaypoints(false, { + name: waypoint.name, + location: newWaypoints[waypoint.name].location, + }); } }); diff --git a/src/renderer/missionWindow/MissionWindow.tsx b/src/renderer/missionWindow/MissionWindow.tsx index 395ef54e..9d029620 100644 --- a/src/renderer/missionWindow/MissionWindow.tsx +++ b/src/renderer/missionWindow/MissionWindow.tsx @@ -12,8 +12,8 @@ import { VehicleObject, VehicleStatus } from '../../types/vehicle'; import { updateVehicles } from '../../util/util'; -import ActiveVehicleMapping from './mapping/ActiveVehicleMapping'; -import MissionOptions from './mapping/MissionOptions'; +import ActiveVehicleMapping from './extra/ActiveVehicleMapping'; +import MissionOptions from './extra/MissionOptions'; import ISRSearch from './parameter/ISRSearch'; import PayloadDrop from './parameter/PayloadDrop'; diff --git a/src/renderer/missionWindow/mapping/ActiveVehicleMapping.tsx b/src/renderer/missionWindow/extra/ActiveVehicleMapping.tsx similarity index 94% rename from src/renderer/missionWindow/mapping/ActiveVehicleMapping.tsx rename to src/renderer/missionWindow/extra/ActiveVehicleMapping.tsx index f57c3ea6..22395a3d 100644 --- a/src/renderer/missionWindow/mapping/ActiveVehicleMapping.tsx +++ b/src/renderer/missionWindow/extra/ActiveVehicleMapping.tsx @@ -9,7 +9,7 @@ import { VehicleObject } from '../../../types/vehicle'; import ipc from '../../../util/ipc'; -import Select from '../../form/Select'; +import Select from '../../common/Select'; export interface ActiveVehicleMappingProps { title: { [missionName in MissionInformation.MissionName]: string }; @@ -27,7 +27,7 @@ export default class ActiveVehicleMapping extends PureComponent parseInt(a, 10) - parseInt(b, 10)) .filter((vehicleIdString): boolean => { const vehicleId = parseInt(vehicleIdString, 10); - return vehicles[vehicleId].jobs.includes(jobType); + return vehicles[vehicleId].status !== 'disconnected' && vehicles[vehicleId].jobs.includes(jobType); }); const optionValues = eligibleVehicles diff --git a/src/renderer/form/Checkbox.tsx b/src/renderer/missionWindow/extra/Checkbox.tsx similarity index 100% rename from src/renderer/form/Checkbox.tsx rename to src/renderer/missionWindow/extra/Checkbox.tsx diff --git a/src/renderer/missionWindow/extra/CreateWaypointButton.tsx b/src/renderer/missionWindow/extra/CreateWaypointButton.tsx new file mode 100644 index 00000000..46feceed --- /dev/null +++ b/src/renderer/missionWindow/extra/CreateWaypointButton.tsx @@ -0,0 +1,37 @@ +import React, { PureComponent, ReactNode } from 'react'; + +import ipc from '../../../util/ipc'; + +export interface CreateWaypointButtonProps { + /** + * Identifier that distinguishes this button from other buttons. + */ + name: string; + + /** + * Name of the waypoint itself, when it shows up on the map. + */ + value: string; +} + +/** + * One of the buttons that creates a waypoint on clicked. + */ +export default class CreateWaypointButton extends PureComponent { + public constructor(props: CreateWaypointButtonProps) { + super(props); + + this.onClick = this.onClick.bind(this); + } + + private onClick(): void { + const { name, value } = this.props; + + ipc.postUnlockParameterInputs(name); + ipc.postCreateWaypoints({ name: value }); + } + + public render(): ReactNode { + return ; + } +} diff --git a/src/renderer/missionWindow/mapping/MissionOptions.tsx b/src/renderer/missionWindow/extra/MissionOptions.tsx similarity index 98% rename from src/renderer/missionWindow/mapping/MissionOptions.tsx rename to src/renderer/missionWindow/extra/MissionOptions.tsx index 3a15cf71..4ad27b66 100644 --- a/src/renderer/missionWindow/mapping/MissionOptions.tsx +++ b/src/renderer/missionWindow/extra/MissionOptions.tsx @@ -4,7 +4,7 @@ import * as MissionInformation from '../../../types/missionInformation'; import ipc from '../../../util/ipc'; -import Checkbox from '../../form/Checkbox'; +import Checkbox from './Checkbox'; export interface MissionOptionsProps { title: { [missionName in MissionInformation.MissionName]: string }; diff --git a/src/renderer/missionWindow/parameter/ISRSearch.tsx b/src/renderer/missionWindow/parameter/ISRSearch.tsx index 0f8ce197..4d8a2fbb 100644 --- a/src/renderer/missionWindow/parameter/ISRSearch.tsx +++ b/src/renderer/missionWindow/parameter/ISRSearch.tsx @@ -5,69 +5,30 @@ import { missionName } from '../../../common/missions/ISRSearch'; import { Location } from '../../../static/index'; -import { ISRSearchInformation } from '../../../types/missionInformation'; +// import { ISRSearchInformation } from '../../../types/missionInformation'; import { VehicleObject } from '../../../types/vehicle'; import ipc from '../../../util/ipc'; +import { readyToStart } from '../../../util/parameter'; -export const name = 'ISR Search'; - -const information: ISRSearchInformation = { - missionName: 'isrSearch', - parameters: { - takeoff: { - lat: 0, - lng: 0, - alt: 0, - loiter: { - lat: 0, - lng: 0, - alt: 0, - radius: 0, - direction: 0, - }, - }, - isrSearch: { - alt: 0, - waypoints: [ - { - lat: 0, - lng: 0, - }, - { - lat: 0, - lng: 0, - }, - { - lat: 0, - lng: 0, - }, - ], - }, - land: { - waypoints: [ - { - lat: 0, - lng: 0, - alt: 0, - }, - { - lat: 0, - lng: 0, - alt: 0, - }, - ], - }, - }, -}; +import CreateWaypointButton from '../extra/CreateWaypointButton'; + +type ISRChecklistType = 'takeoffLat' | 'takeoffLng' | 'takeoffAlt' +| 'loiterLat' | 'loiterLng' | 'loiterAlt' | 'loiterRadius' | 'loiterDirection' +| 'isrSearchAlt' +| 'isrSearchLat1' | 'isrSearchLng1' +| 'isrSearchLat2' | 'isrSearchLng2' +| 'isrSearchLat3' | 'isrSearchLng3' +| 'landLat1' | 'landLng1' | 'landAlt1' +| 'landLat2' | 'landLng2' | 'landAlt2'; -const checklist: { [check: string]: number | undefined } = { - // Parameter checks. +const checklistCache: { [check in ISRChecklistType]: number | undefined } = { takeoffLat: undefined, takeoffLng: undefined, takeoffAlt: undefined, loiterLat: undefined, loiterLng: undefined, + loiterAlt: undefined, loiterRadius: undefined, loiterDirection: undefined, isrSearchAlt: undefined, @@ -85,20 +46,147 @@ const checklist: { [check: string]: number | undefined } = { landAlt2: undefined, }; +type ISRWaypointType = 'takeoff' | 'loiter' | 'isrSearch1' | 'isrSearch2' | 'isrSearch3' | 'land1' | 'land2'; + +type Locked = { [waypointType in ISRWaypointType]: boolean } & { + takeoff: boolean; + loiter: boolean; + isrSearch1: boolean; + isrSearch2: boolean; + isrSearch3: boolean; + land1: boolean; + land2: boolean; +} + +const lockedCache: Locked = { + takeoff: true, + loiter: true, + isrSearch1: true, + isrSearch2: true, + isrSearch3: true, + land1: true, + land2: true, +}; + // eslint-disable-next-line @typescript-eslint/interface-name-prefix export interface ISRSearchProps { vehicles: { [vehicleId: number]: VehicleObject }; } -export class ISRSearch extends Component { - private static onChange(event: React.ChangeEvent): void { - ISRSearch.updateChecklist({ - [event.target.name]: parseInt(event.target.value, 10), - }); +interface State { + /** + * Checklist of all required waypoints/coordinates. This is used to generate the + * parameters for the mission. + */ + checklist: { [check in ISRChecklistType]: number | undefined }; + + /** + * True once all checks in checklist are filled in properly. + */ + ready: boolean; + + /** + * True if the inputs for the waypoint type is disabled. Will become disabled + * once the create pin is clicked. + */ + locked: Locked; +} + +export class ISRSearch extends Component { + public constructor(props: ISRSearchProps) { + super(props); + + this.state = { + checklist: checklistCache, + ready: false, + locked: lockedCache, + }; + + this.onChange = this.onChange.bind(this); + this.updateWaypoints = this.updateWaypoints.bind(this); + this.updateChecklist = this.updateChecklist.bind(this); + this.unlockParameterInputs = this.unlockParameterInputs.bind(this); + this.readyToStart = this.readyToStart.bind(this); + } + + public componentDidMount(): void { + ipcRenderer.on('updateWaypoints', (__: Event, _: boolean, ...waypoints: { name: string; location: Location }[]): void => this.updateWaypoints(...waypoints)); + ipcRenderer.on('unlockParameterInputs', (_: Event, waypointType: string): void => this.unlockParameterInputs(waypointType)); } - private static updateWaypoints(...waypoints: { name: string; location: Location }[]): void { - const checks: { [checkName: string]: number } = {}; + private onChange(event: React.ChangeEvent): void { + const { checklist } = this.state; + + if (!(event.target.name in checklist)) return; + + const name = event.target.name as ISRChecklistType; + const value = parseInt(event.target.value, 10); + + switch (name) { + case 'takeoffLat': + ipc.postUpdateWaypoints(true, { name: 'Takeoff', location: { lat: value, lng: checklist.takeoffLng as number } }); + break; + + case 'takeoffLng': + ipc.postUpdateWaypoints(true, { name: 'Takeoff', location: { lat: checklist.takeoffLat as number, lng: value } }); + break; + + case 'loiterLat': + ipc.postUpdateWaypoints(true, { name: 'Loiter', location: { lat: value, lng: checklist.loiterLng as number } }); + break; + + case 'loiterLng': + ipc.postUpdateWaypoints(true, { name: 'Loiter', location: { lat: checklist.loiterLat as number, lng: value } }); + break; + + case 'isrSearchLat1': + ipc.postUpdateWaypoints(true, { name: 'ISR Search 1', location: { lat: value, lng: checklist.isrSearchLng1 as number } }); + break; + + case 'isrSearchLng1': + ipc.postUpdateWaypoints(true, { name: 'ISR Search 1', location: { lat: checklist.isrSearchLat1 as number, lng: value } }); + break; + + case 'isrSearchLat2': + ipc.postUpdateWaypoints(true, { name: 'ISR Search 2', location: { lat: value, lng: checklist.isrSearchLng2 as number } }); + break; + + case 'isrSearchLng2': + ipc.postUpdateWaypoints(true, { name: 'ISR Search 2', location: { lat: checklist.isrSearchLat2 as number, lng: value } }); + break; + + case 'isrSearchLat3': + ipc.postUpdateWaypoints(true, { name: 'ISR Search 3', location: { lat: value, lng: checklist.isrSearchLng3 as number } }); + break; + + case 'isrSearchLng3': + ipc.postUpdateWaypoints(true, { name: 'ISR Search 3', location: { lat: checklist.isrSearchLat3 as number, lng: value } }); + break; + + case 'landLat1': + ipc.postUpdateWaypoints(true, { name: 'Land 1', location: { lat: value, lng: checklist.landLng1 as number } }); + break; + + case 'landLng1': + ipc.postUpdateWaypoints(true, { name: 'Land 1', location: { lat: checklist.landLat1 as number, lng: value } }); + break; + + case 'landLat2': + ipc.postUpdateWaypoints(true, { name: 'Land 2', location: { lat: value, lng: checklist.landLng2 as number } }); + break; + + case 'landLng2': + ipc.postUpdateWaypoints(true, { name: 'Land 2', location: { lat: checklist.landLat2 as number, lng: value } }); + break; + + default: + this.updateChecklist({ [name]: value }); + break; + } + } + + private updateWaypoints(...waypoints: { name: string; location: Location }[]): void { + const checks: { [checkName in ISRChecklistType]?: number } = {}; waypoints.forEach((waypoint): void => { switch (waypoint.name) { @@ -124,7 +212,7 @@ export class ISRSearch extends Component { case 'ISR Search 3': checks.isrSearchLat3 = waypoint.location.lat; - checks.isrSearchLng4 = waypoint.location.lng; + checks.isrSearchLng3 = waypoint.location.lng; break; case 'Land 1': @@ -141,114 +229,91 @@ export class ISRSearch extends Component { } }); - ISRSearch.updateChecklist(checks); + this.updateChecklist(checks); } - private static updateChecklist(checks: { [checkName: string]: number }): void { - Object.keys(checks).forEach((checkName): void => { - const value = checks[checkName]; - /* - * Following check is to ensure that we have all the "name" fields in the - * form correctly put in. Delete the following check when we ensure it works. - */ - if (!(checkName in checklist)) { - throw new RangeError('Wrong value provided'); - } + private updateChecklist(checks: { [checklistType in ISRChecklistType]?: number }): void { + const { checklist: newChecklist, ready } = this.state; - checklist[checkName] = value; + Object.keys(checks).forEach((checklistTypeString): void => { + const checklistType = checklistTypeString as ISRChecklistType; + const value = checks[checklistType]; + + newChecklist[checklistType] = value; }); - if (ISRSearch.isReadyToStart) { - ipc.postUpdateInformation(information); + if (ready || readyToStart(this)) { + // ipc.postUpdateInformation(information); } - } - - private static isReadyToStart(): boolean { - return Object.values(checklist).every((value): boolean => value !== undefined); - } - - private static onClickTakeoff(): void { - ipc.postCreateWaypoints({ name: 'Takeoff' }); - } - private static onClickLoiter(): void { - ipc.postCreateWaypoints({ name: 'Loiter' }); + this.setState({ checklist: newChecklist }); } - private static onClickSearch1(): void { - ipc.postCreateWaypoints({ name: 'ISR Search 1' }); - } + private unlockParameterInputs(waypointType: string): void { + const { locked: newLocked } = this.state; - private static onClickSearch2(): void { - ipc.postCreateWaypoints({ name: 'ISR Search 2' }); - } + if (waypointType in newLocked) { + newLocked[waypointType as ISRWaypointType] = false; + } - private static onClickSearch3(): void { - ipc.postCreateWaypoints({ name: 'ISR Search 3' }); + this.setState({ locked: newLocked }); } - private static onClickLand1(): void { - ipc.postCreateWaypoints({ name: 'Land 1' }); - } + private readyToStart(): boolean { + const { checklist } = this.state; - private static onClickLand2(): void { - ipc.postCreateWaypoints({ name: 'Land 2' }); - } + const ready = Object.values(checklist).every((value): boolean => value !== undefined); - public componentDidMount(): void { - ipcRenderer.on('updateWaypoints', (__: Event, _: boolean, ...waypoints: { name: string; location: Location }[]): void => ISRSearch.updateWaypoints(...waypoints)); + if (ready) this.setState({ ready }); + return ready; } public render(): ReactNode { - const { parameters } = information; + const { checklist, locked } = this.state; return (
-

ISR Search

-

Takeoff Coordinates

- - - -
- + + + +

Loiter Coordinates

- - - -
- - - + + + + + +

ISR Search Waypoints

- - - +
- - - + + +
- - - + + +
- + + +

Land Waypoints

- - - + + + +
-
- - - + + + +
-
); } diff --git a/src/util/ipc.ts b/src/util/ipc.ts index b31f60d6..4cea0864 100644 --- a/src/util/ipc.ts +++ b/src/util/ipc.ts @@ -356,6 +356,16 @@ function postToggleTheme(): void { ipcRenderer.send('post', 'toggleTheme'); } +/** + * Post "unlockParameterInputs" notification. + * + * Files that take this notification: + * - renderer/missionWindow/parameter/ISRSearch + */ +function postUnlockParameterInputs(waypointType: string): void { + ipcRenderer.send('post', 'unlockParameterInputs', waypointType); +} + /** * Post "updateActiveVehicleMapping" notification. * @@ -483,6 +493,7 @@ export default { postStopSendingMessage, postStopSendingMessages, postToggleTheme, + postUnlockParameterInputs, postUpdateActiveVehicleMapping, postUpdateBoundingBoxes, postUpdateInformation, diff --git a/src/util/parameter.ts b/src/util/parameter.ts new file mode 100644 index 00000000..febfee5d --- /dev/null +++ b/src/util/parameter.ts @@ -0,0 +1,17 @@ +/* eslint-disable import/prefer-default-export */ + +import { Component } from 'react'; + +type ParameterComponent = + Component<{}, { ready: boolean; checklist: { [check: string]: number | undefined } }>; + +export function readyToStart( + component: ParameterComponent, +): boolean { + const { checklist } = component.state; + + const ready = Object.values(checklist).every((value): boolean => value !== undefined); + + if (ready) component.setState({ ready }); + return ready; +} From 2515fc8c59939f3051705d38f00a015777046d3f Mon Sep 17 00:00:00 2001 From: Paul Matthew Barrameda Date: Thu, 25 Apr 2019 23:06:35 -0700 Subject: [PATCH 121/152] Wrote ISRSearch parameter object --- .../missionWindow/parameter/ISRSearch.tsx | 50 ++++++++++++++++++- 1 file changed, 48 insertions(+), 2 deletions(-) diff --git a/src/renderer/missionWindow/parameter/ISRSearch.tsx b/src/renderer/missionWindow/parameter/ISRSearch.tsx index 4d8a2fbb..bd34fbad 100644 --- a/src/renderer/missionWindow/parameter/ISRSearch.tsx +++ b/src/renderer/missionWindow/parameter/ISRSearch.tsx @@ -5,7 +5,6 @@ import { missionName } from '../../../common/missions/ISRSearch'; import { Location } from '../../../static/index'; -// import { ISRSearchInformation } from '../../../types/missionInformation'; import { VehicleObject } from '../../../types/vehicle'; import ipc from '../../../util/ipc'; @@ -243,7 +242,54 @@ export class ISRSearch extends Component { }); if (ready || readyToStart(this)) { - // ipc.postUpdateInformation(information); + ipc.postUpdateInformation({ + missionName: 'isrSearch', + parameters: { + takeoff: { + lat: newChecklist.takeoffLat as number, + lng: newChecklist.takeoffLng as number, + alt: newChecklist.takeoffAlt as number, + loiter: { + lat: newChecklist.loiterLat as number, + lng: newChecklist.loiterLng as number, + alt: newChecklist.loiterAlt as number, + radius: newChecklist.loiterRadius as number, + direction: newChecklist.loiterDirection as number, + }, + }, + isrSearch: { + alt: newChecklist.isrSearchAlt as number, + waypoints: [ + { + lat: newChecklist.isrSearchLat1 as number, + lng: newChecklist.isrSearchLng1 as number, + }, + { + lat: newChecklist.isrSearchLat2 as number, + lng: newChecklist.isrSearchLng2 as number, + }, + { + lat: newChecklist.isrSearchLat3 as number, + lng: newChecklist.isrSearchLng3 as number, + }, + ], + }, + land: { + waypoints: [ + { + lat: newChecklist.landLat1 as number, + lng: newChecklist.landLng1 as number, + alt: newChecklist.landAlt1 as number, + }, + { + lat: newChecklist.landLat2 as number, + lng: newChecklist.landLng2 as number, + alt: newChecklist.landAlt2 as number, + }, + ], + }, + }, + }); } this.setState({ checklist: newChecklist }); From 95540bcc220228164e115808f77eb9e52716c914 Mon Sep 17 00:00:00 2001 From: Paul Matthew Barrameda Date: Fri, 26 Apr 2019 12:18:15 -0700 Subject: [PATCH 122/152] Fixed edge case when field is empty --- src/renderer/missionWindow/parameter/ISRSearch.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/renderer/missionWindow/parameter/ISRSearch.tsx b/src/renderer/missionWindow/parameter/ISRSearch.tsx index bd34fbad..f42afc14 100644 --- a/src/renderer/missionWindow/parameter/ISRSearch.tsx +++ b/src/renderer/missionWindow/parameter/ISRSearch.tsx @@ -119,7 +119,7 @@ export class ISRSearch extends Component { if (!(event.target.name in checklist)) return; const name = event.target.name as ISRChecklistType; - const value = parseInt(event.target.value, 10); + const value = parseInt(event.target.value, 10) || 0; switch (name) { case 'takeoffLat': From 45368d9619d443ae4ae64dbe55aceb8c5e4642bd Mon Sep 17 00:00:00 2001 From: tnleang Date: Fri, 26 Apr 2019 15:03:13 -0700 Subject: [PATCH 123/152] implement PayloadDrop, VTOLSearch --- .../missionWindow/extra/CreateBoundingBox.tsx | 34 + .../missionWindow/parameter/PayloadDrop.tsx | 337 +++++++- .../missionWindow/parameter/VTOLSearch.tsx | 244 +++++- src/types/messages.ts | 754 ++++++++++++++++++ src/types/types.ts | 144 ++++ src/util/ipc.ts | 5 + 6 files changed, 1506 insertions(+), 12 deletions(-) create mode 100644 src/renderer/missionWindow/extra/CreateBoundingBox.tsx create mode 100644 src/types/messages.ts create mode 100644 src/types/types.ts diff --git a/src/renderer/missionWindow/extra/CreateBoundingBox.tsx b/src/renderer/missionWindow/extra/CreateBoundingBox.tsx new file mode 100644 index 00000000..86bafe4a --- /dev/null +++ b/src/renderer/missionWindow/extra/CreateBoundingBox.tsx @@ -0,0 +1,34 @@ +import React, { PureComponent, ReactNode } from 'react'; + +import ipc from '../../../util/ipc'; + +export interface CreateBoundingBoxProps { + /** + * Identifier that distinguishes + */ + name: string; + + /** + * Name of the box itself, when it shows up on the map. + */ + value: string; +} + +export default class CreateBoundingBoxButton extends PureComponent { + public constructor(props: CreateBoundingBoxProps) { + super(props); + + this.onClick = this.onClick.bind(this); + } + + private onClick(): void { + const { name, value } = this.props; + + ipc.postUnlockParameterInputs(name); + ipc.postCreateBoundingBoxes({ name: value }); + } + + public render(): ReactNode { + return ; + } +} diff --git a/src/renderer/missionWindow/parameter/PayloadDrop.tsx b/src/renderer/missionWindow/parameter/PayloadDrop.tsx index e908730b..ca46f680 100644 --- a/src/renderer/missionWindow/parameter/PayloadDrop.tsx +++ b/src/renderer/missionWindow/parameter/PayloadDrop.tsx @@ -1,15 +1,340 @@ -import React, { ReactNode } from 'react'; +import { Event, ipcRenderer } from 'electron'; +import React, { Component, ReactNode } from 'react'; import { missionName } from '../../../common/missions/PayloadDrop'; -export const name = 'Payload Drop'; +import { Location } from '../../../static/index'; -export function PayloadDrop(): ReactNode { - return ( -
Payload Drop
- ); +import { VehicleObject } from '../../../types/vehicle'; + +import ipc from '../../../util/ipc'; +import { readyToStart } from '../../../util/parameter'; + +import CreateWaypointButton from '../extra/CreateWaypointButton'; + +type PayloadDropChecklistType = 'takeoffLat' | 'takeoffLng' | 'takeoffAlt' +| 'loiterLat' | 'loiterLng' | 'loiterAlt' | 'loiterRadius' | 'loiterDirection' +| 'payloadDropLat1' | 'payloadDropLng1' | 'payloadDropAlt1' +| 'payloadDropLat2' | 'payloadDropLng2' | 'payloadDropAlt2' +| 'landLat1' | 'landLng1' | 'landAlt1' +| 'landLat2' | 'landLng2' | 'landAlt2'; + +const checklistCache: { [check in PayloadDropChecklistType ]: number | undefined } = { + takeoffLat: undefined, + takeoffLng: undefined, + takeoffAlt: undefined, + loiterLat: undefined, + loiterLng: undefined, + loiterAlt: undefined, + loiterRadius: undefined, + loiterDirection: undefined, + payloadDropAlt1: undefined, + payloadDropLat1: undefined, + payloadDropLng1: undefined, + payloadDropAlt2: undefined, + payloadDropLat2: undefined, + payloadDropLng2: undefined, + landLat1: undefined, + landLng1: undefined, + landAlt1: undefined, + landLat2: undefined, + landLng2: undefined, + landAlt2: undefined, +}; + +type PayloadType = 'takeoff' | 'loiter' | 'payloadDrop1'| 'payloadDrop2' | 'land1' | 'land2'; + +type Locked = { [type in PayloadType]: boolean} & { + takeoff: boolean; + loiter: boolean; + payloadDrop1: boolean; + payloadDrop2: boolean; + land1: boolean; + land2: boolean; +} + +const lockedCache: Locked = { + takeoff: true, + loiter: true, + payloadDrop1: true, + payloadDrop2: true, + land1: true, + land2: true, +}; + +// eslint-disable-next-line @typescript-eslint/interface-name-prefix +export interface PayloadDropProps { + vehicles: { [vehicleId: number]: VehicleObject }; +} + +interface State { + /** + * Checklist of all required waypoints/coordinates. This is used to generate the + * parameters for the mission. + */ + checklist: { [check in PayloadDropChecklistType]: number | undefined }; + + /** + * True once all checks are filled in properly. + */ + ready: boolean; + + /** + * True if the inputs for the waypoint type is disabled. Will become disabled + * once the create pin is clicked. + */ + locked: Locked; } +export class PayloadDrop extends Component { + public constructor(props: PayloadDropProps) { + super(props); + + this.state = { + checklist: checklistCache, + ready: false, + locked: lockedCache, + }; + + this.onChange = this.onChange.bind(this); + this.updateWaypoints = this.updateWaypoints.bind(this); + this.updateChecklist = this.updateChecklist.bind(this); + this.readyToStart = this.readyToStart.bind(this); + this.unlockParameterInputs = this.unlockParameterInputs.bind(this); + } + + public componentDidMount(): void { + ipcRenderer.on('updateWaypoints', (__: Event, _: boolean, ...waypoints: { name: string; location: Location }[]): void => this.updateWaypoints(...waypoints)); + ipcRenderer.on('unlockParameterInputs', (_: Event, waypointType: string): void => this.unlockParameterInputs(waypointType)); + } + + private onChange(event: React.ChangeEvent): void { + const { checklist } = this.state; + if (!(event.target.name in checklist)) return; + + const name = event.target.name as PayloadDropChecklistType; + const value = parseInt(event.target.value, 10) || 0; + switch (name) { + case 'takeoffLat': + ipc.postUpdateWaypoints(true, { name: 'Takeoff', location: { lat: value, lng: checklist.takeoffLng as number } }); + break; + case 'takeoffLng': + ipc.postUpdateWaypoints(true, { name: 'Takeoff', location: { lat: checklist.takeoffLat as number, lng: value } }); + break; + + case 'payloadDropLat1': + ipc.postUpdateWaypoints(true, { name: 'Payload Drop 1', location: { lat: value, lng: checklist.payloadDropLng1 as number } }); + break; + + case 'payloadDropLng1': + ipc.postUpdateWaypoints(true, { name: 'Payload Drop 1', location: { lat: checklist.payloadDropLat1 as number, lng: value } }); + break; + + case 'payloadDropLat2': + ipc.postUpdateWaypoints(true, { name: 'Payload Drop 2', location: { lat: value, lng: checklist.payloadDropLng2 as number } }); + break; + + case 'payloadDropLng2': + ipc.postUpdateWaypoints(true, { name: 'Payload Drop 2', location: { lat: checklist.payloadDropLat2 as number, lng: value } }); + break; + + case 'landLat1': + ipc.postUpdateWaypoints(true, { name: 'Land 1', location: { lat: value, lng: checklist.landLng1 as number } }); + break; + + case 'landLng1': + ipc.postUpdateWaypoints(true, { name: 'Land 1', location: { lat: checklist.landLat1 as number, lng: value } }); + break; + + case 'landLat2': + ipc.postUpdateWaypoints(true, { name: 'Land 2', location: { lat: value, lng: checklist.landLng2 as number } }); + break; + + case 'landLng2': + ipc.postUpdateWaypoints(true, { name: 'Land 2', location: { lat: checklist.landLat2 as number, lng: value } }); + break; + + default: + this.updateChecklist({ [name]: value }); + break; + } + } + + private updateWaypoints(...waypoints: { name: string; location: Location }[]): void { + const checks: { [checkName in PayloadDropChecklistType]?: number} = {}; + + waypoints.forEach((waypoint): void => { + switch (waypoint.name) { + case 'Takeoff': + checks.takeoffLat = waypoint.location.lat; + checks.takeoffLng = waypoint.location.lng; + break; + + case 'Loiter': + checks.loiterLat = waypoint.location.lat; + checks.loiterLng = waypoint.location.lng; + break; + + case 'Payload Drop 1': + checks.payloadDropLat1 = waypoint.location.lat; + checks.payloadDropLng1 = waypoint.location.lng; + break; + + case 'Payload Drop 2': + checks.payloadDropLat2 = waypoint.location.lat; + checks.payloadDropLng2 = waypoint.location.lng; + break; + + case 'Land 1': + checks.landLat1 = waypoint.location.lat; + checks.landLng1 = waypoint.location.lng; + break; + + case 'Land 2': + checks.landLat2 = waypoint.location.lat; + checks.landLng2 = waypoint.location.lng; + break; + + default: break; + } + }); + + this.updateChecklist(checks); + } + + private updateChecklist(checks: { [checklistType in PayloadDropChecklistType]?: number }): void { + const { checklist: newChecklist, ready } = this.state; + + Object.keys(checks).forEach((checklistTypeString): void => { + const checklistType = checklistTypeString as PayloadDropChecklistType; + const value = checks[checklistType]; + + newChecklist[checklistType] = value; + }); + + if (ready || readyToStart(this)) { + ipc.postUpdateInformation({ + missionName: 'payloadDrop', + parameters: { + takeoff: { + lat: newChecklist.takeoffLat as number, + lng: newChecklist.takeoffLng as number, + alt: newChecklist.takeoffAlt as number, + loiter: { + lat: newChecklist.loiterLat as number, + lng: newChecklist.loiterLng as number, + alt: newChecklist.loiterAlt as number, + radius: newChecklist.loiterRadius as number, + direction: newChecklist.loiterDirection as number, + }, + }, + payloadDrop: { + waypoints: [ + { + lat: newChecklist.payloadDropLat1 as number, + lng: newChecklist.payloadDropLng1 as number, + alt: newChecklist.payloadDropAlt1 as number, + }, + { + lat: newChecklist.payloadDropLat2 as number, + lng: newChecklist.payloadDropLng2 as number, + alt: newChecklist.payloadDropAlt2 as number, + }, + ], + }, + land: { + waypoints: [ + { + lat: newChecklist.landLat1 as number, + lng: newChecklist.landLng1 as number, + alt: newChecklist.landAlt1 as number, + }, + { + lat: newChecklist.landLat2 as number, + lng: newChecklist.landLng2 as number, + alt: newChecklist.landAlt2 as number, + }, + ], + }, + }, + }); + } + + this.setState({ checklist: newChecklist }); + } + + private unlockParameterInputs(waypointType: string): void { + const { locked: newLocked } = this.state; + + if (waypointType in newLocked) { + newLocked[waypointType as PayloadType] = false; + } + + this.setState({ locked: newLocked }); + } + + private readyToStart(): boolean { + const { checklist } = this.state; + const ready = Object.values(checklist).every((value): boolean => value !== undefined); + + if (ready) this.setState({ ready }); + return ready; + } + + public render(): ReactNode { + const { checklist, locked } = this.state; + return ( +
+

Takeoff Coordinates

+ + + + + +

Loiter Coordinates

+ + + + + + + +

Payload Drop Coordinates

+ + + + +
+ + + + + +

Land Waypoints

+ + + + +
+
+ + + + +
+
+ ); + } +} + +/* + * export function PayloadDrop(): ReactNode { + * return ( + *
Payload Drop
+ * ); + * } + */ + export default { missionName, layout: PayloadDrop as React.ElementType, diff --git a/src/renderer/missionWindow/parameter/VTOLSearch.tsx b/src/renderer/missionWindow/parameter/VTOLSearch.tsx index e9cf5401..987e2fc3 100644 --- a/src/renderer/missionWindow/parameter/VTOLSearch.tsx +++ b/src/renderer/missionWindow/parameter/VTOLSearch.tsx @@ -1,13 +1,245 @@ -import React, { ReactNode } from 'react'; +import { Event, ipcRenderer } from 'electron'; +import React, { Component, ReactNode } from 'react'; import { missionName } from '../../../common/missions/VTOLSearch'; -export const name = 'VTOL Search'; +import { Location } from '../../../static/index'; -export function VTOLSearch(): ReactNode { - return ( -
VTOL Search
- ); +import { VehicleObject } from '../../../types/vehicle'; + +import ipc from '../../../util/ipc'; +import { readyToStart } from '../../../util/parameter'; + +import CreateBoundingBoxButton from '../extra/CreateBoundingBox'; + +type VTOLSearchChecklistType = 'quickScanTopLeftLat' | 'quickScanTopLeftLng' | 'quickScanTopRightLat' +| 'quickScanTopRightLng' | 'quickScanBottomLeftLat' | 'quickScanBottomLeftLng' +| 'quickScanBottomRightLat' | 'quickScanBottomRightLng'; + +const checklistCache: { [check in VTOLSearchChecklistType ]: number | undefined} = { + quickScanTopLeftLat: undefined, + quickScanTopLeftLng: undefined, + quickScanTopRightLat: undefined, + quickScanTopRightLng: undefined, + quickScanBottomLeftLat: undefined, + quickScanBottomLeftLng: undefined, + quickScanBottomRightLat: undefined, + quickScanBottomRightLng: undefined, +}; + +type VTOLSearchType = 'quickScan'; + +type Locked = { [type in VTOLSearchType]: boolean} & { + quickScan: boolean; +} + +const lockedCache: Locked = { + quickScan: false, +}; + +// eslint-disable-next-line @typescript-eslint/interface-name-prefix +export interface VTOLSearchProps { + vehicles: { [vehicleId: number]: VehicleObject }; +} + +interface State { + /** + * Checklist of all required waypoints/coordinates. This is used to generate the + * parameters for the mission. + */ + checklist: { [check in VTOLSearchChecklistType]: number | undefined }; + + /** + * True once all checks are filled in properly. + */ + ready: boolean; + + /** + * True if the inputs for the waypoint type is disabled. Will become disabled + * once the create pin is clicked. + */ + locked: Locked; +} + +export class VTOLSearch extends Component { + public constructor(props: VTOLSearchProps) { + super(props); + + this.state = { + checklist: checklistCache, + ready: false, + locked: lockedCache, + }; + + this.onChange = this.onChange.bind(this); + this.updateWaypoints = this.updateWaypoints.bind(this); + this.updateChecklist = this.updateChecklist.bind(this); + this.readyToStart = this.readyToStart.bind(this); + this.unlockParameterInputs = this.unlockParameterInputs.bind(this); + } + + public componentDidMount(): void { + ipcRenderer.on('updateWaypoints', (__: Event, _: boolean, ...waypoints: { name: string; location: Location }[]): void => this.updateWaypoints(...waypoints)); + ipcRenderer.on('unlockParameterInputs', (_: Event, waypointType: string): void => this.unlockParameterInputs(waypointType)); + } + + private onChange(event: React.ChangeEvent): void { + const { checklist } = this.state; + if (!(event.target.name in checklist)) return; + + const name = event.target.name as VTOLSearchChecklistType; + const value = parseInt(event.target.value, 10) || 0; + switch (name) { + case 'quickScanTopLeftLat': + ipc.postUpdateWaypoints(true, { name: 'Quick Scan 1', location: { lat: value, lng: checklist.quickScanTopLeftLng as number } }); + break; + + case 'quickScanTopLeftLng': + ipc.postUpdateWaypoints(true, { name: 'Quick Scan 1', location: { lat: checklist.quickScanTopLeftLat as number, lng: value } }); + break; + + case 'quickScanTopRightLat': + ipc.postUpdateWaypoints(true, { name: 'Quick Scan 2', location: { lat: value, lng: checklist.quickScanTopRightLng as number } }); + break; + + case 'quickScanTopRightLng': + ipc.postUpdateWaypoints(true, { name: 'Quick Scan 2', location: { lat: checklist.quickScanTopRightLat as number, lng: value } }); + break; + + case 'quickScanBottomLeftLat': + ipc.postUpdateWaypoints(true, { name: 'Quick Scan 3', location: { lat: value, lng: checklist.quickScanBottomLeftLng as number } }); + break; + + case 'quickScanBottomLeftLng': + ipc.postUpdateWaypoints(true, { name: 'Quick Scan 3', location: { lat: checklist.quickScanBottomLeftLat as number, lng: value } }); + break; + + case 'quickScanBottomRightLat': + ipc.postUpdateWaypoints(true, { name: 'Quick Scan 4', location: { lat: value, lng: checklist.quickScanBottomRightLng as number } }); + break; + + case 'quickScanBottomRightLng': + ipc.postUpdateWaypoints(true, { name: 'Quick Scan 4', location: { lat: checklist.quickScanBottomRightLat as number, lng: value } }); + break; + + default: + this.updateChecklist({ [name]: value }); + break; + } + } + + private updateWaypoints(...boundingBoxes: { name: string; location: Location }[]): void { + const checks: { [checkName in VTOLSearchChecklistType]?: number} = {}; + + boundingBoxes.forEach((waypoint): void => { + switch (waypoint.name) { + case 'Quick Scan Top Left': + checks.quickScanTopLeftLat = waypoint.location.lat; + checks.quickScanTopLeftLng = waypoint.location.lng; + break; + + case 'Quick Scan Top Right': + checks.quickScanTopRightLat = waypoint.location.lat; + checks.quickScanTopRightLng = waypoint.location.lng; + break; + + case 'Quick Scan Bottom Left': + checks.quickScanBottomLeftLat = waypoint.location.lat; + checks.quickScanBottomLeftLng = waypoint.location.lng; + break; + + case 'Quick Scan Bottom Right': + checks.quickScanBottomRightLat = waypoint.location.lat; + checks.quickScanBottomRightLng = waypoint.location.lng; + break; + + default: break; + } + }); + + this.updateChecklist(checks); + } + + private updateChecklist(checks: { [checklistType in VTOLSearchChecklistType]?: number }): void { + const { checklist: newChecklist, ready } = this.state; + + Object.keys(checks).forEach((checklistTypeString): void => { + const checklistType = checklistTypeString as VTOLSearchChecklistType; + const value = checks[checklistType]; + + newChecklist[checklistType] = value; + + if (ready || readyToStart(this)) { + ipc.postUpdateInformation({ + missionName: 'vtolSearch', + parameters: { + quickScan: { + waypoints: [ + { + lat: newChecklist.quickScanTopLeftLat as number, + lng: newChecklist.quickScanTopLeftLng as number, + }, + { + lat: newChecklist.quickScanTopRightLat as number, + lng: newChecklist.quickScanTopRightLng as number, + }, + { + lat: newChecklist.quickScanBottomLeftLat as number, + lng: newChecklist.quickScanBottomLeftLng as number, + }, + { + lat: newChecklist.quickScanBottomRightLat as number, + lng: newChecklist.quickScanBottomRightLng as number, + }, + ], + }, + }, + }); + } + }); + + this.setState({ checklist: newChecklist }); + } + + private unlockParameterInputs(waypointType: string): void { + const { locked: newLocked } = this.state; + + if (waypointType in newLocked) { + newLocked[waypointType as VTOLSearchType] = false; + } + + this.setState({ locked: newLocked }); + } + + private readyToStart(): boolean { + const { checklist } = this.state; + const ready = Object.values(checklist).every((value): boolean => value !== undefined); + + if (ready) this.setState({ ready }); + return ready; + } + + public render(): ReactNode { + const { checklist, locked } = this.state; + return ( +
+

Quick Scan

+ + +
+ + +
+ + +
+ + +
+ +
+ ); + } } export default { diff --git a/src/types/messages.ts b/src/types/messages.ts new file mode 100644 index 00000000..67bddc62 --- /dev/null +++ b/src/types/messages.ts @@ -0,0 +1,754 @@ +/* + * Definitions and typeguards for all messages that are sent between GCS and vehicles. + * https://ground-control-station.readthedocs.io/en/latest/communications/messages.html + * + * Includes definitions for tasks too, as those are part of a message. + * https://ground-control-station.readthedocs.io/en/latest/communications/jobs.html + */ + +import { JobType, vehicleConfig } from '../static/index'; + +import { isVehicleStatus, VehicleStatus } from './types'; + +/** + * A float represented as a hexadecimal string. + * This is used to reduce the size of messages being sent between vehicles. + * Uses IEEE 754 standard to convert float to hex (and vice versa). + */ +export type HexFloat = string; + +/** + * Type guard for a hex float. + */ +export function isHexFloat(hex: string): boolean { + const modifiedHex = hex.startsWith('0x') ? hex.slice(2) : hex; + return modifiedHex.match(/^[0-9a-fA-F]+$/) !== null; +} + +/** + * Converts a float number to a hex float. + */ +/* eslint-disable no-bitwise */ +export function toHexFloat(float: number): HexFloat { + const getHex = (index: number): string => `00${index.toString(16)}`.slice(-2); + + const view = new DataView(new ArrayBuffer(4)); + view.setFloat32(0, float); + return `0x${[0, 0, 0, 0].map((_, index): string => getHex(view.getUint8(index))).join('')}`; +} + +/** + * Converts a hex string to a float number. + */ +export function toFloat(hex: HexFloat): number { + const int = parseInt(hex, 16); + if (int === 0) return 0; + + const sign = (int >>> 31) ? -1 : 1; + let exp = ((int >>> 23) & 0xff) - 127; + const mantissa = ((int & 0x7fffff) + 0x800000).toString(2); + let float = 0; + + for (let i = 0; i < mantissa.length; i += 1, exp -= 1) { + if (parseInt(mantissa[i], 10)) { + float += 2 ** exp; + } + } + + return float * sign; +} +/* eslint-enable no-bitwise */ + +// Definitions for all tasks. + +interface TaskBase { + taskType: string; +} + +interface TakeoffTaskParameters { + lat: HexFloat; + lng: HexFloat; + alt: HexFloat; + loiter: { + lat: HexFloat; + lng: HexFloat; + alt: HexFloat; + radius: HexFloat; + direction: HexFloat; + }; +} + +export interface TakeoffTask extends TaskBase, TakeoffTaskParameters { + taskType: 'takeoff'; +} + +function isTakeoffTask(task: Task): boolean { + return task.taskType === 'takeoff' + && isHexFloat(task.lat) + && isHexFloat(task.lng) + && isHexFloat(task.alt) + + && task.loiter + && isHexFloat(task.loiter.lat) + && isHexFloat(task.loiter.lng) + && isHexFloat(task.loiter.alt) + && isHexFloat(task.loiter.radius) + && isHexFloat(task.loiter.direction); +} + +interface LoiterTaskParameters { + lat: HexFloat; + lng: HexFloat; + alt: HexFloat; + radius: HexFloat; + direction: HexFloat; +} + +export interface LoiterTask extends TaskBase, LoiterTaskParameters { + taskType: 'loiter'; +} + +function isLoiterTask(task: Task): boolean { + return task.taskType === 'loiter' + && isHexFloat(task.lat) + && isHexFloat(task.lng) + && isHexFloat(task.alt) + && isHexFloat(task.radius) + && isHexFloat(task.direction); +} + +// eslint-disable-next-line @typescript-eslint/interface-name-prefix +interface ISRSearchTaskParameters { + alt: HexFloat; + waypoints: [ + { + lat: HexFloat; + lng: HexFloat; + }, + { + lat: HexFloat; + lng: HexFloat; + }, + { + lat: HexFloat; + lng: HexFloat; + } + ]; +} + +// eslint-disable-next-line @typescript-eslint/interface-name-prefix +export interface ISRSearchTask extends TaskBase, ISRSearchTaskParameters { + taskType: 'isrSearch'; +} + +function isISRSearchTask(task: Task): boolean { + return task.taskType === 'isrSearch' + && isHexFloat(task.alt) + + && task.waypoints && task.waypoints.length === 3 + && task.waypoints.every( + (waypoint): boolean => isHexFloat(waypoint.lat) && isHexFloat(waypoint.lng), + ); +} + +interface PayloadDropTaskParameters { + waypoints: [ + { + lat: HexFloat; + lng: HexFloat; + alt: HexFloat; + }, + { + lat: HexFloat; + lng: HexFloat; + alt: HexFloat; + } + ]; +} + +export interface PayloadDropTask extends TaskBase, PayloadDropTaskParameters { + taskType: 'payloadDrop'; +} + +function isPayloadDropTask(task: Task): boolean { + return task.taskType === 'payloadDrop' + && task.waypoints && task.waypoints.length === 2 + && task.waypoints.every( + (waypoint): boolean => isHexFloat(waypoint.lat) + && isHexFloat(waypoint.lng) + && isHexFloat(waypoint.alt), + ); +} + +interface LandTaskParameters { + waypoints: [ + { + lat: HexFloat; + lng: HexFloat; + alt: HexFloat; + }, + { + lat: HexFloat; + lng: HexFloat; + alt: HexFloat; + } + ]; +} + +export interface LandTask extends TaskBase, LandTaskParameters { + taskType: 'land'; +} + +function isLandTask(task: Task): boolean { + return task.taskType === 'land' + && task.waypoints && task.waypoints.length === 2 + && task.waypoints.every( + (waypoint): boolean => isHexFloat(waypoint.lat) + && isHexFloat(waypoint.lng) + && isHexFloat(waypoint.alt), + ); +} + +interface UGVRetrieveTargetTaskParameters extends TaskBase { + lat: HexFloat; + lng: HexFloat; +} + +export interface UGVRetrieveTargetTask extends TaskBase, UGVRetrieveTargetTaskParameters { + taskType: 'retrieveTarget'; +} + +function isUGVRetrieveTargetTask(task: Task): boolean { + if (task.taskType !== 'retrieveTarget') return false; + + const retrieveTargetTask = task as UGVRetrieveTargetTask; + + return isHexFloat(retrieveTargetTask.lat) && isHexFloat(retrieveTargetTask.lng); +} + +interface DeliverTargetTaskParameters { + taskType: 'deliverTarget'; + + lat: HexFloat; + lng: HexFloat; +} + +export interface DeliverTargetTask extends TaskBase, DeliverTargetTaskParameters { + taskType: 'deliverTarget'; +} + +function isDeliverTargetTask(task: Task): boolean { + return task.taskType === 'deliverTarget' + && isHexFloat(task.lat) + && isHexFloat(task.lng); +} + +// UUVRetrieveTargetTask does not have any parameters. + +export interface UUVRetrieveTargetTask extends TaskBase { + taskType: 'retrieveTarget'; +} + +function isUUVRetrieveTargetTask(task: Task): boolean { + if (task.taskType !== 'retrieveTarget') return false; + + // Cast task to UGV retrieve to ensure that this task is not UGV task, and is UUV. + const retrieveTargetTask = task as UGVRetrieveTargetTask; + + // Check if values for lat and lng are undefined, since UUV version should not have these. + return !retrieveTargetTask.lat && !retrieveTargetTask.lng; +} + +export interface QuickScanTaskParameters { + waypoints: [ + { + lat: HexFloat; + lng: HexFloat; + }, + { + lat: HexFloat; + lng: HexFloat; + }, + { + lat: HexFloat; + lng: HexFloat; + }, + { + lat: HexFloat; + lng: HexFloat; + } + ]; +} + +export interface QuickScanTask extends TaskBase, QuickScanTaskParameters { + taskType: 'quickScan'; +} + +function isQuickScanTask(task: Task): boolean { + return task.taskType === 'quickScan' + && task.waypoints && task.waypoints.length === 4 + && task.waypoints.every( + (waypoint): boolean => isHexFloat(waypoint.lat) && isHexFloat(waypoint.lng), + ); +} + +/* + * DetailedSearchTask parameters are provided by QuickScan and are generated + * autonomously, no need for user interaction. + */ + +export interface DetailedSearchTask extends TaskBase { + taskType: 'detailedSearch'; + + lat: HexFloat; + lng: HexFloat; +} + +function isDetailedSearchTask(task: Task): boolean { + return task.taskType === 'detailedSearch' + && isHexFloat(task.lat) + && isHexFloat(task.lng); +} + +export type Task = TakeoffTask | LoiterTask | ISRSearchTask | PayloadDropTask | LandTask +| UGVRetrieveTargetTask | DeliverTargetTask | UUVRetrieveTargetTask | QuickScanTask +| DetailedSearchTask; + +/** + * Checks if an object is a task. To be a task, the object must match one of the tasks above. + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function isTask(task: { [key: string]: any }): boolean { + if (!task.taskType) return false; + + const specificTask = task as Task; + + return isTakeoffTask(specificTask) + || isLoiterTask(specificTask) + || isISRSearchTask(specificTask) + || isPayloadDropTask(specificTask) + || isLandTask(specificTask) + || isUGVRetrieveTargetTask(specificTask) + || isDeliverTargetTask(specificTask) + || isUUVRetrieveTargetTask(specificTask) + || isQuickScanTask(specificTask) + || isDetailedSearchTask(specificTask); +} + +export const taskTypeGuard = { + isTakeoffTask, + isLoiterTask, + isISRSearchTask, + isPayloadDropTask, + isLandTask, + isUGVRetrieveTargetTask, + isDeliverTargetTask, + isUUVRetrieveTargetTask, + isQuickScanTask, + isDetailedSearchTask, + isTask, +}; + +// eslint-disable-next-line @typescript-eslint/interface-name-prefix +export interface ISRSearchMissionParameters { + takeoff: TakeoffTaskParameters; + isrSearch: ISRSearchTaskParameters; + land: LandTaskParameters; +} + +export function isISRSearchMissionParameters(parameters: MissionParameters): boolean { + return 'takeoff' in parameters && 'isrSearch' in parameters && 'land' in parameters; +} + +export interface VTOLSearchMissionParameters { + quickScan: QuickScanTaskParameters; +} + +export function isVTOLSearchMissionParameters(parameters: MissionParameters): boolean { + return 'quickScan' in parameters; +} + +export interface PayloadDropMissionParameters { + takeoff: TakeoffTaskParameters; + payloadDrop: PayloadDropTaskParameters; + land: LandTaskParameters; +} + +export function isPayloadDropMissionParameters(parameters: MissionParameters): boolean { + return 'takeoff' in parameters && 'payloadDrop' in parameters && 'land' in parameters; +} + +export interface UGVRetreiveMissionParameters { + retrieveTarget: UGVRetreiveMissionParameters; + deliverTarget: DeliverTargetTaskParameters; +} + +export function isUGVRetreiveMissionParameters(parameters: MissionParameters): boolean { + return 'retrieveTarget' in parameters && 'deliverTarget' in parameters; +} + +// UUVRetrieveMission does not need parameters, as its one task does not need any. + +export function isUUVRetrieveMissionParameters(parameters: MissionParameters): boolean { + return Object.keys(parameters).length === 0; +} + +export type MissionParameters = ISRSearchMissionParameters | VTOLSearchMissionParameters +| PayloadDropTaskParameters | UGVRetreiveMissionParameters | {}; + +export const missionParametersTypeGuard = { + isISRSearchMissionParameters, + isVTOLSearchMissionParameters, + isPayloadDropMissionParameters, + isUGVRetreiveMissionParameters, + isUUVRetrieveMissionParameters, +}; + +/** + * Options for the mission. All variables are optional so the UI should + * be able to detect which of the variables are required or not. + */ +export interface MissionOptions { + isrSearch?: { + /** + * Will not require takeoff parameters if this is true. + */ + noTakeoff: boolean; + + /** + * Will not require land parameters if this is true. + */ + noLand: boolean; + }; + + payloadDrop?: { + /** + * Will not require takeoff parameters if this is true. + */ + noTakeoff: boolean; + + /** + * Will not require land parameters if this is true. + */ + noLand: boolean; + }; +} + +/** + * Type of information that should be passed from mission window to the Orchestrator. + */ +export interface MissionInformation { + /** + * Parameters for the mission. + */ + parameters?: MissionParameters; + + /** + * Options for the mission. + */ + options: MissionOptions; +} + +export type MissionName = 'isrSearch' | 'vtolSearch' | 'payloadDrop' | 'ugvRetrieve' | 'uuvRetrieve'; + +export interface MissionDescription { + missionName: MissionName; + activeVehicleMapping: { [vehicleId: number]: JobType }; + information: MissionInformation; +} + +interface MessageBase { + /** + * Type of message. + */ + type: string; +} + +/* + * Definitions for all messages from GCS to vehicles. No need for type guards, TypeScript + * will handle all of it as we write up the message. + * + * MessageHex types are the same as their normal Message class, however, their + */ + +export interface StartMessage extends MessageBase { + type: 'start'; + + /** + * Name of job to perform. + */ + jobType: JobType; + + /** + * Any other information the vehicle should have before performing the job. + */ + options?: any; // eslint-disable-line @typescript-eslint/no-explicit-any +} + +function isStartMessage(message: Message): boolean { + return message.type === 'start' + && vehicleConfig.isValidJobType(message.jobType); +} + +export interface AddMissionMessage extends MessageBase { + type: 'addMission'; + + /** + * Information related to accomplishing specific job. + */ + missionInfo: Task; +} + +function isAddMissionMessage(message: Message): boolean { + return message.type === 'addMission' + && message.missionInfo && isTask(message.missionInfo); +} + +export interface PauseMessage extends MessageBase { + type: 'pause'; +} + +function isPauseMessage(message: Message): boolean { + return message.type === 'pause'; +} + +export interface ResumeMessage extends MessageBase { + type: 'resume'; +} + +function isResumeMessage(message: Message): boolean { + return message.type === 'resume'; +} + +export interface StopMessage extends MessageBase { + type: 'stop'; +} + +function isStopMessage(message: Message): boolean { + return message.type === 'stop'; +} + +export interface ConnectionAckMessage extends MessageBase { + type: 'connectionAck'; +} + +function isConnectionAckMessage(message: Message): boolean { + return message.type === 'connectionAck'; +} + +// Definitions for all messages from vehicles to GCS. + +export interface UpdateMessage extends MessageBase { + type: 'update'; + + /** + * Latitude of the vehicle. + */ + lat: HexFloat; + + /** + * Longitude of the vehicle. + */ + lng: HexFloat; + + /** + * Altitude of the vehicle. + */ + alt?: HexFloat; + + /** + * Current vehicle heading. Value is in degrees. + */ + heading?: HexFloat; + + /** + * Current battery of the vehicle, expressed as a decimal. Will vary from 0 to 1. + */ + battery?: HexFloat; + + /** + * Status of the vehicle. + */ + status: VehicleStatus; + + /** + * Message generated if vehicle is in an error state. + * + * It is best practice to provide this value to be able to see why the the vehicle's + * status is error. + */ + errorMessage?: string; +} + +function isUpdateMessage(message: Message): boolean { + const mandatoryCheck = message.type === 'update' + && isHexFloat(message.lat) + && isHexFloat(message.lng) + && isVehicleStatus(message.status); + + if (!mandatoryCheck) return false; + + const updateMessage = message as UpdateMessage; + + if (updateMessage.alt && !isHexFloat(updateMessage.alt)) return false; + if (updateMessage.heading && !isHexFloat(updateMessage.heading)) return false; + if (updateMessage.battery && !isHexFloat(updateMessage.battery)) return false; + + return true; +} + +export interface POIMessage extends MessageBase { + type: 'poi'; + + /** + * Latitude of the point of interest. + */ + lat: HexFloat; + + /** + * Longitude of the point of interest. + */ + lng: HexFloat; +} + +function isPOIMessage(message: Message): boolean { + return message.type === 'poi' + && isHexFloat(message.lat) + && isHexFloat(message.lng); +} + +export interface CompleteMessage extends MessageBase { + type: 'complete'; +} + +function isCompleteMessage(message: Message): boolean { + return message.type === 'complete'; +} + +export interface ConnectMessage extends MessageBase { + type: 'connect'; + + /** + * List of all the different jobs the vehicle is capable of doing. + */ + jobsAvailable: JobType[]; +} + +export function isConnectMessage(message: Message): boolean { + return message.type === 'connect' + + // Check if jobsAvailable is a string array of valid job types. + && message.jobsAvailable.every(vehicleConfig.isValidJobType); +} + +// Definitions for all other message types. + +export interface AcknowledgementMessage extends MessageBase { + type: 'ack'; + + /** + * ID of the message that is being acknowledged. + */ + ackid: number; +} + +export function isAcknowledgementMessage(message: Message): boolean { + return message.type === 'ack' + && !Number.isNaN(message.ackid); +} + +export interface BadMessage extends MessageBase { + type: 'badMessage'; + + /** + * Description of why message was bad. + * + * It is best practice to provide this value to be able to see why the message received + * is bad. + */ + error?: string; +} + +export function isBadMessage(message: Message): boolean { + return message.type === 'badMessage'; +} + +export type Message = StartMessage | AddMissionMessage | PauseMessage | ResumeMessage | StopMessage +| ConnectionAckMessage | UpdateMessage | POIMessage | CompleteMessage | ConnectMessage +| AcknowledgementMessage | BadMessage; + +/** + * Logic for this function is slightly different from isTask since the GCS does not need to know + * which task it is sending to the vehicle. It just needs to know that the task is a valid task, + * and contains a valid taskType and task information for a task. + * + * For this function, GCS does need to know what messages it receives. GCS needs to check if + * an object is a message (if the object has a type field then it is a message). If not, GCS + * can alert the sender that the object sent is not a message. + * + * After this function, the GCS will check which message the sender has sent with the functions + * above, and if the functions fail (which means that the fields that have been included with + * the message are wrong), then the GCS will tell the sender that the format for the message + * is wrong. + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function isMessage(message: { [key: string]: any }): boolean { + return message.type && typeof message.type === 'string' && [ + 'start', + 'addMission', + 'pause', + 'resume', + 'stop', + 'connectionAck', + 'update', + 'poi', + 'complete', + 'connect', + 'ack', + 'badMessage', + ].includes(message.type.toLowerCase()); +} + +export type JSONMessage = Message & { + id: number; + tid: number; + sid: number; + time: number; +}; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function isJSONMessage(message: { [key: string]: any }): boolean { + if (!isMessage(message)) return false; + + // Check if id is a number. + const idCheck = message.id !== '' && message.id !== true && !Number.isNaN(message.id); + + // Check if tid is a number and is valid. + const tidCheck = !Number.isNaN(message.tid) + && vehicleConfig.vehicleInfos[message.tid] !== undefined; + + // Check if sid is a number and is valid. + const sidCheck = !Number.isNaN(message.sid) + && vehicleConfig.vehicleInfos[message.sid] !== undefined; + + return idCheck && tidCheck && sidCheck; +} + +export const messageTypeGuard = { + isStartMessage, + isAddMissionMessage, + isPauseMessage, + isResumeMessage, + isStopMessage, + isConnectionAckMessage, + isUpdateMessage, + isPOIMessage, + isCompleteMessage, + isConnectMessage, + isAcknowledgementMessage, + isBadMessage, + isMessage, + isJSONMessage, +}; + +export default { taskTypeGuard, messageTypeGuard }; diff --git a/src/types/types.ts b/src/types/types.ts new file mode 100644 index 00000000..9e00b2d9 --- /dev/null +++ b/src/types/types.ts @@ -0,0 +1,144 @@ +import { Moment } from 'moment'; + +/** + * Status of the vehicle, lets us know of where the vehicle is in its mission. + */ +export type VehicleStatus = 'ready' | 'error' | 'disconnected' | 'waiting' | 'running' | 'paused'; + +export function isVehicleStatus(status: string): boolean { + return status === 'ready' || status === 'error' || status === 'disconnected' || status === 'waiting' || status === 'running' || status === 'paused'; +} + +/** + * Props with theme as its child. Feel free to extend this prop. + */ +export interface ThemeProps { + /** + * "Light" for light theme, "dark" for dark theme. + */ + theme: 'light' | 'dark'; +} + +export function isThemeProps(props: { theme: string }): boolean { + return props.theme === 'light' || props.theme === 'dark'; +} + +/** + * Types for all messages. Consists of "success", "failure", "progress", or "". + */ +export type MessageType = '' | 'success' | 'failure' | 'progress'; + +export function isMessageType(type: string): boolean { + return type === '' || type === 'success' || type === 'failure' || type === 'progress'; +} + +/** + * Latitude, longitude and zoom. + */ +export interface LatLngZoom { + /** + * Latitude of center of map. + */ + lat: number; + + /** + * Longitude of center of map. + */ + lng: number; + + /** + * Zoom level of map. + */ + zoom?: number; +} + +/** + * Vehicle object for all classes to use. This is necessary because Vehicle class will + * be uncasted when being sent through ipcRenderer. + */ +export interface VehicleObject { + /** + * ID of the vehicle. + */ + vehicleId: number; + + /** + * Current status of the vehicle. + */ + status: VehicleStatus; + + /** + * Jobs of the vehicle. + */ + jobs: string[]; + + /** + * Current latitude of the vehicle. Starts at 0. + */ + lat: number; + + /** + * Current longitude of the vehicle. Starts at 0. + */ + lng: number; + + /** + * Current altitude of the vehicle. + */ + alt?: number; + /** + * + * Current battery of the vehicle, expressed as a decimal. Will vary from 0 to 1. + */ + battery?: number; + + /** + * Current vehicle heading. Value is in degrees. + */ + heading?: number; +} + +/** + * Data contents for information that is loaded from a configuration file. + */ +export interface FileLoadOptions { + /** + * Information related to the map. + */ + map: LatLngZoom; +} + +/** + * Object structure for information stored into a configuration file. + */ +export interface FileSaveOptions { + /** + * Filepath of the configuration file being saved. + */ + filePath: string; + + /** + * Data contents. Will be modified by classes through the "loadConfig" notification. + */ + data: FileLoadOptions; +} + +/** + * Structure for a message displayed in log container. + */ +export interface LogMessage { + /** + * The type of the message. Defines the color the message will be printed in. + */ + type?: MessageType; + + /** + * The content of the message. + */ + message: string; + + /** + * The time was received and logged. + */ + time?: Moment; +} diff --git a/src/util/ipc.ts b/src/util/ipc.ts index 4cea0864..bd6a66a2 100644 --- a/src/util/ipc.ts +++ b/src/util/ipc.ts @@ -72,6 +72,7 @@ function postConnectToVehicle(jsonMessage: Message.JSONMessage, newMessage: bool * * Files that take this notification: * - renderer/mainWindow/map/MapContainer + * - renderer/missionWindow/extra/CreateBoundingBox */ function postCreateBoundingBoxes( ...boundingBoxes: { name: string; color?: string; bounds?: BoundingBoxBounds}[] @@ -361,6 +362,8 @@ function postToggleTheme(): void { * * Files that take this notification: * - renderer/missionWindow/parameter/ISRSearch + * - renderer/missionWindow/parameter/VTOLSearch + * - renderer/missionWindow/parameter/PayloadDrop */ function postUnlockParameterInputs(waypointType: string): void { ipcRenderer.send('post', 'unlockParameterInputs', waypointType); @@ -453,6 +456,8 @@ function postUpdateVehicles(...vehicles: VehicleObject[]): void { * Files that take this notification: * - renderer/mainWindow/map/MapContainer * - renderer/missionWindow/parameters/ISRSearch + * - renderer/missionWindow/parameters/VTOLSearch + * - renderer/missionWindow/parameters/PayloadDrop * * @param updateMap Set this to true, except from the marker's drag event. */ From e45d81647f3200bd3dbc619a8065fc43816d2933 Mon Sep 17 00:00:00 2001 From: Marlon Aquino Date: Fri, 26 Apr 2019 19:07:10 -0700 Subject: [PATCH 124/152] Updated mission parameters for UUVRescue Task Parameters are also updated --- .../missionWindow/parameter/UUVRescue.tsx | 164 +++++++++++++++++- src/types/missionInformation.ts | 2 +- src/types/task.ts | 2 - src/util/ipc.ts | 1 + 4 files changed, 160 insertions(+), 9 deletions(-) diff --git a/src/renderer/missionWindow/parameter/UUVRescue.tsx b/src/renderer/missionWindow/parameter/UUVRescue.tsx index ebe3af20..cf913464 100644 --- a/src/renderer/missionWindow/parameter/UUVRescue.tsx +++ b/src/renderer/missionWindow/parameter/UUVRescue.tsx @@ -1,13 +1,165 @@ -import React, { ReactNode } from 'react'; +import { Event, ipcRenderer } from 'electron'; +import React, { Component, ReactNode } from 'react'; import { missionName } from '../../../common/missions/UUVRescue'; -export const name = 'UUV Rescue'; +import { Location } from '../../../static/index'; -export function UUVRescue(): ReactNode { - return ( -
UUV Rescue
- ); +import { VehicleObject } from '../../../types/vehicle'; + +import ipc from '../../../util/ipc'; +import { readyToStart } from '../../../util/parameter'; + +import CreateWaypointButton from '../extra/CreateWaypointButton'; + +type UUVChecklistType = 'uuvRetrieveTargetLat' | 'uuvRetrieveTargetLng'; + +const checklistCache: { [check in UUVChecklistType]: number | undefined } = { + uuvRetrieveTargetLat: undefined, + uuvRetrieveTargetLng: undefined, +}; + +type UUVWaypointType = 'retrieveTarget'; + +type Locked = { [waypointType in UUVWaypointType]: boolean } & { + retrieveTarget: boolean; +} + +const lockedCache: Locked = { + retrieveTarget: true, +}; + +export interface UUVRescueProps { + vehicles: { [vehicleId: number]: VehicleObject }; +} + +interface State { + checklist: { [check in UUVChecklistType]: number | undefined }; + + ready: boolean; + + locked: Locked; +} + +export class UUVRescue extends Component { + public constructor(props: UUVRescueProps) { + super(props); + + this.state = { + checklist: checklistCache, + ready: false, + locked: lockedCache, + }; + + this.onChange = this.onChange.bind(this); + this.updateWaypoints = this.updateWaypoints.bind(this); + this.updateChecklist = this.updateChecklist.bind(this); + this.unlockParameterInputs = this.unlockParameterInputs.bind(this); + this.readyToStart = this.readyToStart.bind(this); + } + + public componentDidMount(): void { + ipcRenderer.on('updateWaypoints', (__: Event, _: boolean, ...waypoints: { name: string; location: Location }[]): void => this.updateWaypoints(...waypoints)); + ipcRenderer.on('unlockParameterInputs', (_: Event, waypointType: string): void => this.unlockParameterInputs(waypointType)); + } + + + private onChange(event: React.ChangeEvent): void { + const { checklist } = this.state; + + if (!(event.target.name in checklist)) return; + + const name = event.target.name as UUVChecklistType; + const value = parseInt(event.target.value, 10) || 0; + + switch (name) { + case 'uuvRetrieveTargetLat': + ipc.postUpdateWaypoints(true, { name: 'Retrieve Target', location: { lat: value, lng: checklist.uuvRetrieveTargetLng as number } }); + break; + + case 'uuvRetrieveTargetLng': + ipc.postUpdateWaypoints(true, { name: 'Retrieve Target', location: { lat: checklist.uuvRetrieveTargetLat as number, lng: value } }); + break; + + default: + this.updateChecklist({ [name]: value }); + break; + } + } + + private updateWaypoints(...waypoints: { name: string; location: Location }[]): void { + const checks: { [checkName in UUVChecklistType]?: number } = {}; + + waypoints.forEach((waypoint): void => { + switch (waypoint.name) { + case 'Retrieve Target': + checks.uuvRetrieveTargetLat = waypoint.location.lat; + checks.uuvRetrieveTargetLng = waypoint.location.lng; + break; + + default: break; + } + }); + + this.updateChecklist(checks); + } + + private updateChecklist(checks: { [checklistType in UUVChecklistType]?: number }): void { + const { checklist: newChecklist, ready } = this.state; + + Object.keys(checks).forEach((checklistTypeString): void => { + const checklistType = checklistTypeString as UUVChecklistType; + const value = checks[checklistType]; + + newChecklist[checklistType] = value; + }); + + if (ready || readyToStart(this)) { + ipc.postUpdateInformation({ + missionName: 'uuvRescue', + parameters: { + retrieveTarget: { + lat: newChecklist.uuvRetrieveTargetLat as number, + lng: newChecklist.uuvRetrieveTargetLng as number, + }, + }, + }); + } + + this.setState({ checklist: newChecklist }); + } + + private unlockParameterInputs(waypointType: string): void { + const { locked: newLocked } = this.state; + + if (waypointType in newLocked) { + newLocked[waypointType as UUVWaypointType] = false; + } + + this.setState({ locked: newLocked }); + } + + private readyToStart(): boolean { + const { checklist } = this.state; + + const ready = Object.values(checklist).every((value): boolean => value !== undefined); + + if (ready) this.setState({ ready }); + return ready; + } + + public render(): ReactNode { + const { checklist, locked } = this.state; + + return ( +
+

UUV Retrieve Target

+ + + +
+ ); + } } export default { diff --git a/src/types/missionInformation.ts b/src/types/missionInformation.ts index 2d952de7..5202d17f 100644 --- a/src/types/missionInformation.ts +++ b/src/types/missionInformation.ts @@ -109,7 +109,7 @@ function isPayloadDropInformation(information: Information): boolean { export interface UGVRescueInformation extends InformationBase { missionName: 'ugvRescue'; parameters: { - retrieveTarget: Task.UGVRetrieveTargetTask; + retrieveTarget: Task.UGVRetrieveTargetTaskParameters; deliverTarget: Task.DeliverTargetTaskParameters; }; } diff --git a/src/types/task.ts b/src/types/task.ts index 63779259..2fc7ba23 100644 --- a/src/types/task.ts +++ b/src/types/task.ts @@ -171,8 +171,6 @@ function isUGVRetrieveTargetTask(task: Task): boolean { * Do not import this outside of types/missionInformation. */ export interface DeliverTargetTaskParameters { - taskType: 'deliverTarget'; - lat: number; lng: number; } diff --git a/src/util/ipc.ts b/src/util/ipc.ts index bd6a66a2..675909b2 100644 --- a/src/util/ipc.ts +++ b/src/util/ipc.ts @@ -458,6 +458,7 @@ function postUpdateVehicles(...vehicles: VehicleObject[]): void { * - renderer/missionWindow/parameters/ISRSearch * - renderer/missionWindow/parameters/VTOLSearch * - renderer/missionWindow/parameters/PayloadDrop + * - renderer/missionWindow/parameters/UUVRescue * * @param updateMap Set this to true, except from the marker's drag event. */ From 2a3d95810a45696b72eaf849c0bbed83918f721b Mon Sep 17 00:00:00 2001 From: Paul Matthew Barrameda Date: Fri, 26 Apr 2019 21:39:34 -0700 Subject: [PATCH 125/152] Logic to Orchestrator/Mission + etc Big change to Orchestrator to provide an actual function callback, instead of passing the function, as passing the function will NOT update the instance variables of the Orchestrator. Big note for the future. Initialize Mission after constructing it from the Ochestrator, as its abstract values do not apply until the constructor is done. Before, the mission name would never be applied since the initialize would automatically be called from the Orchestrator. Simplified updateVehicle fixtures to only emit "ready" status to help test missions. Other fixes to Mission class and subclasses that were buggy. Will continue to fix these issues tonight --- src/common/Orchestrator.ts | 19 +- src/common/missions/ISRSearch.ts | 19 +- src/common/missions/PayloadDrop.ts | 19 +- src/common/missions/UGVRescue.ts | 19 +- src/common/missions/UUVRescue.ts | 16 +- src/common/missions/VTOLSearch.ts | 17 +- src/common/missions/index.ts | 4 +- src/common/struct/Mission.ts | 23 +- .../fixtures/updateVehiclesFixtures.ts | 5 +- src/renderer/missionWindow/MissionWindow.tsx | 45 +- src/types/messages.ts | 754 ------------------ src/types/task.ts | 2 +- src/types/types.ts | 144 ---- 13 files changed, 77 insertions(+), 1009 deletions(-) delete mode 100644 src/types/messages.ts delete mode 100644 src/types/types.ts diff --git a/src/common/Orchestrator.ts b/src/common/Orchestrator.ts index 0da525da..a767cc32 100644 --- a/src/common/Orchestrator.ts +++ b/src/common/Orchestrator.ts @@ -96,15 +96,15 @@ class Orchestrator { ipcRenderer.on('startMissions', ( _: Event, missions: MissionInformation.Information[], - options: MissionInformation.MissionOptions, activeVehicleMapping: MissionInformation.ActiveVehicleMapping, + options: MissionInformation.MissionOptions, requireConfirmation: boolean, ): void => this.startMissions(missions, activeVehicleMapping, options, requireConfirmation)); - ipcRenderer.on('pauseMission', this.pauseMission); - ipcRenderer.on('resumeMission', this.resumeMission); - ipcRenderer.on('startNextMission', this.startNextMission); + ipcRenderer.on('pauseMission', (): void => { this.pauseMission(); }); + ipcRenderer.on('resumeMission', (): void => { this.resumeMission(); }); + ipcRenderer.on('startNextMission', (): void => { this.startNextMission(); }); ipcRenderer.on('completeMission', (_: Event, missionName: string, completionParameters: Task.TaskParameters[]): void => this.completeMission(missionName, completionParameters)); - ipcRenderer.on('stopMissions', this.stopMissions); + ipcRenderer.on('stopMissions', (): void => { this.stopMissions(); }); } /** @@ -393,15 +393,14 @@ class Orchestrator { return; } - const mission = missionObj; - - this.currentMissionIndex += 1; - this.currentMission = new mission.constructor( + this.currentMission = new missionObj.Mission( this.vehicles, this.missions[this.currentMissionIndex], this.activeVehicleMapping, this.options, ); + + this.currentMission.initialize(); } /** @@ -413,7 +412,7 @@ class Orchestrator { this.currentMission = null; this.missions = []; this.running = false; - this.requireConfirmation = false; + this.requireConfirmation = true; this.activeVehicleMapping = null; this.options = null; } diff --git a/src/common/missions/ISRSearch.ts b/src/common/missions/ISRSearch.ts index 4241cda3..e969b70c 100644 --- a/src/common/missions/ISRSearch.ts +++ b/src/common/missions/ISRSearch.ts @@ -10,8 +10,6 @@ import { Task, TaskParameters } from '../../types/task'; import ipc from '../../util/ipc'; import { getBoundingBox } from '../../util/util'; -import Vehicle from '../struct/Vehicle'; - export const missionName: MissionInformation.MissionName = 'isrSearch'; export const jobTypes: JobType[] = ['isrSearch']; @@ -23,25 +21,14 @@ export class ISRSearch extends Mission { protected addTaskCompare = {}; - protected information: MissionInformation.ISRSearchInformation; - /** * List of point of interests. */ private missionData: Location[] = []; - public constructor( - vehicles: { [vehicleId: number]: Vehicle }, - information: MissionInformation.ISRSearchInformation, - activeVehicleMapping: MissionInformation.ActiveVehicleMapping, - options: MissionInformation.MissionOptions, - ) { - super(vehicles, information, activeVehicleMapping, options); - this.information = information; - } - protected generateTasks(): DictionaryList | undefined { - const missionParameters = this.information.parameters; + const information = this.information as MissionInformation.ISRSearchInformation; + const missionParameters = information.parameters; const tasks = new DictionaryList(); if (!this.options.isrSearch.noTakeoff) { @@ -118,5 +105,5 @@ export class ISRSearch extends Mission { export default { missionName, jobTypes, - constructor: ISRSearch, + Mission: ISRSearch, }; diff --git a/src/common/missions/PayloadDrop.ts b/src/common/missions/PayloadDrop.ts index 1b7bec6b..e442e629 100644 --- a/src/common/missions/PayloadDrop.ts +++ b/src/common/missions/PayloadDrop.ts @@ -9,8 +9,6 @@ import { Task, TaskParameters } from '../../types/task'; import ipc from '../../util/ipc'; -import Vehicle from '../struct/Vehicle'; - export const missionName: MissionInformation.MissionName = 'payloadDrop'; export const jobTypes: JobType[] = ['payloadDrop']; @@ -22,25 +20,14 @@ export class PayloadDrop extends Mission { protected addTaskCompare = {}; - protected information: MissionInformation.PayloadDropInformation; - /** * Point of interest. */ private missionData: Location | null = null; - public constructor( - vehicles: { [vehicleId: number]: Vehicle }, - information: MissionInformation.PayloadDropInformation, - activeVehicleMapping: MissionInformation.ActiveVehicleMapping, - options: MissionInformation.MissionOptions, - ) { - super(vehicles, information, activeVehicleMapping, options); - this.information = information; - } - protected generateTasks(): DictionaryList | undefined { - const missionParameters = this.information.parameters; + const information = this.information as MissionInformation.PayloadDropInformation; + const missionParameters = information.parameters; const tasks = new DictionaryList(); if (!this.options.payloadDrop.noTakeoff) { @@ -100,5 +87,5 @@ export class PayloadDrop extends Mission { export default { missionName, jobTypes, - constructor: PayloadDrop, + Mission: PayloadDrop, }; diff --git a/src/common/missions/UGVRescue.ts b/src/common/missions/UGVRescue.ts index bc2d19ab..0440b7c0 100644 --- a/src/common/missions/UGVRescue.ts +++ b/src/common/missions/UGVRescue.ts @@ -6,8 +6,6 @@ import { JobType } from '../../static/index'; import * as MissionInformation from '../../types/missionInformation'; import { Task, TaskParameters } from '../../types/task'; -import Vehicle from '../struct/Vehicle'; - export const missionName: MissionInformation.MissionName = 'ugvRescue'; export const jobTypes: JobType[] = ['ugvRescue']; @@ -19,20 +17,9 @@ export class UGVRescue extends Mission { protected addTaskCompare = {}; - protected information: MissionInformation.UGVRescueInformation; - - public constructor( - vehicles: { [vehicleId: number]: Vehicle }, - information: MissionInformation.UGVRescueInformation, - activeVehicleMapping: MissionInformation.ActiveVehicleMapping, - options: MissionInformation.MissionOptions, - ) { - super(vehicles, information, activeVehicleMapping, options); - this.information = information; - } - protected generateTasks(): DictionaryList | undefined { - const missionParameters = this.information.parameters; + const information = this.information as MissionInformation.UGVRescueInformation; + const missionParameters = information.parameters; const tasks = new DictionaryList(); tasks.push('ugvRescue', { @@ -57,5 +44,5 @@ export class UGVRescue extends Mission { export default { missionName, jobTypes, - constructor: UGVRescue, + Mission: UGVRescue, }; diff --git a/src/common/missions/UUVRescue.ts b/src/common/missions/UUVRescue.ts index b74a3acf..ce352735 100644 --- a/src/common/missions/UUVRescue.ts +++ b/src/common/missions/UUVRescue.ts @@ -6,8 +6,6 @@ import { JobType } from '../../static/index'; import * as MissionInformation from '../../types/missionInformation'; import { Task, TaskParameters } from '../../types/task'; -import Vehicle from '../struct/Vehicle'; - export const missionName: MissionInformation.MissionName = 'uuvRescue'; export const jobTypes: JobType[] = ['uuvRescue']; @@ -19,18 +17,6 @@ export class UUVRescue extends Mission { protected addTaskCompare = {}; - protected information: MissionInformation.UUVRescueInformation; - - public constructor( - vehicles: { [vehicleId: number]: Vehicle }, - information: MissionInformation.UUVRescueInformation, - activeVehicleMapping: MissionInformation.ActiveVehicleMapping, - options: MissionInformation.MissionOptions, - ) { - super(vehicles, information, activeVehicleMapping, options); - this.information = information; - } - // eslint-disable-next-line class-methods-use-this protected generateTasks(): DictionaryList | undefined { const tasks = new DictionaryList(); @@ -51,5 +37,5 @@ export class UUVRescue extends Mission { export default { missionName, jobTypes, - constructor: UUVRescue, + Mission: UUVRescue, }; diff --git a/src/common/missions/VTOLSearch.ts b/src/common/missions/VTOLSearch.ts index c494228f..c4680e08 100644 --- a/src/common/missions/VTOLSearch.ts +++ b/src/common/missions/VTOLSearch.ts @@ -37,23 +37,11 @@ export class VTOLSearch extends Mission { }, }; - protected information: MissionInformation.VTOLSearchInformation; - /** * Point of interest. */ private missionData: Location | null = null; - public constructor( - vehicles: { [vehicleId: number]: Vehicle }, - information: MissionInformation.VTOLSearchInformation, - activeVehicleMapping: MissionInformation.ActiveVehicleMapping, - options: MissionInformation.MissionOptions, - ) { - super(vehicles, information, activeVehicleMapping, options); - this.information = information; - } - private getDetailedSearchVehicle(): Vehicle | undefined { const vehicleIdString = Object.keys(this.activeVehicleMapping) .find((idString): boolean => { @@ -67,7 +55,8 @@ export class VTOLSearch extends Mission { } protected generateTasks(): DictionaryList | undefined { - const missionParameters = this.information.parameters; + const information = this.information as MissionInformation.VTOLSearchInformation; + const missionParameters = information.parameters; const tasks = new DictionaryList(); tasks.push('quickScan', { @@ -121,5 +110,5 @@ export class VTOLSearch extends Mission { export default { missionName, jobTypes, - constructor: VTOLSearch, + Mission: VTOLSearch, }; diff --git a/src/common/missions/index.ts b/src/common/missions/index.ts index bd6ee6d6..686fe80d 100644 --- a/src/common/missions/index.ts +++ b/src/common/missions/index.ts @@ -32,9 +32,9 @@ export interface MissionObject { jobTypes: JobType[]; /** - * Constructor of the mission. + * Mission class itself. */ - constructor: MissionWrapper; + Mission: MissionWrapper; } const missionObject: { [missionName in MissionName]: MissionObject } = { diff --git a/src/common/struct/Mission.ts b/src/common/struct/Mission.ts index 29fb2a38..420c08a6 100644 --- a/src/common/struct/Mission.ts +++ b/src/common/struct/Mission.ts @@ -126,8 +126,6 @@ export default abstract class Mission { return status === 'ready'; }); - - this.initialize(); } /** @@ -148,9 +146,9 @@ export default abstract class Mission { * Initializes the mission. Called from the Message's constructor. * Will set status to "initializing" if mission successfully initializes. */ - private initialize(): void { + public initialize(): void { if (this.status !== 'ready') { - this.stop(false, `Something wrong happend in ${this.missionName} mission`); + this.stop(false, `Something wrong happened in ${this.missionName} mission`); return; } @@ -174,7 +172,7 @@ export default abstract class Mission { return; } - const pendingAssignVehicleIds = Object.keys(this.activeVehicleMapping).map( + const pendingAssignVehicleIds = Object.keys(this.activeVehicleMapping[this.missionName]).map( (vehicleIdString): number => parseInt(vehicleIdString, 10), ); @@ -192,6 +190,8 @@ export default abstract class Mission { const onSuccess = (vehicleId: number): void => { pendingAssignVehicleIds.splice(pendingAssignVehicleIds.indexOf(vehicleId), 1); + console.log('sucess', vehicleId); + if (pendingAssignVehicleIds.length === 0) { ipc.postLogMessages({ type: 'success', @@ -206,6 +206,8 @@ export default abstract class Mission { * Callback when a vehicle fails to acknowledge the job assignment. */ const onDisconnect = (vehicleId: number): void => { + console.log('disconnect', vehicleId); + ipc.postStopSendingMessages(); this.stop(false, `Failed to assign job to ${(vehicleConfig.vehicleInfos[vehicleId] as VehicleInfo).name}, as it has disconnected`); }; @@ -214,6 +216,7 @@ export default abstract class Mission { * Callback when a vehicle performing a job enters an error state. */ const onError = (vehicleId: number, message?: string): void => { + console.log('error', vehicleId); ipc.postLogMessages({ type: 'failure', message: `${(vehicleConfig.vehicleInfos[vehicleId] as VehicleInfo).name} has entered an error state in ${this.missionName}: ${message || 'No error message specified'}`, @@ -243,6 +246,8 @@ export default abstract class Mission { return; } + console.log('LETS GO'); + const jobTasks = this.generateTasks(); if (!jobTasks) { this.stop(false, `No tasks were generated from ${this.missionName}`); @@ -284,7 +289,7 @@ export default abstract class Mission { } }); - const pendingAssignVehicleIds = Object.keys(this.activeVehicleMapping).map( + const pendingAssignVehicleIds = Object.keys(this.activeVehicleMapping[this.missionName]).map( (vehicleIdString): number => parseInt(vehicleIdString, 10), ); @@ -434,7 +439,7 @@ export default abstract class Mission { if (this.status !== 'ready') { if (this.status === 'initializing') ipc.postStopSendingMessages(); - Object.keys(this.activeVehicleMapping).forEach((vehicleIdString): void => { + Object.keys(this.activeVehicleMapping[this.missionName]).forEach((vehicleIdString): void => { const vehicleId = parseInt(vehicleIdString, 10); this.vehicles[vehicleId].stop(); }); @@ -482,6 +487,8 @@ export default abstract class Mission { * @param activeVehicleMapping User provided map of vehicle to their job type. */ private checkActiveVehicleMapping(): boolean { + if (Object.keys(this.activeVehicleMapping[this.missionName]).length === 0) return false; + const providedJobTypes = new Set( Object.values(this.activeVehicleMapping[this.missionName]), ); @@ -492,7 +499,7 @@ export default abstract class Mission { if (!hasRequiredJobTypes) return false; - const hasValidVehicleAssigned = Object.keys(this.activeVehicleMapping).map( + const hasValidVehicleAssigned = Object.keys(this.activeVehicleMapping[this.missionName]).map( (vehicleIdString): number => parseInt(vehicleIdString, 10), ).every((vehicleId): boolean => vehicleConfig.isValidVehicleId(vehicleId) && this.vehicles[vehicleId] diff --git a/src/renderer/fixtures/updateVehiclesFixtures.ts b/src/renderer/fixtures/updateVehiclesFixtures.ts index cd6b5023..adafb796 100644 --- a/src/renderer/fixtures/updateVehiclesFixtures.ts +++ b/src/renderer/fixtures/updateVehiclesFixtures.ts @@ -9,7 +9,6 @@ import { JobType, locationConfig } from '../../static/index'; import * as Message from '../../types/message'; -import { VehicleStatus } from '../../types/vehicle'; import ipc from '../../util/ipc'; @@ -55,8 +54,6 @@ let fixtures: Fixture[] = fixtureOptions.map( (fixtureOption): Fixture => ({ ...fixtureOption, ...locationConfig.startLocation }), ); -const status: VehicleStatus[] = ['ready', 'waiting', 'paused', 'running', 'error']; - let messageId = 0; function generateJSONMessage(vehicleId: number, message: Message.Message): Message.JSONMessage { @@ -98,7 +95,7 @@ function updateVehicles(): void { type: 'update', lat: fixture.lat, lng: fixture.lng, - status: status[Math.floor(Math.random() * status.length)], + status: 'ready', }), true); }); } diff --git a/src/renderer/missionWindow/MissionWindow.tsx b/src/renderer/missionWindow/MissionWindow.tsx index 9d029620..2235d0c2 100644 --- a/src/renderer/missionWindow/MissionWindow.tsx +++ b/src/renderer/missionWindow/MissionWindow.tsx @@ -10,6 +10,7 @@ import { ThemeProps } from '../../types/componentStyle'; import * as MissionInformation from '../../types/missionInformation'; import { VehicleObject, VehicleStatus } from '../../types/vehicle'; +import ipc from '../../util/ipc'; import { updateVehicles } from '../../util/util'; import ActiveVehicleMapping from './extra/ActiveVehicleMapping'; @@ -21,8 +22,6 @@ import UGVRescue from './parameter/UGVRescue'; import UUVRescue from './parameter/UUVRescue'; import VTOLSearch from './parameter/VTOLSearch'; -import ipc from '../../util/ipc'; - import './mission.css'; const Range = Slider.createSliderWithTooltip(Slider.Range); @@ -68,13 +67,15 @@ interface State { endMissionIndex: number; /** - * Current status of mission. All statuses will apply to mission except for - * "waiting", "error", and "disconnected". + * Current status of mission. + * - ready: no mission running + * - running: mission running + * - paused: mission paused * * A little different since when the first mission finishes, the status will go * to "next" instead of back to ready. From "next", it goes to "running", etc... */ - status: VehicleStatus | 'next'; + status: VehicleStatus | 'next' | 'finished'; /** * Current vehicles connected. @@ -135,6 +136,10 @@ export default class MissionWindow extends Component { this.updateInformation = this.updateInformation.bind(this); this.updateOptions = this.updateOptions.bind(this); this.postStartMissions = this.postStartMissions.bind(this); + this.postPauseMission = this.postPauseMission.bind(this); + this.postResumeMission = this.postResumeMission.bind(this); + this.postStartNextMission = this.postStartNextMission.bind(this); + this.stopMissions = this.stopMissions.bind(this); this.toggleMissionType = this.toggleMissionType.bind(this); this.tipFormatter = this.tipFormatter.bind(this); } @@ -147,7 +152,8 @@ export default class MissionWindow extends Component { ipcRenderer.on('updateActiveVehicleMapping', (_: Event, missionName: MissionInformation.MissionName, jobType: JobType, vehicleId: number): void => this.updateActiveVehicleMapping(missionName, jobType, vehicleId)); ipcRenderer.on('confirmCompleteMission', (): void => { this.setState({ status: 'next' }); }); - ipcRenderer.on('stopMissions', (): void => { this.setState({ status: 'ready' }); }); + ipcRenderer.on('stopMissions', (): void => { this.stopMissions(); }); + ipcRenderer.on('finishMissions', (): void => { this.setState({ status: 'finished' }); }); } private onSliderChange(value: [number, number]): void { @@ -224,6 +230,26 @@ export default class MissionWindow extends Component { }); ipc.postStartMissions(missionInformation, activeVehicleMapping, options, requireConfirmation); + this.setState({ status: 'running' }); + } + + private postPauseMission(): void { + ipc.postPauseMission(); + this.setState({ status: 'paused' }); + } + + private postResumeMission(): void { + ipc.postResumeMission(); + this.setState({ status: 'running' }); + } + + private postStartNextMission(): void { + ipc.postStartNextMission(); + this.setState({ status: 'running' }); + } + + private stopMissions(): void { + this.setState({ status: 'ready' }); } private toggleMissionType(): void { @@ -297,9 +323,10 @@ export default class MissionWindow extends Component {
{status === 'ready' && } {status !== 'ready' && } - {status === 'running' && } - {status === 'paused' && } - {status === 'next' && } + {status === 'running' && } + {status === 'paused' && } + {status === 'next' && } + {status === 'finished' && }
); diff --git a/src/types/messages.ts b/src/types/messages.ts deleted file mode 100644 index 67bddc62..00000000 --- a/src/types/messages.ts +++ /dev/null @@ -1,754 +0,0 @@ -/* - * Definitions and typeguards for all messages that are sent between GCS and vehicles. - * https://ground-control-station.readthedocs.io/en/latest/communications/messages.html - * - * Includes definitions for tasks too, as those are part of a message. - * https://ground-control-station.readthedocs.io/en/latest/communications/jobs.html - */ - -import { JobType, vehicleConfig } from '../static/index'; - -import { isVehicleStatus, VehicleStatus } from './types'; - -/** - * A float represented as a hexadecimal string. - * This is used to reduce the size of messages being sent between vehicles. - * Uses IEEE 754 standard to convert float to hex (and vice versa). - */ -export type HexFloat = string; - -/** - * Type guard for a hex float. - */ -export function isHexFloat(hex: string): boolean { - const modifiedHex = hex.startsWith('0x') ? hex.slice(2) : hex; - return modifiedHex.match(/^[0-9a-fA-F]+$/) !== null; -} - -/** - * Converts a float number to a hex float. - */ -/* eslint-disable no-bitwise */ -export function toHexFloat(float: number): HexFloat { - const getHex = (index: number): string => `00${index.toString(16)}`.slice(-2); - - const view = new DataView(new ArrayBuffer(4)); - view.setFloat32(0, float); - return `0x${[0, 0, 0, 0].map((_, index): string => getHex(view.getUint8(index))).join('')}`; -} - -/** - * Converts a hex string to a float number. - */ -export function toFloat(hex: HexFloat): number { - const int = parseInt(hex, 16); - if (int === 0) return 0; - - const sign = (int >>> 31) ? -1 : 1; - let exp = ((int >>> 23) & 0xff) - 127; - const mantissa = ((int & 0x7fffff) + 0x800000).toString(2); - let float = 0; - - for (let i = 0; i < mantissa.length; i += 1, exp -= 1) { - if (parseInt(mantissa[i], 10)) { - float += 2 ** exp; - } - } - - return float * sign; -} -/* eslint-enable no-bitwise */ - -// Definitions for all tasks. - -interface TaskBase { - taskType: string; -} - -interface TakeoffTaskParameters { - lat: HexFloat; - lng: HexFloat; - alt: HexFloat; - loiter: { - lat: HexFloat; - lng: HexFloat; - alt: HexFloat; - radius: HexFloat; - direction: HexFloat; - }; -} - -export interface TakeoffTask extends TaskBase, TakeoffTaskParameters { - taskType: 'takeoff'; -} - -function isTakeoffTask(task: Task): boolean { - return task.taskType === 'takeoff' - && isHexFloat(task.lat) - && isHexFloat(task.lng) - && isHexFloat(task.alt) - - && task.loiter - && isHexFloat(task.loiter.lat) - && isHexFloat(task.loiter.lng) - && isHexFloat(task.loiter.alt) - && isHexFloat(task.loiter.radius) - && isHexFloat(task.loiter.direction); -} - -interface LoiterTaskParameters { - lat: HexFloat; - lng: HexFloat; - alt: HexFloat; - radius: HexFloat; - direction: HexFloat; -} - -export interface LoiterTask extends TaskBase, LoiterTaskParameters { - taskType: 'loiter'; -} - -function isLoiterTask(task: Task): boolean { - return task.taskType === 'loiter' - && isHexFloat(task.lat) - && isHexFloat(task.lng) - && isHexFloat(task.alt) - && isHexFloat(task.radius) - && isHexFloat(task.direction); -} - -// eslint-disable-next-line @typescript-eslint/interface-name-prefix -interface ISRSearchTaskParameters { - alt: HexFloat; - waypoints: [ - { - lat: HexFloat; - lng: HexFloat; - }, - { - lat: HexFloat; - lng: HexFloat; - }, - { - lat: HexFloat; - lng: HexFloat; - } - ]; -} - -// eslint-disable-next-line @typescript-eslint/interface-name-prefix -export interface ISRSearchTask extends TaskBase, ISRSearchTaskParameters { - taskType: 'isrSearch'; -} - -function isISRSearchTask(task: Task): boolean { - return task.taskType === 'isrSearch' - && isHexFloat(task.alt) - - && task.waypoints && task.waypoints.length === 3 - && task.waypoints.every( - (waypoint): boolean => isHexFloat(waypoint.lat) && isHexFloat(waypoint.lng), - ); -} - -interface PayloadDropTaskParameters { - waypoints: [ - { - lat: HexFloat; - lng: HexFloat; - alt: HexFloat; - }, - { - lat: HexFloat; - lng: HexFloat; - alt: HexFloat; - } - ]; -} - -export interface PayloadDropTask extends TaskBase, PayloadDropTaskParameters { - taskType: 'payloadDrop'; -} - -function isPayloadDropTask(task: Task): boolean { - return task.taskType === 'payloadDrop' - && task.waypoints && task.waypoints.length === 2 - && task.waypoints.every( - (waypoint): boolean => isHexFloat(waypoint.lat) - && isHexFloat(waypoint.lng) - && isHexFloat(waypoint.alt), - ); -} - -interface LandTaskParameters { - waypoints: [ - { - lat: HexFloat; - lng: HexFloat; - alt: HexFloat; - }, - { - lat: HexFloat; - lng: HexFloat; - alt: HexFloat; - } - ]; -} - -export interface LandTask extends TaskBase, LandTaskParameters { - taskType: 'land'; -} - -function isLandTask(task: Task): boolean { - return task.taskType === 'land' - && task.waypoints && task.waypoints.length === 2 - && task.waypoints.every( - (waypoint): boolean => isHexFloat(waypoint.lat) - && isHexFloat(waypoint.lng) - && isHexFloat(waypoint.alt), - ); -} - -interface UGVRetrieveTargetTaskParameters extends TaskBase { - lat: HexFloat; - lng: HexFloat; -} - -export interface UGVRetrieveTargetTask extends TaskBase, UGVRetrieveTargetTaskParameters { - taskType: 'retrieveTarget'; -} - -function isUGVRetrieveTargetTask(task: Task): boolean { - if (task.taskType !== 'retrieveTarget') return false; - - const retrieveTargetTask = task as UGVRetrieveTargetTask; - - return isHexFloat(retrieveTargetTask.lat) && isHexFloat(retrieveTargetTask.lng); -} - -interface DeliverTargetTaskParameters { - taskType: 'deliverTarget'; - - lat: HexFloat; - lng: HexFloat; -} - -export interface DeliverTargetTask extends TaskBase, DeliverTargetTaskParameters { - taskType: 'deliverTarget'; -} - -function isDeliverTargetTask(task: Task): boolean { - return task.taskType === 'deliverTarget' - && isHexFloat(task.lat) - && isHexFloat(task.lng); -} - -// UUVRetrieveTargetTask does not have any parameters. - -export interface UUVRetrieveTargetTask extends TaskBase { - taskType: 'retrieveTarget'; -} - -function isUUVRetrieveTargetTask(task: Task): boolean { - if (task.taskType !== 'retrieveTarget') return false; - - // Cast task to UGV retrieve to ensure that this task is not UGV task, and is UUV. - const retrieveTargetTask = task as UGVRetrieveTargetTask; - - // Check if values for lat and lng are undefined, since UUV version should not have these. - return !retrieveTargetTask.lat && !retrieveTargetTask.lng; -} - -export interface QuickScanTaskParameters { - waypoints: [ - { - lat: HexFloat; - lng: HexFloat; - }, - { - lat: HexFloat; - lng: HexFloat; - }, - { - lat: HexFloat; - lng: HexFloat; - }, - { - lat: HexFloat; - lng: HexFloat; - } - ]; -} - -export interface QuickScanTask extends TaskBase, QuickScanTaskParameters { - taskType: 'quickScan'; -} - -function isQuickScanTask(task: Task): boolean { - return task.taskType === 'quickScan' - && task.waypoints && task.waypoints.length === 4 - && task.waypoints.every( - (waypoint): boolean => isHexFloat(waypoint.lat) && isHexFloat(waypoint.lng), - ); -} - -/* - * DetailedSearchTask parameters are provided by QuickScan and are generated - * autonomously, no need for user interaction. - */ - -export interface DetailedSearchTask extends TaskBase { - taskType: 'detailedSearch'; - - lat: HexFloat; - lng: HexFloat; -} - -function isDetailedSearchTask(task: Task): boolean { - return task.taskType === 'detailedSearch' - && isHexFloat(task.lat) - && isHexFloat(task.lng); -} - -export type Task = TakeoffTask | LoiterTask | ISRSearchTask | PayloadDropTask | LandTask -| UGVRetrieveTargetTask | DeliverTargetTask | UUVRetrieveTargetTask | QuickScanTask -| DetailedSearchTask; - -/** - * Checks if an object is a task. To be a task, the object must match one of the tasks above. - */ -// eslint-disable-next-line @typescript-eslint/no-explicit-any -function isTask(task: { [key: string]: any }): boolean { - if (!task.taskType) return false; - - const specificTask = task as Task; - - return isTakeoffTask(specificTask) - || isLoiterTask(specificTask) - || isISRSearchTask(specificTask) - || isPayloadDropTask(specificTask) - || isLandTask(specificTask) - || isUGVRetrieveTargetTask(specificTask) - || isDeliverTargetTask(specificTask) - || isUUVRetrieveTargetTask(specificTask) - || isQuickScanTask(specificTask) - || isDetailedSearchTask(specificTask); -} - -export const taskTypeGuard = { - isTakeoffTask, - isLoiterTask, - isISRSearchTask, - isPayloadDropTask, - isLandTask, - isUGVRetrieveTargetTask, - isDeliverTargetTask, - isUUVRetrieveTargetTask, - isQuickScanTask, - isDetailedSearchTask, - isTask, -}; - -// eslint-disable-next-line @typescript-eslint/interface-name-prefix -export interface ISRSearchMissionParameters { - takeoff: TakeoffTaskParameters; - isrSearch: ISRSearchTaskParameters; - land: LandTaskParameters; -} - -export function isISRSearchMissionParameters(parameters: MissionParameters): boolean { - return 'takeoff' in parameters && 'isrSearch' in parameters && 'land' in parameters; -} - -export interface VTOLSearchMissionParameters { - quickScan: QuickScanTaskParameters; -} - -export function isVTOLSearchMissionParameters(parameters: MissionParameters): boolean { - return 'quickScan' in parameters; -} - -export interface PayloadDropMissionParameters { - takeoff: TakeoffTaskParameters; - payloadDrop: PayloadDropTaskParameters; - land: LandTaskParameters; -} - -export function isPayloadDropMissionParameters(parameters: MissionParameters): boolean { - return 'takeoff' in parameters && 'payloadDrop' in parameters && 'land' in parameters; -} - -export interface UGVRetreiveMissionParameters { - retrieveTarget: UGVRetreiveMissionParameters; - deliverTarget: DeliverTargetTaskParameters; -} - -export function isUGVRetreiveMissionParameters(parameters: MissionParameters): boolean { - return 'retrieveTarget' in parameters && 'deliverTarget' in parameters; -} - -// UUVRetrieveMission does not need parameters, as its one task does not need any. - -export function isUUVRetrieveMissionParameters(parameters: MissionParameters): boolean { - return Object.keys(parameters).length === 0; -} - -export type MissionParameters = ISRSearchMissionParameters | VTOLSearchMissionParameters -| PayloadDropTaskParameters | UGVRetreiveMissionParameters | {}; - -export const missionParametersTypeGuard = { - isISRSearchMissionParameters, - isVTOLSearchMissionParameters, - isPayloadDropMissionParameters, - isUGVRetreiveMissionParameters, - isUUVRetrieveMissionParameters, -}; - -/** - * Options for the mission. All variables are optional so the UI should - * be able to detect which of the variables are required or not. - */ -export interface MissionOptions { - isrSearch?: { - /** - * Will not require takeoff parameters if this is true. - */ - noTakeoff: boolean; - - /** - * Will not require land parameters if this is true. - */ - noLand: boolean; - }; - - payloadDrop?: { - /** - * Will not require takeoff parameters if this is true. - */ - noTakeoff: boolean; - - /** - * Will not require land parameters if this is true. - */ - noLand: boolean; - }; -} - -/** - * Type of information that should be passed from mission window to the Orchestrator. - */ -export interface MissionInformation { - /** - * Parameters for the mission. - */ - parameters?: MissionParameters; - - /** - * Options for the mission. - */ - options: MissionOptions; -} - -export type MissionName = 'isrSearch' | 'vtolSearch' | 'payloadDrop' | 'ugvRetrieve' | 'uuvRetrieve'; - -export interface MissionDescription { - missionName: MissionName; - activeVehicleMapping: { [vehicleId: number]: JobType }; - information: MissionInformation; -} - -interface MessageBase { - /** - * Type of message. - */ - type: string; -} - -/* - * Definitions for all messages from GCS to vehicles. No need for type guards, TypeScript - * will handle all of it as we write up the message. - * - * MessageHex types are the same as their normal Message class, however, their - */ - -export interface StartMessage extends MessageBase { - type: 'start'; - - /** - * Name of job to perform. - */ - jobType: JobType; - - /** - * Any other information the vehicle should have before performing the job. - */ - options?: any; // eslint-disable-line @typescript-eslint/no-explicit-any -} - -function isStartMessage(message: Message): boolean { - return message.type === 'start' - && vehicleConfig.isValidJobType(message.jobType); -} - -export interface AddMissionMessage extends MessageBase { - type: 'addMission'; - - /** - * Information related to accomplishing specific job. - */ - missionInfo: Task; -} - -function isAddMissionMessage(message: Message): boolean { - return message.type === 'addMission' - && message.missionInfo && isTask(message.missionInfo); -} - -export interface PauseMessage extends MessageBase { - type: 'pause'; -} - -function isPauseMessage(message: Message): boolean { - return message.type === 'pause'; -} - -export interface ResumeMessage extends MessageBase { - type: 'resume'; -} - -function isResumeMessage(message: Message): boolean { - return message.type === 'resume'; -} - -export interface StopMessage extends MessageBase { - type: 'stop'; -} - -function isStopMessage(message: Message): boolean { - return message.type === 'stop'; -} - -export interface ConnectionAckMessage extends MessageBase { - type: 'connectionAck'; -} - -function isConnectionAckMessage(message: Message): boolean { - return message.type === 'connectionAck'; -} - -// Definitions for all messages from vehicles to GCS. - -export interface UpdateMessage extends MessageBase { - type: 'update'; - - /** - * Latitude of the vehicle. - */ - lat: HexFloat; - - /** - * Longitude of the vehicle. - */ - lng: HexFloat; - - /** - * Altitude of the vehicle. - */ - alt?: HexFloat; - - /** - * Current vehicle heading. Value is in degrees. - */ - heading?: HexFloat; - - /** - * Current battery of the vehicle, expressed as a decimal. Will vary from 0 to 1. - */ - battery?: HexFloat; - - /** - * Status of the vehicle. - */ - status: VehicleStatus; - - /** - * Message generated if vehicle is in an error state. - * - * It is best practice to provide this value to be able to see why the the vehicle's - * status is error. - */ - errorMessage?: string; -} - -function isUpdateMessage(message: Message): boolean { - const mandatoryCheck = message.type === 'update' - && isHexFloat(message.lat) - && isHexFloat(message.lng) - && isVehicleStatus(message.status); - - if (!mandatoryCheck) return false; - - const updateMessage = message as UpdateMessage; - - if (updateMessage.alt && !isHexFloat(updateMessage.alt)) return false; - if (updateMessage.heading && !isHexFloat(updateMessage.heading)) return false; - if (updateMessage.battery && !isHexFloat(updateMessage.battery)) return false; - - return true; -} - -export interface POIMessage extends MessageBase { - type: 'poi'; - - /** - * Latitude of the point of interest. - */ - lat: HexFloat; - - /** - * Longitude of the point of interest. - */ - lng: HexFloat; -} - -function isPOIMessage(message: Message): boolean { - return message.type === 'poi' - && isHexFloat(message.lat) - && isHexFloat(message.lng); -} - -export interface CompleteMessage extends MessageBase { - type: 'complete'; -} - -function isCompleteMessage(message: Message): boolean { - return message.type === 'complete'; -} - -export interface ConnectMessage extends MessageBase { - type: 'connect'; - - /** - * List of all the different jobs the vehicle is capable of doing. - */ - jobsAvailable: JobType[]; -} - -export function isConnectMessage(message: Message): boolean { - return message.type === 'connect' - - // Check if jobsAvailable is a string array of valid job types. - && message.jobsAvailable.every(vehicleConfig.isValidJobType); -} - -// Definitions for all other message types. - -export interface AcknowledgementMessage extends MessageBase { - type: 'ack'; - - /** - * ID of the message that is being acknowledged. - */ - ackid: number; -} - -export function isAcknowledgementMessage(message: Message): boolean { - return message.type === 'ack' - && !Number.isNaN(message.ackid); -} - -export interface BadMessage extends MessageBase { - type: 'badMessage'; - - /** - * Description of why message was bad. - * - * It is best practice to provide this value to be able to see why the message received - * is bad. - */ - error?: string; -} - -export function isBadMessage(message: Message): boolean { - return message.type === 'badMessage'; -} - -export type Message = StartMessage | AddMissionMessage | PauseMessage | ResumeMessage | StopMessage -| ConnectionAckMessage | UpdateMessage | POIMessage | CompleteMessage | ConnectMessage -| AcknowledgementMessage | BadMessage; - -/** - * Logic for this function is slightly different from isTask since the GCS does not need to know - * which task it is sending to the vehicle. It just needs to know that the task is a valid task, - * and contains a valid taskType and task information for a task. - * - * For this function, GCS does need to know what messages it receives. GCS needs to check if - * an object is a message (if the object has a type field then it is a message). If not, GCS - * can alert the sender that the object sent is not a message. - * - * After this function, the GCS will check which message the sender has sent with the functions - * above, and if the functions fail (which means that the fields that have been included with - * the message are wrong), then the GCS will tell the sender that the format for the message - * is wrong. - */ -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export function isMessage(message: { [key: string]: any }): boolean { - return message.type && typeof message.type === 'string' && [ - 'start', - 'addMission', - 'pause', - 'resume', - 'stop', - 'connectionAck', - 'update', - 'poi', - 'complete', - 'connect', - 'ack', - 'badMessage', - ].includes(message.type.toLowerCase()); -} - -export type JSONMessage = Message & { - id: number; - tid: number; - sid: number; - time: number; -}; - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export function isJSONMessage(message: { [key: string]: any }): boolean { - if (!isMessage(message)) return false; - - // Check if id is a number. - const idCheck = message.id !== '' && message.id !== true && !Number.isNaN(message.id); - - // Check if tid is a number and is valid. - const tidCheck = !Number.isNaN(message.tid) - && vehicleConfig.vehicleInfos[message.tid] !== undefined; - - // Check if sid is a number and is valid. - const sidCheck = !Number.isNaN(message.sid) - && vehicleConfig.vehicleInfos[message.sid] !== undefined; - - return idCheck && tidCheck && sidCheck; -} - -export const messageTypeGuard = { - isStartMessage, - isAddMissionMessage, - isPauseMessage, - isResumeMessage, - isStopMessage, - isConnectionAckMessage, - isUpdateMessage, - isPOIMessage, - isCompleteMessage, - isConnectMessage, - isAcknowledgementMessage, - isBadMessage, - isMessage, - isJSONMessage, -}; - -export default { taskTypeGuard, messageTypeGuard }; diff --git a/src/types/task.ts b/src/types/task.ts index 2fc7ba23..5bf41b4e 100644 --- a/src/types/task.ts +++ b/src/types/task.ts @@ -150,7 +150,7 @@ function isLandTask(task: Task): boolean { /** * Do not import this outside of types/missionInformation. */ -export interface UGVRetrieveTargetTaskParameters extends TaskBase { +export interface UGVRetrieveTargetTaskParameters { lat: number; lng: number; } diff --git a/src/types/types.ts b/src/types/types.ts deleted file mode 100644 index 9e00b2d9..00000000 --- a/src/types/types.ts +++ /dev/null @@ -1,144 +0,0 @@ -import { Moment } from 'moment'; - -/** - * Status of the vehicle, lets us know of where the vehicle is in its mission. - */ -export type VehicleStatus = 'ready' | 'error' | 'disconnected' | 'waiting' | 'running' | 'paused'; - -export function isVehicleStatus(status: string): boolean { - return status === 'ready' || status === 'error' || status === 'disconnected' || status === 'waiting' || status === 'running' || status === 'paused'; -} - -/** - * Props with theme as its child. Feel free to extend this prop. - */ -export interface ThemeProps { - /** - * "Light" for light theme, "dark" for dark theme. - */ - theme: 'light' | 'dark'; -} - -export function isThemeProps(props: { theme: string }): boolean { - return props.theme === 'light' || props.theme === 'dark'; -} - -/** - * Types for all messages. Consists of "success", "failure", "progress", or "". - */ -export type MessageType = '' | 'success' | 'failure' | 'progress'; - -export function isMessageType(type: string): boolean { - return type === '' || type === 'success' || type === 'failure' || type === 'progress'; -} - -/** - * Latitude, longitude and zoom. - */ -export interface LatLngZoom { - /** - * Latitude of center of map. - */ - lat: number; - - /** - * Longitude of center of map. - */ - lng: number; - - /** - * Zoom level of map. - */ - zoom?: number; -} - -/** - * Vehicle object for all classes to use. This is necessary because Vehicle class will - * be uncasted when being sent through ipcRenderer. - */ -export interface VehicleObject { - /** - * ID of the vehicle. - */ - vehicleId: number; - - /** - * Current status of the vehicle. - */ - status: VehicleStatus; - - /** - * Jobs of the vehicle. - */ - jobs: string[]; - - /** - * Current latitude of the vehicle. Starts at 0. - */ - lat: number; - - /** - * Current longitude of the vehicle. Starts at 0. - */ - lng: number; - - /** - * Current altitude of the vehicle. - */ - alt?: number; - /** - * - * Current battery of the vehicle, expressed as a decimal. Will vary from 0 to 1. - */ - battery?: number; - - /** - * Current vehicle heading. Value is in degrees. - */ - heading?: number; -} - -/** - * Data contents for information that is loaded from a configuration file. - */ -export interface FileLoadOptions { - /** - * Information related to the map. - */ - map: LatLngZoom; -} - -/** - * Object structure for information stored into a configuration file. - */ -export interface FileSaveOptions { - /** - * Filepath of the configuration file being saved. - */ - filePath: string; - - /** - * Data contents. Will be modified by classes through the "loadConfig" notification. - */ - data: FileLoadOptions; -} - -/** - * Structure for a message displayed in log container. - */ -export interface LogMessage { - /** - * The type of the message. Defines the color the message will be printed in. - */ - type?: MessageType; - - /** - * The content of the message. - */ - message: string; - - /** - * The time was received and logged. - */ - time?: Moment; -} From 340e80d94c91e9946222ed5df0a047ecff6087dc Mon Sep 17 00:00:00 2001 From: Paul Matthew Barrameda Date: Fri, 26 Apr 2019 22:26:50 -0700 Subject: [PATCH 126/152] Small changes to UI Removed all fields in UUV Rescue, as it requires none. Distinguished variables in ISR Search from Payload Drop. --- src/renderer/missionWindow/MissionWindow.tsx | 4 +- .../missionWindow/parameter/ISRSearch.tsx | 180 +++++++++--------- .../missionWindow/parameter/PayloadDrop.tsx | 168 ++++++++-------- .../missionWindow/parameter/UUVRescue.tsx | 160 +--------------- .../missionWindow/parameter/VTOLSearch.tsx | 2 +- 5 files changed, 174 insertions(+), 340 deletions(-) diff --git a/src/renderer/missionWindow/MissionWindow.tsx b/src/renderer/missionWindow/MissionWindow.tsx index 2235d0c2..322695e6 100644 --- a/src/renderer/missionWindow/MissionWindow.tsx +++ b/src/renderer/missionWindow/MissionWindow.tsx @@ -71,6 +71,8 @@ interface State { * - ready: no mission running * - running: mission running * - paused: mission paused + * - next: current mission has completed, waiting for next one to start + * - finished: all missions have finished * * A little different since when the first mission finishes, the status will go * to "next" instead of back to ready. From "next", it goes to "running", etc... @@ -308,7 +310,7 @@ export default class MissionWindow extends Component { />
-

Parameters

+

{`${title[missionName]} Mission Parameters`}

{status === 'ready' && } {status !== 'ready' &&

:)

}
diff --git a/src/renderer/missionWindow/parameter/ISRSearch.tsx b/src/renderer/missionWindow/parameter/ISRSearch.tsx index f42afc14..be18f5cc 100644 --- a/src/renderer/missionWindow/parameter/ISRSearch.tsx +++ b/src/renderer/missionWindow/parameter/ISRSearch.tsx @@ -5,31 +5,29 @@ import { missionName } from '../../../common/missions/ISRSearch'; import { Location } from '../../../static/index'; -import { VehicleObject } from '../../../types/vehicle'; - import ipc from '../../../util/ipc'; import { readyToStart } from '../../../util/parameter'; import CreateWaypointButton from '../extra/CreateWaypointButton'; -type ISRChecklistType = 'takeoffLat' | 'takeoffLng' | 'takeoffAlt' -| 'loiterLat' | 'loiterLng' | 'loiterAlt' | 'loiterRadius' | 'loiterDirection' +type ISRChecklistType = 'isrTakeoffLat' | 'isrTakeoffLng' | 'isrTakeoffAlt' +| 'isrLoiterLat' | 'isrLoiterLng' | 'isrLoiterAlt' | 'isrLoiterRadius' | 'isrLoiterDirection' | 'isrSearchAlt' | 'isrSearchLat1' | 'isrSearchLng1' | 'isrSearchLat2' | 'isrSearchLng2' | 'isrSearchLat3' | 'isrSearchLng3' -| 'landLat1' | 'landLng1' | 'landAlt1' -| 'landLat2' | 'landLng2' | 'landAlt2'; +| 'isrLandLat1' | 'isrLandLng1' | 'isrLandAlt1' +| 'isrLandLat2' | 'isrLandLng2' | 'isrLandAlt2'; const checklistCache: { [check in ISRChecklistType]: number | undefined } = { - takeoffLat: undefined, - takeoffLng: undefined, - takeoffAlt: undefined, - loiterLat: undefined, - loiterLng: undefined, - loiterAlt: undefined, - loiterRadius: undefined, - loiterDirection: undefined, + isrTakeoffLat: undefined, + isrTakeoffLng: undefined, + isrTakeoffAlt: undefined, + isrLoiterLat: undefined, + isrLoiterLng: undefined, + isrLoiterAlt: undefined, + isrLoiterRadius: undefined, + isrLoiterDirection: undefined, isrSearchAlt: undefined, isrSearchLat1: undefined, isrSearchLng1: undefined, @@ -37,41 +35,36 @@ const checklistCache: { [check in ISRChecklistType]: number | undefined } = { isrSearchLng2: undefined, isrSearchLat3: undefined, isrSearchLng3: undefined, - landLat1: undefined, - landLng1: undefined, - landAlt1: undefined, - landLat2: undefined, - landLng2: undefined, - landAlt2: undefined, + isrLandLat1: undefined, + isrLandLng1: undefined, + isrLandAlt1: undefined, + isrLandLat2: undefined, + isrLandLng2: undefined, + isrLandAlt2: undefined, }; -type ISRWaypointType = 'takeoff' | 'loiter' | 'isrSearch1' | 'isrSearch2' | 'isrSearch3' | 'land1' | 'land2'; +type ISRWaypointType = 'isrTakeoff' | 'isrLoiter' | 'isrSearch1' | 'isrSearch2' | 'isrSearch3' | 'isrLand1' | 'isrLand2'; type Locked = { [waypointType in ISRWaypointType]: boolean } & { - takeoff: boolean; - loiter: boolean; + isrTakeoff: boolean; + isrLoiter: boolean; isrSearch1: boolean; isrSearch2: boolean; isrSearch3: boolean; - land1: boolean; - land2: boolean; + isrLand1: boolean; + isrLand2: boolean; } const lockedCache: Locked = { - takeoff: true, - loiter: true, + isrTakeoff: true, + isrLoiter: true, isrSearch1: true, isrSearch2: true, isrSearch3: true, - land1: true, - land2: true, + isrLand1: true, + isrLand2: true, }; -// eslint-disable-next-line @typescript-eslint/interface-name-prefix -export interface ISRSearchProps { - vehicles: { [vehicleId: number]: VehicleObject }; -} - interface State { /** * Checklist of all required waypoints/coordinates. This is used to generate the @@ -91,8 +84,8 @@ interface State { locked: Locked; } -export class ISRSearch extends Component { - public constructor(props: ISRSearchProps) { +export class ISRSearch extends Component<{}, State> { + public constructor(props: {}) { super(props); this.state = { @@ -115,27 +108,26 @@ export class ISRSearch extends Component { private onChange(event: React.ChangeEvent): void { const { checklist } = this.state; - if (!(event.target.name in checklist)) return; const name = event.target.name as ISRChecklistType; const value = parseInt(event.target.value, 10) || 0; switch (name) { - case 'takeoffLat': - ipc.postUpdateWaypoints(true, { name: 'Takeoff', location: { lat: value, lng: checklist.takeoffLng as number } }); + case 'isrTakeoffLat': + ipc.postUpdateWaypoints(true, { name: 'Takeoff', location: { lat: value, lng: checklist.isrTakeoffLng as number } }); break; - case 'takeoffLng': - ipc.postUpdateWaypoints(true, { name: 'Takeoff', location: { lat: checklist.takeoffLat as number, lng: value } }); + case 'isrTakeoffLng': + ipc.postUpdateWaypoints(true, { name: 'Takeoff', location: { lat: checklist.isrTakeoffLat as number, lng: value } }); break; - case 'loiterLat': - ipc.postUpdateWaypoints(true, { name: 'Loiter', location: { lat: value, lng: checklist.loiterLng as number } }); + case 'isrLoiterLat': + ipc.postUpdateWaypoints(true, { name: 'Loiter', location: { lat: value, lng: checklist.isrLoiterLng as number } }); break; - case 'loiterLng': - ipc.postUpdateWaypoints(true, { name: 'Loiter', location: { lat: checklist.loiterLat as number, lng: value } }); + case 'isrLoiterLng': + ipc.postUpdateWaypoints(true, { name: 'Loiter', location: { lat: checklist.isrLoiterLat as number, lng: value } }); break; case 'isrSearchLat1': @@ -162,20 +154,20 @@ export class ISRSearch extends Component { ipc.postUpdateWaypoints(true, { name: 'ISR Search 3', location: { lat: checklist.isrSearchLat3 as number, lng: value } }); break; - case 'landLat1': - ipc.postUpdateWaypoints(true, { name: 'Land 1', location: { lat: value, lng: checklist.landLng1 as number } }); + case 'isrLandLat1': + ipc.postUpdateWaypoints(true, { name: 'Land 1', location: { lat: value, lng: checklist.isrLandLng1 as number } }); break; - case 'landLng1': - ipc.postUpdateWaypoints(true, { name: 'Land 1', location: { lat: checklist.landLat1 as number, lng: value } }); + case 'isrLandLng1': + ipc.postUpdateWaypoints(true, { name: 'Land 1', location: { lat: checklist.isrLandLat1 as number, lng: value } }); break; - case 'landLat2': - ipc.postUpdateWaypoints(true, { name: 'Land 2', location: { lat: value, lng: checklist.landLng2 as number } }); + case 'isrLandLat2': + ipc.postUpdateWaypoints(true, { name: 'Land 2', location: { lat: value, lng: checklist.isrLandLng2 as number } }); break; - case 'landLng2': - ipc.postUpdateWaypoints(true, { name: 'Land 2', location: { lat: checklist.landLat2 as number, lng: value } }); + case 'isrLandLng2': + ipc.postUpdateWaypoints(true, { name: 'Land 2', location: { lat: checklist.isrLandLat2 as number, lng: value } }); break; default: @@ -190,13 +182,13 @@ export class ISRSearch extends Component { waypoints.forEach((waypoint): void => { switch (waypoint.name) { case 'Takeoff': - checks.takeoffLat = waypoint.location.lat; - checks.takeoffLng = waypoint.location.lng; + checks.isrTakeoffLat = waypoint.location.lat; + checks.isrTakeoffLng = waypoint.location.lng; break; case 'Loiter': - checks.loiterLat = waypoint.location.lat; - checks.loiterLng = waypoint.location.lng; + checks.isrLoiterLat = waypoint.location.lat; + checks.isrLoiterLng = waypoint.location.lng; break; case 'ISR Search 1': @@ -215,13 +207,13 @@ export class ISRSearch extends Component { break; case 'Land 1': - checks.landLat1 = waypoint.location.lat; - checks.landLng1 = waypoint.location.lng; + checks.isrLandLat1 = waypoint.location.lat; + checks.isrLandLng1 = waypoint.location.lng; break; case 'Land 2': - checks.landLat2 = waypoint.location.lat; - checks.landLng2 = waypoint.location.lng; + checks.isrLandLat2 = waypoint.location.lat; + checks.isrLandLng2 = waypoint.location.lng; break; default: break; @@ -246,15 +238,15 @@ export class ISRSearch extends Component { missionName: 'isrSearch', parameters: { takeoff: { - lat: newChecklist.takeoffLat as number, - lng: newChecklist.takeoffLng as number, - alt: newChecklist.takeoffAlt as number, + lat: newChecklist.isrTakeoffLat as number, + lng: newChecklist.isrTakeoffLng as number, + alt: newChecklist.isrTakeoffAlt as number, loiter: { - lat: newChecklist.loiterLat as number, - lng: newChecklist.loiterLng as number, - alt: newChecklist.loiterAlt as number, - radius: newChecklist.loiterRadius as number, - direction: newChecklist.loiterDirection as number, + lat: newChecklist.isrLoiterLat as number, + lng: newChecklist.isrLoiterLng as number, + alt: newChecklist.isrLoiterAlt as number, + radius: newChecklist.isrLoiterRadius as number, + direction: newChecklist.isrLoiterDirection as number, }, }, isrSearch: { @@ -277,14 +269,14 @@ export class ISRSearch extends Component { land: { waypoints: [ { - lat: newChecklist.landLat1 as number, - lng: newChecklist.landLng1 as number, - alt: newChecklist.landAlt1 as number, + lat: newChecklist.isrLandLat1 as number, + lng: newChecklist.isrLandLng1 as number, + alt: newChecklist.isrLandAlt1 as number, }, { - lat: newChecklist.landLat2 as number, - lng: newChecklist.landLng2 as number, - alt: newChecklist.landAlt2 as number, + lat: newChecklist.isrLandLat2 as number, + lng: newChecklist.isrLandLng2 as number, + alt: newChecklist.isrLandAlt2 as number, }, ], }, @@ -320,18 +312,18 @@ export class ISRSearch extends Component { return (

Takeoff Coordinates

- - - - + + + +

Loiter Coordinates

- - - - - - + + + + + +

ISR Search Waypoints

@@ -349,16 +341,16 @@ export class ISRSearch extends Component {

Land Waypoints

- - - - + + + +

- - - - + + + +
); @@ -367,5 +359,5 @@ export class ISRSearch extends Component { export default { missionName, - layout: ISRSearch as React.ElementType, + layout: ISRSearch, }; diff --git a/src/renderer/missionWindow/parameter/PayloadDrop.tsx b/src/renderer/missionWindow/parameter/PayloadDrop.tsx index ca46f680..e7551c05 100644 --- a/src/renderer/missionWindow/parameter/PayloadDrop.tsx +++ b/src/renderer/missionWindow/parameter/PayloadDrop.tsx @@ -12,54 +12,54 @@ import { readyToStart } from '../../../util/parameter'; import CreateWaypointButton from '../extra/CreateWaypointButton'; -type PayloadDropChecklistType = 'takeoffLat' | 'takeoffLng' | 'takeoffAlt' -| 'loiterLat' | 'loiterLng' | 'loiterAlt' | 'loiterRadius' | 'loiterDirection' +type PayloadDropChecklistType = 'payloadDropTakoffLat' | 'payloadDropTakoffLng' | 'payloadDropTakoffAlt' +| 'payloadDropLoiterLat' | 'payloadDropLoiterLng' | 'payloadDropLoiterAlt' | 'payloadDropLoiterRadius' | 'payloadDropLoiterDirection' | 'payloadDropLat1' | 'payloadDropLng1' | 'payloadDropAlt1' | 'payloadDropLat2' | 'payloadDropLng2' | 'payloadDropAlt2' -| 'landLat1' | 'landLng1' | 'landAlt1' -| 'landLat2' | 'landLng2' | 'landAlt2'; +| 'payloadDropLandLat1' | 'payloadDropLandLng1' | 'payloadDropLandAlt1' +| 'payloadDropLandLat2' | 'payloadDropLandLng2' | 'payloadDropLandAlt2'; const checklistCache: { [check in PayloadDropChecklistType ]: number | undefined } = { - takeoffLat: undefined, - takeoffLng: undefined, - takeoffAlt: undefined, - loiterLat: undefined, - loiterLng: undefined, - loiterAlt: undefined, - loiterRadius: undefined, - loiterDirection: undefined, + payloadDropTakoffLat: undefined, + payloadDropTakoffLng: undefined, + payloadDropTakoffAlt: undefined, + payloadDropLoiterLat: undefined, + payloadDropLoiterLng: undefined, + payloadDropLoiterAlt: undefined, + payloadDropLoiterRadius: undefined, + payloadDropLoiterDirection: undefined, payloadDropAlt1: undefined, payloadDropLat1: undefined, payloadDropLng1: undefined, payloadDropAlt2: undefined, payloadDropLat2: undefined, payloadDropLng2: undefined, - landLat1: undefined, - landLng1: undefined, - landAlt1: undefined, - landLat2: undefined, - landLng2: undefined, - landAlt2: undefined, + payloadDropLandLat1: undefined, + payloadDropLandLng1: undefined, + payloadDropLandAlt1: undefined, + payloadDropLandLat2: undefined, + payloadDropLandLng2: undefined, + payloadDropLandAlt2: undefined, }; -type PayloadType = 'takeoff' | 'loiter' | 'payloadDrop1'| 'payloadDrop2' | 'land1' | 'land2'; +type PayloadType = 'payloadDropTakoff' | 'payloadDropLoiter' | 'payloadDrop1'| 'payloadDrop2' | 'payloadDropLand1' | 'payloadDropLand2'; type Locked = { [type in PayloadType]: boolean} & { - takeoff: boolean; - loiter: boolean; + payloadDropTakoff: boolean; + payloadDropLoiter: boolean; payloadDrop1: boolean; payloadDrop2: boolean; - land1: boolean; - land2: boolean; + payloadDropLand1: boolean; + payloadDropLand2: boolean; } const lockedCache: Locked = { - takeoff: true, - loiter: true, + payloadDropTakoff: true, + payloadDropLoiter: true, payloadDrop1: true, payloadDrop2: true, - land1: true, - land2: true, + payloadDropLand1: true, + payloadDropLand2: true, }; // eslint-disable-next-line @typescript-eslint/interface-name-prefix @@ -115,11 +115,11 @@ export class PayloadDrop extends Component { const name = event.target.name as PayloadDropChecklistType; const value = parseInt(event.target.value, 10) || 0; switch (name) { - case 'takeoffLat': - ipc.postUpdateWaypoints(true, { name: 'Takeoff', location: { lat: value, lng: checklist.takeoffLng as number } }); + case 'payloadDropTakoffLat': + ipc.postUpdateWaypoints(true, { name: 'Takeoff', location: { lat: value, lng: checklist.payloadDropTakoffLng as number } }); break; - case 'takeoffLng': - ipc.postUpdateWaypoints(true, { name: 'Takeoff', location: { lat: checklist.takeoffLat as number, lng: value } }); + case 'payloadDropTakoffLng': + ipc.postUpdateWaypoints(true, { name: 'Takeoff', location: { lat: checklist.payloadDropTakoffLat as number, lng: value } }); break; case 'payloadDropLat1': @@ -138,20 +138,20 @@ export class PayloadDrop extends Component { ipc.postUpdateWaypoints(true, { name: 'Payload Drop 2', location: { lat: checklist.payloadDropLat2 as number, lng: value } }); break; - case 'landLat1': - ipc.postUpdateWaypoints(true, { name: 'Land 1', location: { lat: value, lng: checklist.landLng1 as number } }); + case 'payloadDropLandLat1': + ipc.postUpdateWaypoints(true, { name: 'Land 1', location: { lat: value, lng: checklist.payloadDropLandLng1 as number } }); break; - case 'landLng1': - ipc.postUpdateWaypoints(true, { name: 'Land 1', location: { lat: checklist.landLat1 as number, lng: value } }); + case 'payloadDropLandLng1': + ipc.postUpdateWaypoints(true, { name: 'Land 1', location: { lat: checklist.payloadDropLandLat1 as number, lng: value } }); break; - case 'landLat2': - ipc.postUpdateWaypoints(true, { name: 'Land 2', location: { lat: value, lng: checklist.landLng2 as number } }); + case 'payloadDropLandLat2': + ipc.postUpdateWaypoints(true, { name: 'Land 2', location: { lat: value, lng: checklist.payloadDropLandLng2 as number } }); break; - case 'landLng2': - ipc.postUpdateWaypoints(true, { name: 'Land 2', location: { lat: checklist.landLat2 as number, lng: value } }); + case 'payloadDropLandLng2': + ipc.postUpdateWaypoints(true, { name: 'Land 2', location: { lat: checklist.payloadDropLandLat2 as number, lng: value } }); break; default: @@ -166,13 +166,13 @@ export class PayloadDrop extends Component { waypoints.forEach((waypoint): void => { switch (waypoint.name) { case 'Takeoff': - checks.takeoffLat = waypoint.location.lat; - checks.takeoffLng = waypoint.location.lng; + checks.payloadDropTakoffLat = waypoint.location.lat; + checks.payloadDropTakoffLng = waypoint.location.lng; break; case 'Loiter': - checks.loiterLat = waypoint.location.lat; - checks.loiterLng = waypoint.location.lng; + checks.payloadDropLoiterLat = waypoint.location.lat; + checks.payloadDropLoiterLng = waypoint.location.lng; break; case 'Payload Drop 1': @@ -186,13 +186,13 @@ export class PayloadDrop extends Component { break; case 'Land 1': - checks.landLat1 = waypoint.location.lat; - checks.landLng1 = waypoint.location.lng; + checks.payloadDropLandLat1 = waypoint.location.lat; + checks.payloadDropLandLng1 = waypoint.location.lng; break; case 'Land 2': - checks.landLat2 = waypoint.location.lat; - checks.landLng2 = waypoint.location.lng; + checks.payloadDropLandLat2 = waypoint.location.lat; + checks.payloadDropLandLng2 = waypoint.location.lng; break; default: break; @@ -217,15 +217,15 @@ export class PayloadDrop extends Component { missionName: 'payloadDrop', parameters: { takeoff: { - lat: newChecklist.takeoffLat as number, - lng: newChecklist.takeoffLng as number, - alt: newChecklist.takeoffAlt as number, + lat: newChecklist.payloadDropTakoffLat as number, + lng: newChecklist.payloadDropTakoffLng as number, + alt: newChecklist.payloadDropTakoffAlt as number, loiter: { - lat: newChecklist.loiterLat as number, - lng: newChecklist.loiterLng as number, - alt: newChecklist.loiterAlt as number, - radius: newChecklist.loiterRadius as number, - direction: newChecklist.loiterDirection as number, + lat: newChecklist.payloadDropLoiterLat as number, + lng: newChecklist.payloadDropLoiterLng as number, + alt: newChecklist.payloadDropLoiterAlt as number, + radius: newChecklist.payloadDropLoiterRadius as number, + direction: newChecklist.payloadDropLoiterDirection as number, }, }, payloadDrop: { @@ -245,14 +245,14 @@ export class PayloadDrop extends Component { land: { waypoints: [ { - lat: newChecklist.landLat1 as number, - lng: newChecklist.landLng1 as number, - alt: newChecklist.landAlt1 as number, + lat: newChecklist.payloadDropLandLat1 as number, + lng: newChecklist.payloadDropLandLng1 as number, + alt: newChecklist.payloadDropLandAlt1 as number, }, { - lat: newChecklist.landLat2 as number, - lng: newChecklist.landLng2 as number, - alt: newChecklist.landAlt2 as number, + lat: newChecklist.payloadDropLandLat2 as number, + lng: newChecklist.payloadDropLandLng2 as number, + alt: newChecklist.payloadDropLandAlt2 as number, }, ], }, @@ -286,18 +286,18 @@ export class PayloadDrop extends Component { return (

Takeoff Coordinates

- - - - + + + +

Loiter Coordinates

- - - - - - + + + + + +

Payload Drop Coordinates

@@ -311,31 +311,23 @@ export class PayloadDrop extends Component {

Land Waypoints

- - - - + + + +

- - - - + + + +
); } } -/* - * export function PayloadDrop(): ReactNode { - * return ( - *
Payload Drop
- * ); - * } - */ - export default { missionName, - layout: PayloadDrop as React.ElementType, + layout: PayloadDrop, }; diff --git a/src/renderer/missionWindow/parameter/UUVRescue.tsx b/src/renderer/missionWindow/parameter/UUVRescue.tsx index cf913464..bc782a84 100644 --- a/src/renderer/missionWindow/parameter/UUVRescue.tsx +++ b/src/renderer/missionWindow/parameter/UUVRescue.tsx @@ -1,165 +1,13 @@ -import { Event, ipcRenderer } from 'electron'; -import React, { Component, ReactNode } from 'react'; +import React, { ReactNode } from 'react'; import { missionName } from '../../../common/missions/UUVRescue'; -import { Location } from '../../../static/index'; - -import { VehicleObject } from '../../../types/vehicle'; - import ipc from '../../../util/ipc'; -import { readyToStart } from '../../../util/parameter'; - -import CreateWaypointButton from '../extra/CreateWaypointButton'; - -type UUVChecklistType = 'uuvRetrieveTargetLat' | 'uuvRetrieveTargetLng'; - -const checklistCache: { [check in UUVChecklistType]: number | undefined } = { - uuvRetrieveTargetLat: undefined, - uuvRetrieveTargetLng: undefined, -}; - -type UUVWaypointType = 'retrieveTarget'; - -type Locked = { [waypointType in UUVWaypointType]: boolean } & { - retrieveTarget: boolean; -} - -const lockedCache: Locked = { - retrieveTarget: true, -}; - -export interface UUVRescueProps { - vehicles: { [vehicleId: number]: VehicleObject }; -} - -interface State { - checklist: { [check in UUVChecklistType]: number | undefined }; - - ready: boolean; - - locked: Locked; -} - -export class UUVRescue extends Component { - public constructor(props: UUVRescueProps) { - super(props); - - this.state = { - checklist: checklistCache, - ready: false, - locked: lockedCache, - }; - - this.onChange = this.onChange.bind(this); - this.updateWaypoints = this.updateWaypoints.bind(this); - this.updateChecklist = this.updateChecklist.bind(this); - this.unlockParameterInputs = this.unlockParameterInputs.bind(this); - this.readyToStart = this.readyToStart.bind(this); - } - - public componentDidMount(): void { - ipcRenderer.on('updateWaypoints', (__: Event, _: boolean, ...waypoints: { name: string; location: Location }[]): void => this.updateWaypoints(...waypoints)); - ipcRenderer.on('unlockParameterInputs', (_: Event, waypointType: string): void => this.unlockParameterInputs(waypointType)); - } - - - private onChange(event: React.ChangeEvent): void { - const { checklist } = this.state; - - if (!(event.target.name in checklist)) return; - - const name = event.target.name as UUVChecklistType; - const value = parseInt(event.target.value, 10) || 0; - - switch (name) { - case 'uuvRetrieveTargetLat': - ipc.postUpdateWaypoints(true, { name: 'Retrieve Target', location: { lat: value, lng: checklist.uuvRetrieveTargetLng as number } }); - break; - - case 'uuvRetrieveTargetLng': - ipc.postUpdateWaypoints(true, { name: 'Retrieve Target', location: { lat: checklist.uuvRetrieveTargetLat as number, lng: value } }); - break; - - default: - this.updateChecklist({ [name]: value }); - break; - } - } - - private updateWaypoints(...waypoints: { name: string; location: Location }[]): void { - const checks: { [checkName in UUVChecklistType]?: number } = {}; - - waypoints.forEach((waypoint): void => { - switch (waypoint.name) { - case 'Retrieve Target': - checks.uuvRetrieveTargetLat = waypoint.location.lat; - checks.uuvRetrieveTargetLng = waypoint.location.lng; - break; - - default: break; - } - }); - - this.updateChecklist(checks); - } - - private updateChecklist(checks: { [checklistType in UUVChecklistType]?: number }): void { - const { checklist: newChecklist, ready } = this.state; - - Object.keys(checks).forEach((checklistTypeString): void => { - const checklistType = checklistTypeString as UUVChecklistType; - const value = checks[checklistType]; - - newChecklist[checklistType] = value; - }); - - if (ready || readyToStart(this)) { - ipc.postUpdateInformation({ - missionName: 'uuvRescue', - parameters: { - retrieveTarget: { - lat: newChecklist.uuvRetrieveTargetLat as number, - lng: newChecklist.uuvRetrieveTargetLng as number, - }, - }, - }); - } - - this.setState({ checklist: newChecklist }); - } - - private unlockParameterInputs(waypointType: string): void { - const { locked: newLocked } = this.state; - - if (waypointType in newLocked) { - newLocked[waypointType as UUVWaypointType] = false; - } - - this.setState({ locked: newLocked }); - } - - private readyToStart(): boolean { - const { checklist } = this.state; - - const ready = Object.values(checklist).every((value): boolean => value !== undefined); - - if (ready) this.setState({ ready }); - return ready; - } - public render(): ReactNode { - const { checklist, locked } = this.state; +export function UUVRescue(): ReactNode { + ipc.postUpdateInformation({ missionName: 'uuvRescue', parameters: { retrieveTarget: {} } }); - return ( -
-

UUV Retrieve Target

- - - -
- ); - } + return
; } export default { diff --git a/src/renderer/missionWindow/parameter/VTOLSearch.tsx b/src/renderer/missionWindow/parameter/VTOLSearch.tsx index 987e2fc3..d6fe9155 100644 --- a/src/renderer/missionWindow/parameter/VTOLSearch.tsx +++ b/src/renderer/missionWindow/parameter/VTOLSearch.tsx @@ -244,5 +244,5 @@ export class VTOLSearch extends Component { export default { missionName, - layout: VTOLSearch as React.ElementType, + layout: VTOLSearch, }; From 2caef4346f14ff070399d7708691bcd1bb79ea47 Mon Sep 17 00:00:00 2001 From: Marlon Aquino Date: Fri, 26 Apr 2019 22:45:48 -0700 Subject: [PATCH 127/152] Updated mission parameters for UGVRescue --- .../missionWindow/parameter/UGVRescue.tsx | 190 +++++++++++++++++- 1 file changed, 184 insertions(+), 6 deletions(-) diff --git a/src/renderer/missionWindow/parameter/UGVRescue.tsx b/src/renderer/missionWindow/parameter/UGVRescue.tsx index e9e4911b..7fbedd11 100644 --- a/src/renderer/missionWindow/parameter/UGVRescue.tsx +++ b/src/renderer/missionWindow/parameter/UGVRescue.tsx @@ -1,13 +1,191 @@ -import React, { ReactNode } from 'react'; +import { Event, ipcRenderer } from 'electron'; +import React, { Component, ReactNode } from 'react'; import { missionName } from '../../../common/missions/UGVRescue'; -export const name = 'UGV Rescue'; +import { Location } from '../../../static/index'; -export function UGVRescue(): ReactNode { - return ( -
UGV Rescue
- ); +import { VehicleObject } from '../../../types/vehicle'; + +import ipc from '../../../util/ipc'; +import { readyToStart } from '../../../util/parameter'; + +import CreateWaypointButton from '../extra/CreateWaypointButton'; + +type UGVChecklistType = 'ugvRetrieveTargetLat' | 'ugvRetrieveTargetLng' | 'ugvDeliverTargetLat' | 'ugvDeliverTargetLng'; + +const checklistCache: { [check in UGVChecklistType]: number | undefined } = { + ugvRetrieveTargetLat: undefined, + ugvRetrieveTargetLng: undefined, + ugvDeliverTargetLat: undefined, + ugvDeliverTargetLng: undefined, +}; + +type UGVWaypointType = 'retrieveTarget' | 'deliverTarget'; + +type Locked = { [waypointType in UGVWaypointType]: boolean } & { + retrieveTarget: boolean; + deliverTarget: boolean; +} + +const lockedCache: Locked = { + retrieveTarget: true, + deliverTarget: true, +}; + +export interface UGVRescueProps { + vehicles: { [vehicleId: number]: VehicleObject }; +} + +interface State { + checklist: { [check in UGVChecklistType]: number | undefined }; + + ready: boolean; + + locked: Locked; +} + +export class UGVRescue extends Component { + public constructor(props: UGVRescueProps) { + super(props); + + this.state = { + checklist: checklistCache, + ready: false, + locked: lockedCache, + }; + + this.onChange = this.onChange.bind(this); + this.updateWaypoints = this.updateWaypoints.bind(this); + this.updateChecklist = this.updateChecklist.bind(this); + this.unlockParameterInputs = this.unlockParameterInputs.bind(this); + this.readyToStart = this.readyToStart.bind(this); + } + + public componentDidMount(): void { + ipcRenderer.on('updateWaypoints', (__: Event, _: boolean, ...waypoints: { name: string; location: Location }[]): void => this.updateWaypoints(...waypoints)); + ipcRenderer.on('unlockParameterInputs', (_: Event, waypointType: string): void => this.unlockParameterInputs(waypointType)); + } + + + private onChange(event: React.ChangeEvent): void { + const { checklist } = this.state; + + if (!(event.target.name in checklist)) return; + + const name = event.target.name as UGVChecklistType; + const value = parseInt(event.target.value, 10) || 0; + + switch (name) { + case 'ugvRetrieveTargetLat': + ipc.postUpdateWaypoints(true, { name: 'Retrieve Target', location: { lat: value, lng: checklist.ugvRetrieveTargetLng as number } }); + break; + + case 'ugvRetrieveTargetLng': + ipc.postUpdateWaypoints(true, { name: 'Retrieve Target', location: { lat: checklist.ugvRetrieveTargetLat as number, lng: value } }); + break; + + case 'ugvDeliverTargetLat': + ipc.postUpdateWaypoints(true, { name: 'Deliver Target', location: { lat: value, lng: checklist.ugvDeliverTargetLng as number } }); + break; + + case 'ugvDeliverTargetLng': + ipc.postUpdateWaypoints(true, { name: 'Deliver Target', location: { lat: checklist.ugvDeliverTargetLat as number, lng: value } }); + break; + + default: + this.updateChecklist({ [name]: value }); + break; + } + } + + private updateWaypoints(...waypoints: { name: string; location: Location }[]): void { + const checks: { [checkName in UGVChecklistType]?: number } = {}; + + waypoints.forEach((waypoint): void => { + switch (waypoint.name) { + case 'Retrieve Target': + checks.ugvRetrieveTargetLat = waypoint.location.lat; + checks.ugvRetrieveTargetLng = waypoint.location.lng; + break; + + case 'Deliver Target': + checks.ugvDeliverTargetLat = waypoint.location.lat; + checks.ugvDeliverTargetLng = waypoint.location.lng; + break; + + default: break; + } + }); + + this.updateChecklist(checks); + } + + private updateChecklist(checks: { [checklistType in UGVChecklistType]?: number }): void { + const { checklist: newChecklist, ready } = this.state; + + Object.keys(checks).forEach((checklistTypeString): void => { + const checklistType = checklistTypeString as UGVChecklistType; + const value = checks[checklistType]; + + newChecklist[checklistType] = value; + }); + + if (ready || readyToStart(this)) { + ipc.postUpdateInformation({ + missionName: 'ugvRescue', + parameters: { + retrieveTarget: { + lat: newChecklist.ugvRetrieveTargetLat as number, + lng: newChecklist.ugvRetrieveTargetLng as number, + }, + deliverTarget: { + lat: newChecklist.ugvDeliverTargetLat as number, + lng: newChecklist.ugvDeliverTargetLng as number, + }, + }, + }); + } + + this.setState({ checklist: newChecklist }); + } + + private unlockParameterInputs(waypointType: string): void { + const { locked: newLocked } = this.state; + + if (waypointType in newLocked) { + newLocked[waypointType as UGVWaypointType] = false; + } + + this.setState({ locked: newLocked }); + } + + private readyToStart(): boolean { + const { checklist } = this.state; + + const ready = Object.values(checklist).every((value): boolean => value !== undefined); + + if (ready) this.setState({ ready }); + return ready; + } + + public render(): ReactNode { + const { checklist, locked } = this.state; + + return ( +
+

UGV Retrieve Target

+ + + + +

UGV Deliver Target

+ + + +
+ ); + } } export default { From 6d8bb5927f1b4aec37428d4b6097b27ce0284989 Mon Sep 17 00:00:00 2001 From: tnleang Date: Fri, 26 Apr 2019 23:06:44 -0700 Subject: [PATCH 128/152] Update on VTOLSearch Bounding box on the map coresponds to VTOLSearch.tsx --- src/renderer/mainWindow/map/MapContainer.tsx | 6 + ...ingBox.tsx => CreateBoundingBoxButton.tsx} | 7 +- .../missionWindow/parameter/VTOLSearch.tsx | 159 +++++++++--------- 3 files changed, 86 insertions(+), 86 deletions(-) rename src/renderer/missionWindow/extra/{CreateBoundingBox.tsx => CreateBoundingBoxButton.tsx} (82%) diff --git a/src/renderer/mainWindow/map/MapContainer.tsx b/src/renderer/mainWindow/map/MapContainer.tsx index 75be75b1..abfb520e 100644 --- a/src/renderer/mainWindow/map/MapContainer.tsx +++ b/src/renderer/mainWindow/map/MapContainer.tsx @@ -26,6 +26,7 @@ import BoundingBox from './shape/BoundingBox'; import VehicleMarker from './marker/VehicleMarker'; import WaypointMarker from './marker/WaypointMarker'; + import './map.css'; const mapOptions = { @@ -284,6 +285,11 @@ export default class MapContainer extends Component { locked: false, }; } + + ipc.postUpdateBoundingBoxes(false, { + name: boundingBox.name, + bounds: newBoundingBoxes[boundingBox.name].bounds, + }); }); this.setState({ boundingBoxes: newBoundingBoxes }); diff --git a/src/renderer/missionWindow/extra/CreateBoundingBox.tsx b/src/renderer/missionWindow/extra/CreateBoundingBoxButton.tsx similarity index 82% rename from src/renderer/missionWindow/extra/CreateBoundingBox.tsx rename to src/renderer/missionWindow/extra/CreateBoundingBoxButton.tsx index 86bafe4a..e5688ded 100644 --- a/src/renderer/missionWindow/extra/CreateBoundingBox.tsx +++ b/src/renderer/missionWindow/extra/CreateBoundingBoxButton.tsx @@ -2,7 +2,7 @@ import React, { PureComponent, ReactNode } from 'react'; import ipc from '../../../util/ipc'; -export interface CreateBoundingBoxProps { +export interface CreateBoundingBoxButtonProps { /** * Identifier that distinguishes */ @@ -14,8 +14,8 @@ export interface CreateBoundingBoxProps { value: string; } -export default class CreateBoundingBoxButton extends PureComponent { - public constructor(props: CreateBoundingBoxProps) { +export default class CreateBoundingBoxButton extends PureComponent { + public constructor(props: CreateBoundingBoxButtonProps) { super(props); this.onClick = this.onClick.bind(this); @@ -23,7 +23,6 @@ export default class CreateBoundingBoxButton extends PureComponent { }; this.onChange = this.onChange.bind(this); - this.updateWaypoints = this.updateWaypoints.bind(this); + this.updateBoundingBoxes = this.updateBoundingBoxes.bind(this); this.updateChecklist = this.updateChecklist.bind(this); this.readyToStart = this.readyToStart.bind(this); this.unlockParameterInputs = this.unlockParameterInputs.bind(this); } public componentDidMount(): void { - ipcRenderer.on('updateWaypoints', (__: Event, _: boolean, ...waypoints: { name: string; location: Location }[]): void => this.updateWaypoints(...waypoints)); + ipcRenderer.on('updateBoundingBoxes', (__: Event, _: boolean, ...boxPoints: { name: string; bounds: BoundingBoxBounds }[]): void => this.updateBoundingBoxes(...boxPoints)); ipcRenderer.on('unlockParameterInputs', (_: Event, waypointType: string): void => this.unlockParameterInputs(waypointType)); } @@ -90,36 +85,52 @@ export class VTOLSearch extends Component { const name = event.target.name as VTOLSearchChecklistType; const value = parseInt(event.target.value, 10) || 0; switch (name) { - case 'quickScanTopLeftLat': - ipc.postUpdateWaypoints(true, { name: 'Quick Scan 1', location: { lat: value, lng: checklist.quickScanTopLeftLng as number } }); - break; - - case 'quickScanTopLeftLng': - ipc.postUpdateWaypoints(true, { name: 'Quick Scan 1', location: { lat: checklist.quickScanTopLeftLat as number, lng: value } }); - break; - - case 'quickScanTopRightLat': - ipc.postUpdateWaypoints(true, { name: 'Quick Scan 2', location: { lat: value, lng: checklist.quickScanTopRightLng as number } }); - break; - - case 'quickScanTopRightLng': - ipc.postUpdateWaypoints(true, { name: 'Quick Scan 2', location: { lat: checklist.quickScanTopRightLat as number, lng: value } }); - break; - - case 'quickScanBottomLeftLat': - ipc.postUpdateWaypoints(true, { name: 'Quick Scan 3', location: { lat: value, lng: checklist.quickScanBottomLeftLng as number } }); + case 'quickScanTop': + ipc.postUpdateBoundingBoxes(true, { + name: 'Bounding Box', + bounds: { + top: value, + bottom: checklist.quickScanBottom as number, + left: checklist.quickScanLeft as number, + right: checklist.quickScanRight as number, + }, + }); break; - case 'quickScanBottomLeftLng': - ipc.postUpdateWaypoints(true, { name: 'Quick Scan 3', location: { lat: checklist.quickScanBottomLeftLat as number, lng: value } }); + case 'quickScanLeft': + ipc.postUpdateBoundingBoxes(true, { + name: 'Bounding Box', + bounds: { + top: checklist.quickScanTop as number, + bottom: checklist.quickScanBottom as number, + left: value, + right: checklist.quickScanRight as number, + }, + }); break; - case 'quickScanBottomRightLat': - ipc.postUpdateWaypoints(true, { name: 'Quick Scan 4', location: { lat: value, lng: checklist.quickScanBottomRightLng as number } }); + case 'quickScanRight': + ipc.postUpdateBoundingBoxes(true, { + name: 'Bounding Box', + bounds: { + top: checklist.quickScanTop as number, + bottom: checklist.quickScanBottom as number, + left: checklist.quickScanLeft as number, + right: value, + }, + }); break; - case 'quickScanBottomRightLng': - ipc.postUpdateWaypoints(true, { name: 'Quick Scan 4', location: { lat: checklist.quickScanBottomRightLat as number, lng: value } }); + case 'quickScanBottom': + ipc.postUpdateBoundingBoxes(true, { + name: 'Bounding Box', + bounds: { + top: checklist.quickScanTop as number, + bottom: value, + left: checklist.quickScanLeft as number, + right: checklist.quickScanRight as number, + }, + }); break; default: @@ -128,29 +139,18 @@ export class VTOLSearch extends Component { } } - private updateWaypoints(...boundingBoxes: { name: string; location: Location }[]): void { + private updateBoundingBoxes( + ...boundingBoxes: { name: string; bounds: BoundingBoxBounds }[] + ): void { const checks: { [checkName in VTOLSearchChecklistType]?: number} = {}; - boundingBoxes.forEach((waypoint): void => { - switch (waypoint.name) { - case 'Quick Scan Top Left': - checks.quickScanTopLeftLat = waypoint.location.lat; - checks.quickScanTopLeftLng = waypoint.location.lng; - break; - - case 'Quick Scan Top Right': - checks.quickScanTopRightLat = waypoint.location.lat; - checks.quickScanTopRightLng = waypoint.location.lng; - break; - - case 'Quick Scan Bottom Left': - checks.quickScanBottomLeftLat = waypoint.location.lat; - checks.quickScanBottomLeftLng = waypoint.location.lng; - break; - - case 'Quick Scan Bottom Right': - checks.quickScanBottomRightLat = waypoint.location.lat; - checks.quickScanBottomRightLng = waypoint.location.lng; + boundingBoxes.forEach((boxpoint): void => { + switch (boxpoint.name) { + case 'Bounding Box': + checks.quickScanTop = boxpoint.bounds.top; + checks.quickScanRight = boxpoint.bounds.right; + checks.quickScanLeft = boxpoint.bounds.left; + checks.quickScanBottom = boxpoint.bounds.bottom; break; default: break; @@ -176,20 +176,20 @@ export class VTOLSearch extends Component { quickScan: { waypoints: [ { - lat: newChecklist.quickScanTopLeftLat as number, - lng: newChecklist.quickScanTopLeftLng as number, + lat: newChecklist.quickScanTop as number, + lng: newChecklist.quickScanLeft as number, }, { - lat: newChecklist.quickScanTopRightLat as number, - lng: newChecklist.quickScanTopRightLng as number, + lat: newChecklist.quickScanTop as number, + lng: newChecklist.quickScanRight as number, }, { - lat: newChecklist.quickScanBottomLeftLat as number, - lng: newChecklist.quickScanBottomLeftLng as number, + lat: newChecklist.quickScanBottom as number, + lng: newChecklist.quickScanLeft as number, }, { - lat: newChecklist.quickScanBottomRightLat as number, - lng: newChecklist.quickScanBottomRightLng as number, + lat: newChecklist.quickScanBottom as number, + lng: newChecklist.quickScanRight as number, }, ], }, @@ -224,19 +224,14 @@ export class VTOLSearch extends Component { return (

Quick Scan

- - -
- - +
- - +
- - +
- + +
); } @@ -244,5 +239,5 @@ export class VTOLSearch extends Component { export default { missionName, - layout: VTOLSearch, + layout: VTOLSearch as React.ElementType, }; From ab04e40ba42425c40223ea462327eafef46f4378 Mon Sep 17 00:00:00 2001 From: Paul Matthew Barrameda Date: Sat, 27 Apr 2019 00:39:01 -0700 Subject: [PATCH 129/152] Final logic changes Ensured class functions are being called when called on event callbacks. Modified check on Mission before assigning jobs to vehicles. Was able to test that it works right until Task assignment, not sure after that. --- package.json | 2 +- src/common/MessageHandler.ts | 2 +- src/common/Orchestrator.ts | 8 +-- src/common/struct/DictionaryList.ts | 13 +++++ src/common/struct/Mission.ts | 30 ++++-------- src/common/struct/Vehicle.ts | 12 +++-- src/renderer/mainWindow/map/MapContainer.tsx | 8 ++- src/renderer/missionWindow/MissionWindow.tsx | 49 +++++++++++++++++-- .../missionWindow/parameter/UGVRescue.tsx | 2 +- .../missionWindow/parameter/VTOLSearch.tsx | 5 +- src/static/config.json | 6 +-- src/types/vehicle.ts | 4 +- src/util/ipc.ts | 8 +-- 13 files changed, 99 insertions(+), 50 deletions(-) diff --git a/package.json b/package.json index bd48e10b..fc00d723 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "GCS", - "version": "0.7.0", + "version": "0.7.1", "license": "MIT", "author": "Northrop Grumman Collaboration Project", "description": "Ground Control Station for autonomous vehicle platforms in NGCP", diff --git a/src/common/MessageHandler.ts b/src/common/MessageHandler.ts index f5a1cab5..b0d4635c 100644 --- a/src/common/MessageHandler.ts +++ b/src/common/MessageHandler.ts @@ -43,7 +43,7 @@ class MessageHandler { ipcRenderer.on('receiveMessage', (_: Event, message: any): void => this.receiveMessage(message)); // eslint-disable-line @typescript-eslint/no-explicit-any ipcRenderer.on('stopSendingMessage', (_: Event, ackMessage: Message.JSONMessage): void => this.stopSendingMessage(ackMessage)); - ipcRenderer.on('stopSendingMessages', this.stopSendingMessages); + ipcRenderer.on('stopSendingMessages', (): void => this.stopSendingMessages()); } /** diff --git a/src/common/Orchestrator.ts b/src/common/Orchestrator.ts index a767cc32..295eaba0 100644 --- a/src/common/Orchestrator.ts +++ b/src/common/Orchestrator.ts @@ -100,11 +100,11 @@ class Orchestrator { options: MissionInformation.MissionOptions, requireConfirmation: boolean, ): void => this.startMissions(missions, activeVehicleMapping, options, requireConfirmation)); - ipcRenderer.on('pauseMission', (): void => { this.pauseMission(); }); - ipcRenderer.on('resumeMission', (): void => { this.resumeMission(); }); - ipcRenderer.on('startNextMission', (): void => { this.startNextMission(); }); + ipcRenderer.on('pauseMission', (): void => this.pauseMission()); + ipcRenderer.on('resumeMission', (): void => this.resumeMission()); + ipcRenderer.on('startNextMission', (): void => this.startNextMission()); ipcRenderer.on('completeMission', (_: Event, missionName: string, completionParameters: Task.TaskParameters[]): void => this.completeMission(missionName, completionParameters)); - ipcRenderer.on('stopMissions', (): void => { this.stopMissions(); }); + ipcRenderer.on('stopMissions', (): void => this.stopMissions()); } /** diff --git a/src/common/struct/DictionaryList.ts b/src/common/struct/DictionaryList.ts index 1cfb1780..4713002b 100644 --- a/src/common/struct/DictionaryList.ts +++ b/src/common/struct/DictionaryList.ts @@ -208,6 +208,19 @@ export default class DictionaryList { return list.some(callback); } + /** + * Determines whether all the members of an array satisfy the specified test. + * + * @param key The key to access the list in the dictionary. + * @param callback Test each element, and return true if an element passes the callback. + */ + public every(key: string, callback: Callback): boolean { + const list = this.dictionary[key]; + if (!list) return true; + + return list.every(callback); + } + /** * Sorts a list for a given key. * diff --git a/src/common/struct/Mission.ts b/src/common/struct/Mission.ts index 420c08a6..f32137e2 100644 --- a/src/common/struct/Mission.ts +++ b/src/common/struct/Mission.ts @@ -172,11 +172,10 @@ export default abstract class Mission { return; } - const pendingAssignVehicleIds = Object.keys(this.activeVehicleMapping[this.missionName]).map( - (vehicleIdString): number => parseInt(vehicleIdString, 10), - ); + const pendingAssignVehicleIds = Object.keys(this.activeVehicleMapping[this.missionName]) + .map((vehicleIdString): number => parseInt(vehicleIdString, 10)); - const allVehiclesReady = pendingAssignVehicleIds.some((vehicleId): boolean => this.vehicles[vehicleId].getStatus() !== 'ready'); + const allVehiclesReady = pendingAssignVehicleIds.every((vehicleId): boolean => this.vehicles[vehicleId].getStatus() === 'ready'); if (!allVehiclesReady) { this.stop(false, `Cannot assign jobs to all vehicles in ${this.missionName} as some are not in a ready state`); @@ -190,8 +189,6 @@ export default abstract class Mission { const onSuccess = (vehicleId: number): void => { pendingAssignVehicleIds.splice(pendingAssignVehicleIds.indexOf(vehicleId), 1); - console.log('sucess', vehicleId); - if (pendingAssignVehicleIds.length === 0) { ipc.postLogMessages({ type: 'success', @@ -206,8 +203,6 @@ export default abstract class Mission { * Callback when a vehicle fails to acknowledge the job assignment. */ const onDisconnect = (vehicleId: number): void => { - console.log('disconnect', vehicleId); - ipc.postStopSendingMessages(); this.stop(false, `Failed to assign job to ${(vehicleConfig.vehicleInfos[vehicleId] as VehicleInfo).name}, as it has disconnected`); }; @@ -216,7 +211,6 @@ export default abstract class Mission { * Callback when a vehicle performing a job enters an error state. */ const onError = (vehicleId: number, message?: string): void => { - console.log('error', vehicleId); ipc.postLogMessages({ type: 'failure', message: `${(vehicleConfig.vehicleInfos[vehicleId] as VehicleInfo).name} has entered an error state in ${this.missionName}: ${message || 'No error message specified'}`, @@ -246,24 +240,18 @@ export default abstract class Mission { return; } - console.log('LETS GO'); - const jobTasks = this.generateTasks(); if (!jobTasks) { this.stop(false, `No tasks were generated from ${this.missionName}`); return; } - const allValidTasksForJobs = jobTasks.keys().some( - (jobType): boolean => !vehicleConfig.isValidJobType(jobType as JobType) - || jobTasks.some( - jobType, - (task): boolean => !vehicleConfig.isValidTaskTypeForJob( - task.taskType, - jobType as JobType, - ), - ), - ); + const allValidTasksForJobs = jobTasks.keys() + .every((jobType): boolean => vehicleConfig.isValidJobType(jobType as JobType) + || jobTasks.every(jobType, (task): boolean => !vehicleConfig.isValidTaskTypeForJob( + task.taskType, + jobType as JobType, + ))); if (!allValidTasksForJobs) { this.stop(false, `Generated tasks in ${this.missionName} are invalid for their respective jobs`); diff --git a/src/common/struct/Vehicle.ts b/src/common/struct/Vehicle.ts index 0d9e210f..5e4c55f5 100644 --- a/src/common/struct/Vehicle.ts +++ b/src/common/struct/Vehicle.ts @@ -59,7 +59,7 @@ export default class Vehicle { /** * Jobs the vehicle has. These define the tasks the vehicle is capable of performing. */ - private jobs: string[]; + private jobs: JobType[]; /** * Current latitude of the vehicle. Starts at 0. @@ -158,7 +158,7 @@ export default class Vehicle { public getStatus(): VehicleStatus { return this.status; } - public getJobs(): string[] { return this.jobs; } + public getJobs(): JobType[] { return this.jobs; } public getLat(): number { return this.lat; } @@ -207,8 +207,10 @@ export default class Vehicle { * its update messages will bring it to its real status. */ public connect(): void { - this.status = 'ready'; - this.lastConnectionTime = Date.now(); + this.updateEventHandler.events({ + status: 'ready', + time: Date.now(), + }); } /** @@ -217,7 +219,7 @@ export default class Vehicle { * if it is disconnected. */ public disconnect(): void { - this.status = 'disconnected'; + this.updateEventHandler.event('status', 'disconnected'); } /** diff --git a/src/renderer/mainWindow/map/MapContainer.tsx b/src/renderer/mainWindow/map/MapContainer.tsx index abfb520e..fce1dca8 100644 --- a/src/renderer/mainWindow/map/MapContainer.tsx +++ b/src/renderer/mainWindow/map/MapContainer.tsx @@ -242,11 +242,13 @@ export default class MapContainer extends Component { updateMap: boolean, ...waypoints: { name: string; location: Location }[] ): void { + if (!updateMap) return; + const { waypoints: currentWaypoints } = this.state; const newWaypoints = currentWaypoints; waypoints.forEach((waypoint): void => { - if (newWaypoints[waypoint.name] && updateMap) { + if (newWaypoints[waypoint.name]) { newWaypoints[waypoint.name].location = waypoint.location; } }); @@ -306,11 +308,13 @@ export default class MapContainer extends Component { color?: string; }[] ): void { + if (!updateMap) return; + const { boundingBoxes: currentBoundingBoxes } = this.state; const newBoundingBoxes = currentBoundingBoxes; boundingBoxes.forEach((boundingBox): void => { - if (newBoundingBoxes[boundingBox.name] && updateMap) { + if (newBoundingBoxes[boundingBox.name]) { newBoundingBoxes[boundingBox.name].bounds = boundingBox.bounds; if (boundingBox.color) newBoundingBoxes[boundingBox.name].color = boundingBox.color; } diff --git a/src/renderer/missionWindow/MissionWindow.tsx b/src/renderer/missionWindow/MissionWindow.tsx index 322695e6..04619ce4 100644 --- a/src/renderer/missionWindow/MissionWindow.tsx +++ b/src/renderer/missionWindow/MissionWindow.tsx @@ -135,6 +135,7 @@ export default class MissionWindow extends Component { }; this.onSliderChange = this.onSliderChange.bind(this); + this.updateVehicles = this.updateVehicles.bind(this); this.updateInformation = this.updateInformation.bind(this); this.updateOptions = this.updateOptions.bind(this); this.postStartMissions = this.postStartMissions.bind(this); @@ -147,15 +148,15 @@ export default class MissionWindow extends Component { } public componentDidMount(): void { - ipcRenderer.on('updateVehicles', (_: Event, ...vehicles: VehicleObject[]): void => updateVehicles(this, ...vehicles)); + ipcRenderer.on('updateVehicles', (_: Event, ...vehicles: VehicleObject[]): void => this.updateVehicles(...vehicles)); ipcRenderer.on('updateInformation', (_: Event, information: MissionInformation.Information): void => this.updateInformation(information)); ipcRenderer.on('updateOptions', (_: Event, missionName: MissionInformation.MissionName, option: string, value: boolean): void => this.updateOptions(missionName, option, value)); ipcRenderer.on('updateActiveVehicleMapping', (_: Event, missionName: MissionInformation.MissionName, jobType: JobType, vehicleId: number): void => this.updateActiveVehicleMapping(missionName, jobType, vehicleId)); - ipcRenderer.on('confirmCompleteMission', (): void => { this.setState({ status: 'next' }); }); - ipcRenderer.on('stopMissions', (): void => { this.stopMissions(); }); - ipcRenderer.on('finishMissions', (): void => { this.setState({ status: 'finished' }); }); + ipcRenderer.on('confirmCompleteMission', (): void => this.setState({ status: 'next' })); + ipcRenderer.on('stopMissions', (): void => this.stopMissions()); + ipcRenderer.on('finishMissions', (): void => this.setState({ status: 'finished' })); } private onSliderChange(value: [number, number]): void { @@ -165,6 +166,31 @@ export default class MissionWindow extends Component { }); } + private updateVehicles(...vehicles: VehicleObject[]): void { + updateVehicles(this, ...vehicles); + + const { activeVehicleMapping: newActiveVehicleMapping } = this.state; + + // Check for disconnected vehicles in activeVehicleMapping and remove them. + vehicles.forEach((vehicle): void => { + if (vehicle.status === 'disconnected') { + if (Object.keys(newActiveVehicleMapping.isrSearch).includes(`${vehicle.vehicleId}`)) { + delete newActiveVehicleMapping.isrSearch[vehicle.vehicleId]; + } else if (Object.keys(newActiveVehicleMapping.vtolSearch).includes(`${vehicle.vehicleId}`)) { + delete newActiveVehicleMapping.vtolSearch[vehicle.vehicleId]; + } else if (Object.keys(newActiveVehicleMapping.payloadDrop).includes(`${vehicle.vehicleId}`)) { + delete newActiveVehicleMapping.payloadDrop[vehicle.vehicleId]; + } else if (Object.keys(newActiveVehicleMapping.ugvRescue).includes(`${vehicle.vehicleId}`)) { + delete newActiveVehicleMapping.ugvRescue[vehicle.vehicleId]; + } else if (Object.keys(newActiveVehicleMapping.uuvRescue).includes(`${vehicle.vehicleId}`)) { + delete newActiveVehicleMapping.uuvRescue[vehicle.vehicleId]; + } + } + }); + + this.setState({ activeVehicleMapping: newActiveVehicleMapping }); + } + private updateInformation(information: MissionInformation.Information): void { const { information: currentInformation } = this.state; const newInformation = currentInformation; @@ -207,7 +233,20 @@ export default class MissionWindow extends Component { const { activeVehicleMapping: currentActiveVehicleMapping } = this.state; const newActiveVehicleMapping = currentActiveVehicleMapping; - newActiveVehicleMapping[missionName][vehicleId] = jobType; + if (vehicleId !== 0) { + newActiveVehicleMapping[missionName][vehicleId] = jobType; + } else { + const deleteVehicleIdString = Object.keys(newActiveVehicleMapping) + .find((vehicleIdString): boolean => { + const vid = parseInt(vehicleIdString, 10); + return newActiveVehicleMapping[missionName][vid] === jobType; + }); + + if (deleteVehicleIdString) { + const deleteVehicleId = parseInt(deleteVehicleIdString, 10); + delete newActiveVehicleMapping[missionName][deleteVehicleId]; + } + } this.setState({ activeVehicleMapping: newActiveVehicleMapping }); } diff --git a/src/renderer/missionWindow/parameter/UGVRescue.tsx b/src/renderer/missionWindow/parameter/UGVRescue.tsx index 7fbedd11..4ee71955 100644 --- a/src/renderer/missionWindow/parameter/UGVRescue.tsx +++ b/src/renderer/missionWindow/parameter/UGVRescue.tsx @@ -190,5 +190,5 @@ export class UGVRescue extends Component { export default { missionName, - layout: UGVRescue as React.ElementType, + layout: UGVRescue, }; diff --git a/src/renderer/missionWindow/parameter/VTOLSearch.tsx b/src/renderer/missionWindow/parameter/VTOLSearch.tsx index 03622c1d..6caa5308 100644 --- a/src/renderer/missionWindow/parameter/VTOLSearch.tsx +++ b/src/renderer/missionWindow/parameter/VTOLSearch.tsx @@ -12,8 +12,7 @@ import { readyToStart } from '../../../util/parameter'; import CreateBoundingBoxButton from '../extra/CreateBoundingBoxButton'; -type VTOLSearchChecklistType = 'quickScanTop' | 'quickScanLeft' | 'quickScanRight' -| 'quickScanBottom'; +type VTOLSearchChecklistType = 'quickScanTop' | 'quickScanLeft' | 'quickScanRight' | 'quickScanBottom'; const checklistCache: { [check in VTOLSearchChecklistType ]: number | undefined} = { quickScanTop: undefined, @@ -239,5 +238,5 @@ export class VTOLSearch extends Component { export default { missionName, - layout: VTOLSearch as React.ElementType, + layout: VTOLSearch, }; diff --git a/src/static/config.json b/src/static/config.json index e63f3126..41da663b 100644 --- a/src/static/config.json +++ b/src/static/config.json @@ -1,7 +1,7 @@ { - "fixtures": true, - "geolocation": false, + "fixtures": false, + "geolocation": true, - "vehicleDisconnectionTime": 3, + "vehicleDisconnectionTime": 20, "messageSendRate": 10 } diff --git a/src/types/vehicle.ts b/src/types/vehicle.ts index b7e4e9dc..2245a362 100644 --- a/src/types/vehicle.ts +++ b/src/types/vehicle.ts @@ -1,3 +1,5 @@ +import { JobType } from '../static/index'; + /** * Status of the vehicle, lets us know of where the vehicle is in its mission. */ @@ -25,7 +27,7 @@ export interface VehicleObject { /** * Jobs of the vehicle. */ - jobs: string[]; + jobs: JobType[]; /** * Current latitude of the vehicle. Starts at 0. diff --git a/src/util/ipc.ts b/src/util/ipc.ts index 675909b2..cffdbbe4 100644 --- a/src/util/ipc.ts +++ b/src/util/ipc.ts @@ -52,6 +52,7 @@ function postCompleteMission( * Post "confirmCompleteMission" notification. * * Files that take this notification: + * - renderer/missionWindow/MissionWindow */ function postConfirmCompleteMission(): void { ipcRenderer.send('post', 'confirmCompleteMission'); @@ -72,7 +73,6 @@ function postConnectToVehicle(jsonMessage: Message.JSONMessage, newMessage: bool * * Files that take this notification: * - renderer/mainWindow/map/MapContainer - * - renderer/missionWindow/extra/CreateBoundingBox */ function postCreateBoundingBoxes( ...boundingBoxes: { name: string; color?: string; bounds?: BoundingBoxBounds}[] @@ -105,6 +105,7 @@ function postDisconnectFromVehicle(vehicleId: number): void { * notification in that this is sent once all missions to run are completed. * * Files that take this notification: + * - renderer/missionWindow/MissionWindow */ function postFinishMissions(completionParameters: Task.TaskParameters[]): void { ipcRenderer.send('post', 'finishMissions', completionParameters); @@ -310,7 +311,6 @@ function postStartMissions( * * Files that take this notification: * - common/Orchestrator - * - renderer/missionWindow/MissionWindow */ function postStartNextMission(): void { ipcRenderer.send('post', 'startNextMission'); @@ -362,8 +362,9 @@ function postToggleTheme(): void { * * Files that take this notification: * - renderer/missionWindow/parameter/ISRSearch - * - renderer/missionWindow/parameter/VTOLSearch * - renderer/missionWindow/parameter/PayloadDrop + * - renderer/missionWindow/parameter/UGVRescue + * - renderer/missionWindow/parameter/VTOLSearch */ function postUnlockParameterInputs(waypointType: string): void { ipcRenderer.send('post', 'unlockParameterInputs', waypointType); @@ -445,6 +446,7 @@ function postUpdateOptions( * Files that take this notification: * - renderer/mainWindow/map/MapContainer * - renderer/mainWindow/vehicle/VehicleContainer + * - renderer/missionWindow/MissionWindow */ function postUpdateVehicles(...vehicles: VehicleObject[]): void { ipcRenderer.send('post', 'updateVehicles', ...vehicles); From ec621da8b72947f13898101ee490b87df2f22c20 Mon Sep 17 00:00:00 2001 From: Paul Matthew Barrameda Date: Sat, 27 Apr 2019 01:14:36 -0700 Subject: [PATCH 130/152] Version bump --- .eslintrc.yml | 4 ---- package.json | 12 ++++++------ 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/.eslintrc.yml b/.eslintrc.yml index c79416a7..6904f828 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -31,10 +31,6 @@ settings: # Additional rules rules: - # Turning off this because its buggy https://github.com/benmosher/eslint-plugin-import/pull/1304 - import/named: off - import/no-useless-path-segments: off - # Turn off linting about having wrong line ending, handled in .gitattributes linebreak-style: off diff --git a/package.json b/package.json index fc00d723..1d8e1916 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "GCS", - "version": "0.7.1", + "version": "0.7.2", "license": "MIT", "author": "Northrop Grumman Collaboration Project", "description": "Ground Control Station for autonomous vehicle platforms in NGCP", @@ -37,16 +37,16 @@ "@types/chai": "^4.1.7", "@types/mocha": "^5.2.6", "@types/msgpack-lite": "^0.1.6", - "@types/node": "^11.13.5", + "@types/node": "^11.13.8", "@types/pouchdb": "^6.3.3", "@types/rc-slider": "^8.6.3", - "@types/react": "^16.8.13", + "@types/react": "^16.8.14", "@types/react-dom": "^16.8.4", "@types/react-leaflet": "^2.2.1", "@types/react-virtualized": "^9.21.1", "@types/serialport": "^7.0.2", - "@typescript-eslint/eslint-plugin": "^1.6.0", - "@typescript-eslint/parser": "^1.6.0", + "@typescript-eslint/eslint-plugin": "^1.7.0", + "@typescript-eslint/parser": "^1.7.0", "babel-eslint": "^10.0.1", "chai": "^4.2.0", "dotenv-webpack": "^1.7.0", @@ -67,7 +67,7 @@ "stylelint-config-standard": "^18.3.0", "stylelint-order": "^3.0.0", "ts-node": "^8.1.0", - "typescript": "^3.4.4", + "typescript": "^3.4.5", "webpack": "^4.30.0" }, "build": { From e627fc2ebe1fe3b8ea2d3c64b1a813cc5e8e130d Mon Sep 17 00:00:00 2001 From: Paul Matthew Barrameda Date: Sat, 27 Apr 2019 01:15:22 -0700 Subject: [PATCH 131/152] Node 12 makes serialport crash --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index facbc8ea..915967ab 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,7 +8,7 @@ before_script: # Use latest version of Node.js node_js: - - node + - lts/* # Cache node_modules after testing cache: npm From 96d2a143ede8872bcb3cecf35d27e9f69c1132f5 Mon Sep 17 00:00:00 2001 From: Marlon Aquino Date: Sat, 27 Apr 2019 08:51:26 -0700 Subject: [PATCH 132/152] Added comments for state --- src/renderer/missionWindow/parameter/UGVRescue.tsx | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/renderer/missionWindow/parameter/UGVRescue.tsx b/src/renderer/missionWindow/parameter/UGVRescue.tsx index 4ee71955..0855bac0 100644 --- a/src/renderer/missionWindow/parameter/UGVRescue.tsx +++ b/src/renderer/missionWindow/parameter/UGVRescue.tsx @@ -38,10 +38,21 @@ export interface UGVRescueProps { } interface State { + /** + * Checklist of all required waypoints/coordinates. This is used to generate the + * parameters for the mission. + */ checklist: { [check in UGVChecklistType]: number | undefined }; + /** + * True once all checks in checklist are filled in properly. + */ ready: boolean; + /** + * True if the inputs for the waypoint type is disabled. Will become disabled + * once the create pin is clicked. + */ locked: Locked; } From 21de86f19f3671298174b019787463cf503ad61e Mon Sep 17 00:00:00 2001 From: Paul Matthew Barrameda Date: Sat, 27 Apr 2019 11:39:26 -0700 Subject: [PATCH 133/152] Delete fossa test as it is very inconsistent --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 915967ab..b49db0a1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -22,4 +22,3 @@ script: - fossa init - fossa analyze - - fossa test From 051b756e9521fcbc9aa221ed541f44c4532bb399 Mon Sep 17 00:00:00 2001 From: Paul Matthew Barrameda Date: Sat, 27 Apr 2019 12:31:43 -0700 Subject: [PATCH 134/152] Added support to VTOL's guide job --- src/static/index.ts | 5 +++-- src/static/vehicle.json | 3 ++- src/types/message.ts | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/static/index.ts b/src/static/index.ts index 09b84f79..367d4a0f 100644 --- a/src/static/index.ts +++ b/src/static/index.ts @@ -66,7 +66,7 @@ const vehicleIds: { * All valid job types. This should always match up to the job types in vehicle.json. */ export type JobType = 'isrSearch' | 'payloadDrop' | 'ugvRescue' | 'uuvRescue' -| 'quickScan' | 'detailedSearch'; +| 'quickScan' | 'detailedSearch' | 'guide'; const vehicleInfos: { [vehicleId: number]: VehicleInfo | undefined; @@ -112,7 +112,8 @@ function isValidJobType(jobType: string): boolean { || jobType === 'ugvRescue' || jobType === 'uuvRescue' || jobType === 'quickScan' - || jobType === 'detailedSearch'; + || jobType === 'detailedSearch' + || jobType === 'guide'; } /** diff --git a/src/static/vehicle.json b/src/static/vehicle.json index 4a2efe24..2d8166d0 100644 --- a/src/static/vehicle.json +++ b/src/static/vehicle.json @@ -56,7 +56,8 @@ "ugvRescue": ["retrieveTarget", "deliverTarget"], "uuvRescue": ["retrieveTarget"], "quickScan": ["quickScan"], - "detailedSearch": ["detailedSearch"] + "detailedSearch": ["detailedSearch"], + "guide": [] }, "vehicleStatuses": { "ready": { diff --git a/src/types/message.ts b/src/types/message.ts index 673ae752..ba166606 100644 --- a/src/types/message.ts +++ b/src/types/message.ts @@ -304,7 +304,7 @@ export function isJSONMessage(message: { [key: string]: any }): boolean { if (!isMessage(message)) return false; // Check if id is a number. - const idCheck = message.id !== '' && message.id !== true && !Number.isNaN(message.id); + const idCheck = message.id && typeof message.id !== 'boolean' && !Number.isNaN(message.id); // Check if tid is a number and is valid. const tidCheck = !Number.isNaN(message.tid) From 73a8714383ccfb0be85ac27ac62bc56e157e387f Mon Sep 17 00:00:00 2001 From: Paul Matthew Barrameda Date: Sat, 27 Apr 2019 13:27:01 -0700 Subject: [PATCH 135/152] Made JSONMessage case sensitive to type field --- src/common/MessageHandler.ts | 1 - src/types/message.ts | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/common/MessageHandler.ts b/src/common/MessageHandler.ts index b0d4635c..058c6365 100644 --- a/src/common/MessageHandler.ts +++ b/src/common/MessageHandler.ts @@ -137,7 +137,6 @@ class MessageHandler { return; } - message.type = message.type.toLowerCase(); // eslint-disable-line no-param-reassign const jsonMessage = message as Message.JSONMessage; // Ignore messages from unrecognized vehicles. diff --git a/src/types/message.ts b/src/types/message.ts index ba166606..c7951dea 100644 --- a/src/types/message.ts +++ b/src/types/message.ts @@ -283,7 +283,7 @@ export function isMessage(message: { [key: string]: any }): boolean { 'connect', 'ack', 'badMessage', - ].includes(message.type.toLowerCase()); + ].includes(message.type); } /** From eef5d6152e5cc10fc784fafebdcfbd9d8911a73e Mon Sep 17 00:00:00 2001 From: Paul Matthew Barrameda Date: Mon, 29 Apr 2019 22:53:49 -0700 Subject: [PATCH 136/152] Fixed logic in sending messages Modified to support that connectionAck messages should not be acknowledged. Fixes the first checkpoint in #68. Not tested, but most likely works. --- src/common/MessageHandler.ts | 88 ++++++++++++++++++----------- src/common/Orchestrator.ts | 44 +++++++-------- src/common/struct/DictionaryList.ts | 13 +++++ 3 files changed, 89 insertions(+), 56 deletions(-) diff --git a/src/common/MessageHandler.ts b/src/common/MessageHandler.ts index 058c6365..0ca60175 100644 --- a/src/common/MessageHandler.ts +++ b/src/common/MessageHandler.ts @@ -13,15 +13,15 @@ import xbee from './Xbee'; class MessageHandler { /** - * Dictionary for all messages that are being sent, sent and acknowledged, and received. - * - * Description of keys: - * "outbox": will have a list of messages that are being sent, but are awaiting - * acknowledgement. - * "sent": will have a list of messages that have been sent. - * "received": will have a list of messages that have been received. + * Dictionary for all messages to be sent. Messages are in a list mapped by the vehicle + * id they are to be sent to. + */ + private outbox = new DictionaryList(); + + /** + * Messages that are currently being sent, map by the vehicle id it is being sent to. */ - private messageDictionary = new DictionaryList(); + private sending = new Map(); /** * Handler that listens for different update events. @@ -46,18 +46,12 @@ class MessageHandler { ipcRenderer.on('stopSendingMessages', (): void => this.stopSendingMessages()); } - /** - * Removes a message from the outbox. - * - * @param id Message id - */ - private removeMessage(key: 'outbox' | 'sent' | 'received', id: number): Message.JSONMessage | undefined { - return this.messageDictionary.remove(key, - (message: Message.JSONMessage): boolean => message.id === id); - } - /** * Sends message to vehicle through Xbee. + * Acknowledgement/Connection Acknowledgement/Bad messages are all sent here, but + * all other messages are forwarded to sendMessageAsync. All messages except the + * three mentioned above are only sent one at a time (and everything else will) + * be stored in the outbox to be sent afterwards. * * @param vehicleId Vehicle id to send the message to. * @param message Message format (note this does not have sid, tid, id, and time). @@ -73,16 +67,28 @@ class MessageHandler { this.id += 1; - xbee.sendMessage(jsonMessage); - this.messageDictionary.push('sent', jsonMessage); - if (Message.TypeGuard.isAcknowledgementMessage(message) + || Message.TypeGuard.isConnectionAcknowledgementMessage(message) || Message.TypeGuard.isBadMessage(message)) { + xbee.sendMessage(jsonMessage); + return; + } + + if (this.sending.has(jsonMessage.sid)) { + this.outbox.push(`${jsonMessage.sid}`, jsonMessage); return; } - this.messageDictionary.push('outbox', jsonMessage); + this.sendMessageAsync(jsonMessage); + } + /** + * Either sends the message repeatedly or adds it to the outbox to be + * sent later. Once the message is acknowledged, the next one will be sent, + * if one is in the outbox. + */ + private sendMessageAsync(jsonMessage: Message.JSONMessage): void { + xbee.sendMessage(jsonMessage); const expiry = setInterval( (): void => xbee.sendMessage(jsonMessage), config.messageSendRate * 1000, @@ -90,18 +96,31 @@ class MessageHandler { // Keep track of message, to stop sending it once acknowledged. const hash = `${jsonMessage.tid}#${jsonMessage.id}`; - this.updateHandler.addHandler(hash, (): boolean => { + this.sending.set(jsonMessage.tid, jsonMessage); + + // Callback when this message is acknowledged. + const onAcknowledge = (): boolean => { clearInterval(expiry); - this.removeMessage('outbox', jsonMessage.id); + if (this.outbox.size(`${jsonMessage.tid}`) > 0) { + const nextMessage = this.outbox.shift(`${jsonMessage.tid}`) as Message.JSONMessage; + this.sendMessageAsync(nextMessage); + } else { + this.sending.delete(jsonMessage.tid); + } return true; - }, { - time: config.vehicleDisconnectionTime * 1000, - callback: (): void => { - clearInterval(expiry); - this.removeMessage('outbox', jsonMessage.id); - ipc.postDisconnectFromVehicle(vehicleId); - }, - }); + }; + + const onDisconnect = (): void => { + clearInterval(expiry); + this.sending.delete(jsonMessage.tid); + ipc.postDisconnectFromVehicle(jsonMessage.tid); + }; + + this.updateHandler.addHandler(hash, + (): boolean => onAcknowledge(), { + time: config.vehicleDisconnectionTime * 1000, + callback: (): void => onDisconnect(), + }); } /** @@ -164,7 +183,6 @@ class MessageHandler { // Logic: https://ground-control-station.readthedocs.io/en/latest/communications/messages/other-messages.html#acknowledgement-message if (newMessage) { this.receivedMessageId[jsonMessage.sid] = jsonMessage.id; - this.messageDictionary.push('received', jsonMessage); } } @@ -180,7 +198,9 @@ class MessageHandler { * Stops sending all messages. */ private stopSendingMessages(): void { - this.messageDictionary.forEach('outbox', (jsonMessage): void => { + this.outbox.clear(); + + this.sending.forEach((jsonMessage): void => { const hash = `${jsonMessage.tid}#${jsonMessage.id}`; this.updateHandler.event(hash, true); }); diff --git a/src/common/Orchestrator.ts b/src/common/Orchestrator.ts index 295eaba0..da89f2b7 100644 --- a/src/common/Orchestrator.ts +++ b/src/common/Orchestrator.ts @@ -108,32 +108,32 @@ class Orchestrator { } /** - * Connects to vehicle specified by the connect message. + * Connects to vehicle specified by the connect message from message handler. * @param jsonMessage The connect message. */ private connectToVehicle(jsonMessage: Message.JSONMessage, newMessage: boolean): void { if (this.vehicles[jsonMessage.sid] && this.vehicles[jsonMessage.sid].getStatus() !== 'disconnected') return; - if (!newMessage) return; - // Put this after newMessage return since this message will be sent repeatedly. - if (!config.fixtures) ipc.postSendMessage(jsonMessage.sid, { type: 'connectionAck' }); + if (newMessage) { + if (!this.vehicles[jsonMessage.sid]) { + this.vehicles[jsonMessage.sid] = new Vehicle({ + sid: jsonMessage.sid, + jobs: (jsonMessage as Message.ConnectMessage).jobsAvailable, + status: 'ready', + }); - if (!this.vehicles[jsonMessage.sid]) { - this.vehicles[jsonMessage.sid] = new Vehicle({ - sid: jsonMessage.sid, - jobs: (jsonMessage as Message.ConnectMessage).jobsAvailable, - status: 'ready', - }); + this.ping(this.vehicles[jsonMessage.sid]); + } else { + this.vehicles[jsonMessage.sid].connect(); + } - this.ping(this.vehicles[jsonMessage.sid]); - } else { - this.vehicles[jsonMessage.sid].connect(); + ipc.postLogMessages({ + type: 'success', + message: `${(vehicleConfig.vehicleInfos[jsonMessage.sid] as VehicleInfo).name} has connected`, + }); } - ipc.postLogMessages({ - type: 'success', - message: `${(vehicleConfig.vehicleInfos[jsonMessage.sid] as VehicleInfo).name} has connected`, - }); + ipc.postSendMessage(jsonMessage.sid, { type: 'connectionAck' }); } /** @@ -169,7 +169,7 @@ class Orchestrator { } /** - * Handles acknowledgement messages. + * Handles acknowledgement messages from the message handler. * @param jsonMessage Message from vehicle. */ private handleAcknowledgementMessage( @@ -185,7 +185,7 @@ class Orchestrator { } /** - * Handles bad messages. + * Handles bad messages from the message handler. * @param jsonMessage Message from vehicle. */ private handleBadMessage(jsonMessage: Message.JSONMessage, newMessage: boolean): void { @@ -202,7 +202,7 @@ class Orchestrator { } /** - * Handles update messages. + * Handles update messages from the message handler. * @param jsonMessage Message from vehicle. */ private handleUpdateMessage(jsonMessage: Message.JSONMessage, newMessage: boolean): void { @@ -226,7 +226,7 @@ class Orchestrator { } /** - * Handles point of interest messages. + * Handles point of interest messages from the message handler. * @param jsonMessage Message from vehicle. */ private handlePOIMessage(jsonMessage: Message.JSONMessage, newMessage: boolean): void { @@ -249,7 +249,7 @@ class Orchestrator { } /** - * Handles complete messages. + * Handles complete messages from the message handler. * @param jsonMessage Message from vehicle. */ private handleCompleteMessage(jsonMessage: Message.JSONMessage, newMessage: boolean): void { diff --git a/src/common/struct/DictionaryList.ts b/src/common/struct/DictionaryList.ts index 4713002b..677b16fa 100644 --- a/src/common/struct/DictionaryList.ts +++ b/src/common/struct/DictionaryList.ts @@ -262,4 +262,17 @@ export default class DictionaryList { return this.numItems; } + + /** + * Clears all items in the dictionary, or a certain list if a key is provided. + * + * @param key The key to access the list in the dictionary. + */ + public clear(key?: string): void { + if (key && this.dictionary[key]) { + this.dictionary[key] = []; + } + + this.dictionary = {}; + } } From 524ee0ea8555afd2c410031a081c8312f4672036 Mon Sep 17 00:00:00 2001 From: Paul Matthew Barrameda Date: Mon, 29 Apr 2019 23:58:45 -0700 Subject: [PATCH 137/152] Added logic to support POI makers in VTOL Search mission Not tested. Fixes checkmark 2 of #68. Logic is to keep track of the detailed search task that is assigned, and see if a POI message is sent from the VTOL (that is doing the detailed search job). If so, that POI is valid. If a complete message is sent with no POI message in between, it means that the POI is invalid. --- src/common/missions/VTOLSearch.ts | 59 ++++++++++++++++-- src/common/struct/Mission.ts | 2 +- src/renderer/mainWindow/map/MapContainer.tsx | 30 +++++++++ .../mainWindow/map/marker/POIMarker.tsx | 32 ++++++++++ .../{poi_unkwn.png => poi/invalid.png} | Bin .../markers/{poi_fp.png => poi/unknown.png} | Bin .../markers/{poi_vld.png => poi/valid.png} | Bin src/static/images/pin.png | Bin 7515 -> 0 bytes src/static/index.ts | 18 +++--- src/util/ipc.ts | 11 ++++ 10 files changed, 137 insertions(+), 15 deletions(-) create mode 100644 src/renderer/mainWindow/map/marker/POIMarker.tsx rename src/static/images/markers/{poi_unkwn.png => poi/invalid.png} (100%) rename src/static/images/markers/{poi_fp.png => poi/unknown.png} (100%) rename src/static/images/markers/{poi_vld.png => poi/valid.png} (100%) delete mode 100644 src/static/images/pin.png diff --git a/src/common/missions/VTOLSearch.ts b/src/common/missions/VTOLSearch.ts index c4680e08..9fd6dec6 100644 --- a/src/common/missions/VTOLSearch.ts +++ b/src/common/missions/VTOLSearch.ts @@ -16,6 +16,11 @@ export const missionName: MissionInformation.MissionName = 'vtolSearch'; export const jobTypes: JobType[] = ['quickScan', 'detailedSearch']; +interface POITracker { + location: Location; + valid: boolean; +} + export class VTOLSearch extends Mission { protected missionName = missionName; @@ -23,9 +28,6 @@ export class VTOLSearch extends Mission { protected addTaskCompare = { detailedSearch: (a: Task.Task, b: Task.Task): number => { - if (!Task.TypeGuard.isDetailedSearchTask(a)) return 1; - if (!Task.TypeGuard.isDetailedSearchTask(b)) return 1; - const vehicle = this.getDetailedSearchVehicle(); if (!vehicle) return 1; @@ -42,6 +44,13 @@ export class VTOLSearch extends Mission { */ private missionData: Location | null = null; + /** + * Keeps track of the current detailed search task to see if the POI is valid or not. + */ + private currentDetailedSearchTask: { + [vehicleId: number]: POITracker | undefined; + } = {}; + private getDetailedSearchVehicle(): Vehicle | undefined { const vehicleIdString = Object.keys(this.activeVehicleMapping) .find((idString): boolean => { @@ -67,6 +76,18 @@ export class VTOLSearch extends Mission { return tasks; } + protected assignTask(vehicle: Vehicle, task: Task.Task): boolean { + if (Task.TypeGuard.isDetailedSearchTask(task)) { + const dtask = task as Task.DetailedSearchTask; + this.currentDetailedSearchTask[vehicle.getVehicleId()] = { + location: { lat: dtask.lat, lng: dtask.lng }, + valid: false, + }; + } + + return super.assignTask(vehicle, task); + } + public update(jsonMessage: Message.JSONMessage): void { super.update(jsonMessage); @@ -74,13 +95,39 @@ export class VTOLSearch extends Mission { const poiMessage = jsonMessage as Message.POIMessage; if (this.activeVehicleMapping[this.missionName][jsonMessage.sid] === 'quickScan') { + const location = { lat: poiMessage.lat, lng: poiMessage.lng }; this.addTask('detailedSearch', { taskType: 'detailedSearch', - lat: poiMessage.lat, - lng: poiMessage.lng, + ...location, }); - } else if (this.activeVehicleMapping[this.missionName][jsonMessage.sid] === 'detailedSearch') { + + ipc.postUpdatePOIs({ + location, + type: 'unknown', + }); + + return; + } + + if (this.activeVehicleMapping[this.missionName][jsonMessage.sid] === 'detailedSearch') { this.missionData = { lat: poiMessage.lat, lng: poiMessage.lng }; + + // Sets current task POI as valid, with a check if there is a task right now or not. + const tracker = this.currentDetailedSearchTask[jsonMessage.sid]; + if (tracker) tracker.valid = true; + } + return; + } + + if (Message.TypeGuard.isCompleteMessage(jsonMessage)) { + if (this.activeVehicleMapping[this.missionName][jsonMessage.sid] === 'detailedSearch') { + const tracker = this.currentDetailedSearchTask[jsonMessage.sid]; + if (tracker) { + ipc.postUpdatePOIs({ + location: tracker.location, + type: tracker.valid ? 'valid' : 'invalid', + }); + } } } } diff --git a/src/common/struct/Mission.ts b/src/common/struct/Mission.ts index f32137e2..8fd69772 100644 --- a/src/common/struct/Mission.ts +++ b/src/common/struct/Mission.ts @@ -404,7 +404,7 @@ export default abstract class Mission { * @param vehicle Vehicle to assign task to. * @param task Task to assign vehicle. */ - private assignTask(vehicle: Vehicle, task: Task): boolean { + protected assignTask(vehicle: Vehicle, task: Task): boolean { const success = vehicle.assignTask(task); if (!success) { diff --git a/src/renderer/mainWindow/map/MapContainer.tsx b/src/renderer/mainWindow/map/MapContainer.tsx index fce1dca8..31b7233f 100644 --- a/src/renderer/mainWindow/map/MapContainer.tsx +++ b/src/renderer/mainWindow/map/MapContainer.tsx @@ -23,6 +23,7 @@ import ThemeControl from './control/ThemeControl'; import BoundingBox from './shape/BoundingBox'; // import CachedTileLayer from './CachedTileLayer'; +import POIMarker, { POIMarkerProps } from './marker/POIMarker'; import VehicleMarker from './marker/VehicleMarker'; import WaypointMarker from './marker/WaypointMarker'; @@ -89,6 +90,11 @@ interface State { locked: boolean; }; }; + + /** + * Point of interests. + */ + pois: { [hash: string]: POIMarkerProps }; } /** @@ -108,6 +114,7 @@ export default class MapContainer extends Component { vehicles: {}, boundingBoxes: {}, waypoints: {}, + pois: {}, }; this.ref = React.createRef(); @@ -122,6 +129,7 @@ export default class MapContainer extends Component { this.updateWaypoints = this.updateWaypoints.bind(this); this.createBoundingBoxes = this.createBoundingBoxes.bind(this); this.updateBoundingBoxes = this.updateBoundingBoxes.bind(this); + this.updatePOIs = this.updatePOIs.bind(this); } public componentDidMount(): void { @@ -137,6 +145,8 @@ export default class MapContainer extends Component { ipcRenderer.on('updateWaypoints', (_: Event, updateMap: boolean, ...waypoints: { name: string; location: Location }[]): void => this.updateWaypoints(updateMap, ...waypoints)); ipcRenderer.on('createBoundingBoxes', (_: Event, ...boundingBoxes: { name: string; color?: string; bounds: BoundingBoxBounds}[]): void => this.createBoundingBoxes(...boundingBoxes)); ipcRenderer.on('updateBoundingBoxes', (_: Event, updateMap: boolean, ...boundingBoxes: { name: string; color?: string; bounds: BoundingBoxBounds}[]): void => this.updateBoundingBoxes(updateMap, ...boundingBoxes)); + + ipcRenderer.on('updatePOIs', (_: Event, ...pois: POIMarkerProps[]): void => this.updatePOIs(...pois)); } /** @@ -323,11 +333,27 @@ export default class MapContainer extends Component { this.setState({ boundingBoxes: newBoundingBoxes }); } + /** + * Creates/updates point of interests. + */ + private updatePOIs(...pois: POIMarkerProps[]): void { + const { pois: currentPois } = this.state; + const newPois = currentPois; + + pois.forEach((poi): void => { + const hash = `${poi.location.lat}#${poi.location.lng}`; + newPois[hash] = poi; + }); + + this.setState({ pois: newPois }); + } + public render(): ReactNode { const { theme } = this.props; const { viewport, boundingBoxes, + pois, vehicles, waypoints, } = this.state; @@ -360,6 +386,9 @@ export default class MapContainer extends Component { /> )); + const poiMarkers = Object.keys(pois) + .map((hash): ReactNode => ); + return ( { {boundingBoxRectangles} {vehicleMarkers} {waypointMarkers} + {poiMarkers} ); } diff --git a/src/renderer/mainWindow/map/marker/POIMarker.tsx b/src/renderer/mainWindow/map/marker/POIMarker.tsx new file mode 100644 index 00000000..a75b5c1d --- /dev/null +++ b/src/renderer/mainWindow/map/marker/POIMarker.tsx @@ -0,0 +1,32 @@ +import Leaflet from 'leaflet'; +import React, { PureComponent, ReactNode } from 'react'; +import { Marker } from 'react-leaflet'; + +import { imageConfig, Location, RecursiveImageSignature } from '../../../../static/index'; + +const poiIcons = (imageConfig.markers as RecursiveImageSignature).poi as RecursiveImageSignature; + +export interface POIMarkerProps { + location: Location; + type: 'valid' | 'invalid' | 'unknown'; +} + +export default class POIMarker extends PureComponent { + public render(): ReactNode { + const { + location, + type, + } = this.props; + + return ( + + ); + } +} diff --git a/src/static/images/markers/poi_unkwn.png b/src/static/images/markers/poi/invalid.png similarity index 100% rename from src/static/images/markers/poi_unkwn.png rename to src/static/images/markers/poi/invalid.png diff --git a/src/static/images/markers/poi_fp.png b/src/static/images/markers/poi/unknown.png similarity index 100% rename from src/static/images/markers/poi_fp.png rename to src/static/images/markers/poi/unknown.png diff --git a/src/static/images/markers/poi_vld.png b/src/static/images/markers/poi/valid.png similarity index 100% rename from src/static/images/markers/poi_vld.png rename to src/static/images/markers/poi/valid.png diff --git a/src/static/images/pin.png b/src/static/images/pin.png deleted file mode 100644 index 3d2c6945fc8f30a3e57963ecda2de8a598887edf..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7515 zcmb_>c{o&W`1dnQvW!TPMwT=pMRu~ykTrXx@WognYlMiHlO-fsB3mM47qS$RNkp;} zvhQ0+)-Yz?)9-!%fB*T-b)9SGe$F%ZbDw+v+$YjdUz3>;%Lo8q*49$H4geDVi3Id$ z_{+-kjORbfR!36}fWqi=5-`Q!p=IU;?;iYjAykFZ{NTl--r9QVN2f5nK!Nq}s_Y2> z&a7ywshIc;uciki58g;`nc(H*;tdFHCT__4c zFpUcOP%BIGtb4IAcaxI6FGDNnqy|rO0{hqNf`z8NqKCPe4LdH&A_e{Kc}K;7kj4~A z!}?~4Zqli3ah`-ClZI+quBI4hz56Zg!DfbO{x#lSa=YR%N6F0;=8@ei2)-l^kE*)g zf0SP7rM_Bm-j76snv46_w~gLlg9Fytw)^yp-W;vaJ%wv^9=%Il5E{!BLA+?_&lIWc z(5!#2L_k8-W@Lf=SpUWI+B|pg(n`CyA9w2YvNgrQ9iMU8q>rV!&P7x+C6p+2H6JRLg2&KJm(s4_g0hqs_PVMJY6HK0W zb<>9F{CQBBe>L;pbEuUK-=%eug;w7Uaw0ehwauhZ(ZL(ogkq59oy1e=^AwHYD`~tt zynYa}zij+1>d%wvkt?nkTj0b_Lvk0q&hKe=E-5!kCQMD?h*L^~f$??v*%S*glf0S( zTw4a3b$5lvg3URVF!0zqtvOCxUOUZ|-IfT%pRLbq^EXe>o#1r3eD;VA^g&~Xo)-72 z#d}2RtkQ4%#_x)^Bl%OQ(iyWXpIVM5rP4y-Tp)M1~7f>{?mmE%+c; zg~9^^DTG(YcQxtHB-Op=;GLpTdZv1QtfPKqa|8j|YP*+_uWtLeZP%6Z0FmTW)NvaRP_1priWZVTn7A2R-2*?*pGDEdhHc=jx(ZHmGwPP;&lrbr z68?Or(U+))@r#GDU~E9M2#q@YwXGqd4iRpCMAiGuIMUl4&7-$>%4h$c+h+E0BuYqs zFC%v@CqGMYXf6n=N(5n|RF(r1rO0C`liC@}yc%>nfbk|(X}j7#`|9JNH!Q;xG)6BbT7NanEh~hqYFEuAWt=`ZOm&v@z!(#XoHBx`)olO^U8NrV+^FQ=ax- zO3b|Kk#OfljOycobhcxrRNlS`!?Q$X_wTF-)Z(2z-olc6RymX_7e5hjOM->knC?2m zuNPHzz%brjccc84J;wND%v>O#>+al;-7R&IDWzt$f{Mdm^S9u&9j7mc*s(4OZ->Ng zP{ZS!!JLEuxyREnZRlqzXiVm6!5`s3Cibjpiy(IX6+F*p(fbV&79fkD{FIMd<>I(b9dJamMVmfi&v~Felk6Sl8Zf} znBB9ANo2)(xnlUs&_npcGJ}0ilosr>Qx(!|+*yt}UH8}SH-Yh%Fl-dPwwUQDek9%c z_#P?te5R^|?z&0@d}vPEGtE$YIjb>N6;A{eb{XjIEQ&bPU;C1$1*En# zmfOERSy9Rxh55!3AnR3n!}qajaKD+;avrkIkQ&6C80$*2e!j1#f`cM!qgAz%?~bhE zpoA%f*+mi?;oT;!IaPxh*D0_g#K70FarwrT8T^B``UG@rE&?+^r`($+C)zhv9fyv! z>hkUJ=1}=&7oQuDl#rI;u$Sh^ff>Jm(%DudG;Z?Fv51I30tEB9iEtH`r{OqevRSr-D{x?zUG`;kO!-4X~0Mzbp1n5OWqkR*z28CdH&l9mG6eO4*E zOY2IP?HE{hhw#^no-=>=eXn9Z|5pI@z$$ux`6%UJNvwYWPD_I-_!~I+I=$|biBb!n zt`!AjNR0g`-Sbup`BbRDcB7!-hAj~1BAAQ6R|&W={Kso;t?0w%Tj2u-ebP#)Np4j95ZB))N>33@ zw@&NHf+jpfDt&N2z&OCGAbs|PPqp%?hDW*H3WVznEaioqqOm5TZK(((SE*BU6vK5xYDMeU(GxX;_&>gFJ7@sNoufH zC>hqxQ!B|8gZ`fXRwFOm`;+ElY0t#;-#I3qIIEbgl}RJzQ>ciTC-gej@j=8dZW^!g z-_{%?T|WeMPha7@7@Ce5Z2Os-^|`0yprKS`hfuy#Jl1|_Ep7NbdxT{uzjXNH$z}d@ zNh?9MThzf3(zWa5R%R(WJJ4)yhK2(o#3wjj4Dx;f0f~^KT}fon$;0DFE#b zS_ntwP-tJ=JGah_XJ)9q8!GHI;y8Q&NwE77O z@N=XN3;1kS&^4UCb5a9SLF!W?eC|@;f2YSZ_X;cP)u0v=no&31z$LVgCa!(wFI(#R zSt&M8^Ob<~`W6~9tR9-J+zfeBhK`uxfTR^A?T(_f5~EzTq4L|>YMLS9KnOchb){Qy zqIlJtm(H+V`oF(vFS(=#I70Ups|j{hapIwflXvNBC)oc!c)MCHL_9ooA65Ceb=x2OMK0_t{p z`ukvU^yJ}9H#-o^_4x15-!;^FIaYPh7Ie#_yv;&OYIRYI=J27a+-^t=-%%GQ@YN`b zj))&bJyQ0^|K_-9?I*j*&TW}SD z;j`q~m^uy{VeqQ=1z2IPW!(^%Os~)bOR2XDA1}r4HRyJIJAIXtF!exws47pr(tZ5r z=qs^|LnjPhe@e@WtU0>}isxz0IaT)-;R8Ka-u7YNSCLqfaSM7>X`W-JC3v)5X_n(s zm7O*)EZ#6XwV#LV2;@GDKoD9MuW|v@B4#WkXkWq_>-X5KP~s~KouT3yGo|9c`olg% z5-ksfM|9^0%!tOc$|At#Gn(P}K&i>baxsIPP7FER&m$vB~6b0(QHP3=|;RD?;7 zUolWBynRfuzCJ`?v(6A8DK{4LgX{Azo_a*iItS4O?Z!!#l)SG6xW|sr2Qn{6M3-Mt zQQEkpbdDwPVHk-uN&v#KG%#6mbP`=1ceRGaI|UsBj}zCPOF#1rLD*j+Xg2x; zX@5OI7O4H7g($2@RIW(I^d4i=yOZt``qLuNY|F1^IV^wQ;ZF3WPb_z=4r8KhcCF*3 zdad{oO{$l8wWVvm?yao2(bc9LtVFco<6^|~Y=OX&xvFQ{*k4Q?Z;F$lH2XCsX_-{r zr4BnfRD}KX%c5Ud=4&r!*YKTf668_R%G>8z=2&Y-FL5Y2V)dTmAg-&mCG=2O@5$Pb z?x0i~KR4=DmYZi7wq|XsXd-g5`3W`Nx@y|sqmyoiFYhn5S*JQ!>t_pJ>lKIS@A^t=59*9J2bm@$(yD3&dYy6tgw^AEOUO#u zZa?)tjW9g~E%>=QpA&Xr0tBtdz28_4&&_B3=Zh{!JR6hJa^W|<7Z!+1Ox1E?;5#&E zqMI~7w|CsRGWx-xbjT0&U#Yvi)7*mJVtdbLrW}kfo>C(H=@G^{;tv9Ls! zsQH$d?Gt^-lc|;juAkZMuWY+pW$Lr@yGH!c{B(o*n6%odCcK*c3&R^Z4nWY!Mj|@U zDrUs(iqYywtytK1@zg6nt;oQ(6D%3#SuU4~avLDrolV00^ES;dg#`eM#-xz1y1v^< zMLlCTi-7kDsY@}0X zf!?D{2P8>dtvmgvJ9sR$M^Q!#Xxl^5*v9;Dnqvuf$~Yxskq4^rj{PDQ%YS(NB`tBo z{llg(>$pg$+QqYTb{Q6@cVhQ%X~`fyqzK+q0F@Fa?Hw+>r9IaN^X{ptdm*~U*F{Kg zEP=9`ODkxn$@dQA4pS=e`!!Z4glMx1G?yynvH2w2*lzTvM~qr8EmjBM&hBqCVJG?1 zRc2gAb<;(@KSy8_W_Zi{#0qV$KJ}Ib+Ca~-A?Wi0lI`~Upt$)xJR3Pck`Oo?aQ0g- zajf?ht%m)>H`-A{CT$76?Y#{2y8f|AFT{moK^l05%m=5*hFWEmbSt=beSxsbYMw6a zwm)OtYkOIIp9^)T8lp=tG`kcLZ}zvdw_pHb=sq9f@a3>GeX6JAZkRlS0RO@fg)7@LcpP;2;I&R_fqz0os(;7qUlxW*@@dGW zF>7r)p_Imts1!gYe3xTky}MWR5|+?o-RNHEK$dC_rrZYxd2(`zT7(H(u=F^~nLC=% z=w9WGb4a>wd1-(4<5@E|e(=G>3rv}5gId_)-#6v&LlBj57gEFMjl^|~VaRgFg>LA} zeIt95VIH-iE-lUA!zqWgO>d5CYK^?BuFq&huv!H~nW9mmG<$|_cSmpS_gPm@jfU?#+PF-gk99Ac zIpxu^G+sNORswSIqt1ZmjXO}kF%<`m;(+9Cee!KXDf64pXtChlOuXolwm1C~!7J3O z$W}MH$y**`W6gi8*$8WrAYMAb3yiyqbDrb*YxAMaX ztA0N3R-rVh_z{{m*z<0$y7OZ0k1mYJH!_fZUta#0YWF*jI0=+_bQ_4^TPMw7&L7Jy z(b8wSh669a5%EWe;l={;C7%dgRP@bC9E2>loXDd{O2-9SkH9Js>i2W$mN02O`Mnr% zTnuV7BR{V_&MxsK`fDIFC*jf0OsrZfT6!3w=eAT(3HOlP#V@)hw1Ul*GjC}ysJ@F7 zbUG_+4?SN!qDBT!Y!I5x(!wWGURTq>%xulT=Zc}L$>O67L+F(ieek8Ub*^g(kID4= z(pAqShX+55*{r67MSZ)97)FUaC z{;&tdcGMJLk8gCkAE$~#y83!m@*1{&^NH+O`>Iw5VgKB@!|rtKP4~)l*p&9tDA&GI zd%dxW8z|mZ_ROiw{R~Y2VLxGP+3V-sdS&JcRR>8gi1N53_x{gBdot_a zEyd^LCZ3Kpo}svxo{i9XZ<*dA48uvd)gLM8Qe%_1496h7+pu|IWPa?4^TXF+12x)x zveF3E-+VU@OUPQfIy`K9qXx2|(YI7nJNa5%6B41L^A)3SzxwBU7p`9+; zvdapH1^gaAx%V-16Ke%ak_oV|Om(rK1I9*q73!m`-TGfg_+hc8r`X*zLS-tFT;U3*iiF{ibF=Yv%VQF^DMo7dsUl+%<+3ai!+@wo~|*GpFdQi>me zOLM)#F`BCOPj^kfszsOF+i005ddL2UR&3rLl#w7mS|D*dv(v-Vwxnx;AxvLbZc!&# zt*McSrg}`-fpjFgY(h%@+nONN4*W5Gt)b?}8_iFtI2udnmG;Ug=*n(VZhv>VjAnq; zA|B^k;#sR53Fod2KZK_T@~4S)$5bL;&=CVuXyTB!Zl;NJes?gBX%syt;TngbVCTn% ztMxvb@QAVMHo*dQck5`l9M$5|5c<+UtgrTFjtn=$lNw5zPbNIj*wURmqo=1Fl9D@o z<>?U&pPoadII2mJ)| z@kn$@e?%4T;Jf1mw>;PaByz*yl!yU41yS;DIynU+#m}gnPq?m{#+RoY%+&JjKc)_d z!gYua9K46pm$`k;`I_;zWe9+ku4v2N*;cMADRA@xxm$D*yG%`c8+X}2Wh$aAyXv;_ zl}%XIW6F9v{cCr)2EXrge}VTU3;yoWo#!z9^85#_oL{1YEHI|}V zvD=R|ehTk%z*xwPd@)XXiMV53k>Modo7mKF+koOQR?&`IR@WpmBS(q7BLqq&%&@alO2Y& zApJYotm5m|v?gvYH9UDBMDV$M%+Va%Pf9|H;$$-#r&KLlTbZWhNDd$gwX< z1d{Lf+2KIzQla@xI-8-~i#K$9Z9wcu?QqfDBgN%=Gy4V@aL*ml<*W)~u6%y=rW9s( zG|eRak6`h@`C0}Pq&Vr&CoMa}a%C{Lg2roDs1{7toy^#Zuf4tVXW8dtz5ZpGAD)Rb zU+=V@dos*%t6S3f=l2&YyCM(e6o76LK^bLvmqVPsNw$ zt)qmq-zRV+{&RCQwZ2WD1BCkd$pRv6O)c&&RS}0YV#{&1&@@DQz3Sl;DqrG2#t2Od z7(T^_oky`q{3`&Uxed{Li1bYIq?|%Q)8+*6qpas51i(ozK?1uvWt)8k&IyL2>Cl%g zjkusaF8QZF@N}TL3Zkn?+T zmhjNiNiU{{6kpNQ=YUV4*u)M&vdA{&e{gc?I73!44IZuJ7ZV%G+Kx^AbE|jLGonjv zNNVH7f9W48o`V|C{9lKY_(m`r??QdhMa5xJ&`NVn3HHSkrsLUgb z`_%|AoY;N2b@PD6)SZk5KW}?lu`G5D+Ty+fzPbbOt4c8jA5>GKX$QGpMp?BQQ%DFm z*s%hIkq0#(wqkDrmyr^_M>_wWruo5MEtpe;tk{x=v~>8oM0x>OgNy2NJg-Rm_Q~nf z;0Jnn(y%b~Ga2j_fXQ!bc|X9A4xFdw8e4`@kQ0t|2^ ziZorA%qt#X%R$qNqcGcv=`Rm&?i+gJ5QNwv=O4Zsn*Afr-cS8P`Bb(p5R;UtW)PVY)S8@n29gh_>riwZ;_?|thkGoJ~V{egm*(N|8YY5>C8Odh0Cjg yl!n=s2L@XRt~DkQ+WBzZJt&4Jm&bvR9jtN?X4;?`2PTyDXXgh diff --git a/src/static/index.ts b/src/static/index.ts index 367d4a0f..3ef3bf3e 100644 --- a/src/static/index.ts +++ b/src/static/index.ts @@ -3,15 +3,17 @@ import ngcp_calpoly from './images/logo/ngcp_calpoly.png'; import ngcp_pomona from './images/logo/ngcp_pomona.png'; +import poi_invalid from './images/markers/poi/invalid.png'; +import poi_unknown from './images/markers/poi/unknown.png'; +import poi_valid from './images/markers/poi/valid.png'; + import plane_red from './images/markers/vehicles/plane_red.png'; import plane from './images/markers/vehicles/plane.png'; import rover_red from './images/markers/vehicles/rover_red.png'; import rover from './images/markers/vehicles/rover.png'; import draggable_selector from './images/markers/draggable_selector.png'; -import poi_fp from './images/markers/poi_fp.png'; -import poi_unknwn from './images/markers/poi_unkwn.png'; -import poi_vld from './images/markers/poi_vld.png'; + import fullmoon_hover from './images/other/fullmoon_hover.png'; import fullmoon from './images/other/fullmoon.png'; @@ -22,7 +24,6 @@ import moon from './images/other/moon.png'; import arrow from './images/arrow.png'; import icon from './images/icon.png'; -import pin from './images/pin.png'; import configStatic from './config.json'; import { startLocation as startLocationString, locations as locationsObject } from './location.json'; @@ -150,13 +151,14 @@ export interface RecursiveImageSignature { export const imageConfig: RecursiveImageSignature = { arrow, icon, - pin, logo: { ngcp_calpoly, ngcp_pomona }, markers: { draggable_selector, - poi_fp, - poi_unknwn, - poi_vld, + poi: { + invalid: poi_invalid, + unknown: poi_unknown, + valid: poi_valid, + }, vehicles: { plane_red, plane, diff --git a/src/util/ipc.ts b/src/util/ipc.ts index cffdbbe4..6cdd1d96 100644 --- a/src/util/ipc.ts +++ b/src/util/ipc.ts @@ -440,6 +440,16 @@ function postUpdateOptions( ipcRenderer.send('post', 'updateOptions', missionName, option, value); } +/** + * Post "updatePOIs" notification. + * + * Files that take this notification: + * - renderer/mainWindow/map/MapContainer + */ +function postUpdatePOIs(...pois: { location: Location; type: 'valid' | 'invalid' | 'unknown' }[]): void { + ipcRenderer.send('post', 'updatePOIs', ...pois); +} + /** * Post "updateVehicles" notification. * @@ -507,6 +517,7 @@ export default { postUpdateInformation, postUpdateMapLocation, postUpdateOptions, + postUpdatePOIs, postUpdateVehicles, postUpdateWaypoints, }; From 14c5e55908854960b9e5ab4e42d8517d9db66234 Mon Sep 17 00:00:00 2001 From: Paul Matthew Barrameda Date: Wed, 1 May 2019 00:28:41 -0700 Subject: [PATCH 138/152] Added cached tile layer successfully --- package.json | 3 +- src/renderer/App.tsx | 1 + .../mainWindow/map/CachedTileLayer.ts | 23 ------- src/renderer/mainWindow/map/MapContainer.tsx | 57 +++++++++++++++--- .../mainWindow/map/control/Control.tsx | 6 +- .../map/control/GeolocationControl.tsx | 4 +- .../mainWindow/map/control/ThemeControl.tsx | 2 +- src/renderer/mainWindow/map/map.css | 45 +++++--------- src/static/images/other/fullmoon.png | Bin 2497 -> 0 bytes src/static/images/other/fullmoon_hover.png | Bin 2378 -> 0 bytes src/static/images/other/geolocation.png | Bin 1188 -> 0 bytes src/static/images/other/geolocation_hover.png | Bin 1140 -> 0 bytes src/static/images/other/moon.png | Bin 2716 -> 0 bytes src/static/images/other/moon_hover.png | Bin 1499 -> 0 bytes src/static/index.ts | 16 ----- 15 files changed, 72 insertions(+), 85 deletions(-) delete mode 100644 src/renderer/mainWindow/map/CachedTileLayer.ts delete mode 100644 src/static/images/other/fullmoon.png delete mode 100644 src/static/images/other/fullmoon_hover.png delete mode 100644 src/static/images/other/geolocation.png delete mode 100644 src/static/images/other/geolocation_hover.png delete mode 100644 src/static/images/other/moon.png delete mode 100644 src/static/images/other/moon_hover.png diff --git a/package.json b/package.json index 1d8e1916..bd1b6351 100644 --- a/package.json +++ b/package.json @@ -16,8 +16,9 @@ "build": "electron-webpack && electron-builder" }, "dependencies": { - "@yaga/leaflet-cached-tile-layer": "^1.0.0", + "@fortawesome/fontawesome-free": "^5.8.1", "leaflet": "^1.4.0", + "leaflet.offline": "^1.0.4", "moment": "^2.24.0", "msgpack-lite": "^0.1.26", "pouchdb": "^7.0.0", diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index 2aa128f1..17f91e0a 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -6,6 +6,7 @@ import { ThemeProps } from '../types/componentStyle'; import MainWindow from './mainWindow/MainWindow'; import MissionWindow from './missionWindow/MissionWindow'; +import '@fortawesome/fontawesome-free/css/all.css'; import 'leaflet/dist/leaflet.css'; import 'react-virtualized/styles.css'; import 'rc-slider/assets/index.css'; diff --git a/src/renderer/mainWindow/map/CachedTileLayer.ts b/src/renderer/mainWindow/map/CachedTileLayer.ts deleted file mode 100644 index befb2999..00000000 --- a/src/renderer/mainWindow/map/CachedTileLayer.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* - * import { CachedTileLayer as LeafletCachedTileLayer } from '@yaga/leaflet-cached-tile-layer' - * import { GridLayer, GridLayerProps, withLeaflet } from 'react-leaflet'; - * - * type LeafletElement = LeafletCachedTileLayer; - * type Props = { url: string } & GridLayerProps - * - * class CachedTileLayer extends GridLayer { - * public createLeafletElement(props: Props): LeafletElement { - * return new CachedTileLayer(props.url, this.getOptions(props)); - * } - * - * public updateLafletElement(fromProps: Props, toProps: Props): void { - * super.updateLeafletElement(fromProps, toProps); - * - * if(toProps.url !== fromProps.url) { - * this.leafletElement.setUrl(toProps.url); - * } - * } - * } - * - * export default withLeaflet(CachedTileLayer); - */ diff --git a/src/renderer/mainWindow/map/MapContainer.tsx b/src/renderer/mainWindow/map/MapContainer.tsx index 31b7233f..288b9cc8 100644 --- a/src/renderer/mainWindow/map/MapContainer.tsx +++ b/src/renderer/mainWindow/map/MapContainer.tsx @@ -1,12 +1,9 @@ import { Event, ipcRenderer } from 'electron'; import fs from 'fs'; import Leaflet from 'leaflet'; +import 'leaflet.offline'; import React, { Component, Fragment, ReactNode } from 'react'; -import { - Map, - TileLayer, - Viewport, -} from 'react-leaflet'; +import { Map, Viewport } from 'react-leaflet'; import { Location, locationConfig } from '../../../static/index'; @@ -22,7 +19,6 @@ import ThemeControl from './control/ThemeControl'; import BoundingBox from './shape/BoundingBox'; -// import CachedTileLayer from './CachedTileLayer'; import POIMarker, { POIMarkerProps } from './marker/POIMarker'; import VehicleMarker from './marker/VehicleMarker'; import WaypointMarker from './marker/WaypointMarker'; @@ -121,6 +117,7 @@ export default class MapContainer extends Component { this.onlocationfound = this.onlocationfound.bind(this); this.onViewportChanged = this.onViewportChanged.bind(this); + this.setTileLayer = this.setTileLayer.bind(this); this.setMapToUserLocation = this.setMapToUserLocation.bind(this); this.loadConfig = this.loadConfig.bind(this); this.saveConfig = this.saveConfig.bind(this); @@ -147,6 +144,8 @@ export default class MapContainer extends Component { ipcRenderer.on('updateBoundingBoxes', (_: Event, updateMap: boolean, ...boundingBoxes: { name: string; color?: string; bounds: BoundingBoxBounds}[]): void => this.updateBoundingBoxes(updateMap, ...boundingBoxes)); ipcRenderer.on('updatePOIs', (_: Event, ...pois: POIMarkerProps[]): void => this.updatePOIs(...pois)); + + this.setTileLayer(); } /** @@ -164,14 +163,53 @@ export default class MapContainer extends Component { } /** - * Centers map to user location using geolocation (if possible). + * Create tile layer for map. Called after component is mount. */ - private setMapToUserLocation(): void { + /* eslint-disable no-underscore-dangle, @typescript-eslint/no-explicit-any */ + private setTileLayer(): void { + /* + * Add cached tile layer to the map. Solution to the caching issue as there is no package that + * supports react-leaflet, and creating a cached tile layer with react-leaflet and the package + * above is extremely difficult. + */ const map = this.ref.current; if (map) { - map.leafletElement.locate(); + // @ts-ignore + const cachedTileLayer = Leaflet.tileLayer.offline(mapOptions.url, mapOptions) + .addTo(map.leafletElement); + + // @ts-ignore + Leaflet.control.savetiles(cachedTileLayer, { + saveWhatYouSee: true, + saveText: '', + rmText: '', + }).addTo(map.leafletElement); + + cachedTileLayer.on('savestart', (): void => ipc.postLogMessages({ + type: 'progress', + message: 'Saving tiles to cache', + })); + + cachedTileLayer.on('loadend', (): void => ipc.postLogMessages({ + type: 'success', + message: 'Saved all tiles to cache', + })); + + cachedTileLayer.on('tilesremoved', (): void => ipc.postLogMessages({ + type: 'success', + message: 'Removed all tiles from cache', + })); } } + /* eslint-enable */ + + /** + * Centers map to user location using geolocation (if possible). + */ + private setMapToUserLocation(): void { + const map = this.ref.current; + if (map) map.leafletElement.locate(); + } /** * Updates map location. @@ -399,7 +437,6 @@ export default class MapContainer extends Component { > - {boundingBoxRectangles} {vehicleMarkers} {waypointMarkers} diff --git a/src/renderer/mainWindow/map/control/Control.tsx b/src/renderer/mainWindow/map/control/Control.tsx index 82e909ad..895952fb 100644 --- a/src/renderer/mainWindow/map/control/Control.tsx +++ b/src/renderer/mainWindow/map/control/Control.tsx @@ -36,13 +36,15 @@ export default class Control extends Component { return ( + > + + ); } diff --git a/src/renderer/mainWindow/map/control/GeolocationControl.tsx b/src/renderer/mainWindow/map/control/GeolocationControl.tsx index 7084b563..85af27fe 100644 --- a/src/renderer/mainWindow/map/control/GeolocationControl.tsx +++ b/src/renderer/mainWindow/map/control/GeolocationControl.tsx @@ -15,9 +15,9 @@ export default class GeolocationControl extends Component { public render(): ReactNode { return ( ); diff --git a/src/renderer/mainWindow/map/control/ThemeControl.tsx b/src/renderer/mainWindow/map/control/ThemeControl.tsx index d7be1a9a..eb4b0286 100644 --- a/src/renderer/mainWindow/map/control/ThemeControl.tsx +++ b/src/renderer/mainWindow/map/control/ThemeControl.tsx @@ -19,7 +19,7 @@ export default class ThemeControl extends Component { return ( 8U~$sas{g08-iGp6%H`Hs^dN z-{be)dw;+C-Fv1yF=0iZf0#dy#|zY`mC4|Lu4nj8=J6&~`Wy%X3qPAW!@=VPO!JIM zy!Wa@dA!NNOiDVJu6>1|Sc`zvvpQPfwAcWe$BS9!w2{;%nuBz-k+I78cTb)F@kvZ9IYrNGEBV0Mt|C zI9rb0JQAm#LTEE>p{<+)V6kzdHWSOS4iozyOphN=Z~(HP)sFjkZY&ndxC;lT$_0$a zw0kbvk&h*fq5tVoZ3az5tK!bT|UG|ACQFywrYL>Y{V zU<^yaBm{;MILM}yK+#dCmen(c{O3VMIDz65px_1RNsj!lU_C_`Si6M;q!I?=QH8igX!&KejqIB+DdSPp4a3JE5YNMNC0l$lmbXsiy7v{JN2 zDdz*O0*28OGO48N` zXprI}3TP-3fv8AbG)!U`3gak-(FQnddN_wX0_aT(kE7cTI| zLf{wH`PJ4GA5PtJ0+oak{x>e^iUhi)ZdU zb&S3J-50wa4*tmAJ}q)QxwFi8wYsXXN>_ibvNo@^EQ#(}d8*~b2ITNR5-#}fimYya ztL|cfOn9ebVNSdF*Qz$38-|BdQ#Zac&D{PIwci{{mvrvc56@}3tlPV#&1YWMse7wd zewch8!IP+%-^sgfNWLk$=KAP;ofKw88snW)4h_w`n|p)`J3A|E%ApzO zXV#~5T#{8c9$fmD{GgZ}tvjErJR1Iaakj#>HsbZ23U$b}(9*S!9)1=3AovP2CF|y+ z^Jzo%rqgq}zE(zN?|D%1ZglC*cvW-F-BrI29}0iDq@XkD`X}a&efoFK#74Z4`mtZ0 zyDfOv*GChYYd&aR`l=;m^O7moLZ@`!sjMcE&cJsFKaWA z)UaKB@Z*OHAklnZE)3E zF1maWcUF6@UuO6Fapwlx{q}8M|j{?^Ba$f$Sw)<^oq>jvF$|3G``G2QJWmk;#J4`>VUY6-p!c?AvR|Ei8Clzv~; zG`RI-{enO3iJD(=_|HsX;2$z8cfT<`{N!g_ABQ`;?q8Xme3Ph%FEpvUKHoCu+oliP z4NE#>PW(M8&XCuW8&neP+OudzV@}NRnwr^Z_x2BM{Bgw(F^yWc{GS`J;fC%RKNTnW zM)X|V>)#pawENYTbUTWwitbE}S68fUIy8K2ue5(%?`m!0@*VA)-Ew_~ue+t6>!mtw zMI4wO_v)q6HA~T|@2`h-sfyq;-g~N#&Gqu0_vUvo9}j++iY5IO?>RbD)mwdw^`4fU rK`eR{v#mqf%RUsAHonlR@ojwVY>8`Dt0b((^O2=dB`6z~XKniz{S~1O diff --git a/src/static/images/other/fullmoon_hover.png b/src/static/images/other/fullmoon_hover.png deleted file mode 100644 index 9f7cb171df26df77bf1df22f76da1ccba060a028..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2378 zcmcgueQ?v}8CL?!nn2TF%vaVHp@g&)WJ$4P$7+-;u}xyA(=-?gT-YK@vMpj;o+M(2 zW+j28e5M5GAZyYBP22T@WZ5{N8=u{pv}KH=2MJz}LTFi8uVKx)l+RN3$__5+v0HC{ z%y;kk{rJ7l^Lu{J`#h(${o!lrV38UsU8F$J znDN?3f}U@i3_&RwqHCeL(D@k6NdW`POK!ms34{O|g7OO^A(pEaRM;(e#UO)xvHvgv zi#&roNjY(6$SU~6(&e%+Z+V%ETVBmsc%-lZ&X3T*KtNDgI1=y&6*|HoF}pOFYugwC z#~^AogA{2F;f2m}*eb~aOc_ilN02ydwipN#PEy1yI2R|(7*1ih3B?H-&!ck*c=(3^ zZ!+(pD{QleeSsB&_*6ASV^}yGHiV4^N%mrd#bUwmTr4*i1qf8B4XSJe4Jui;E!YHw zlf{rKN`5)4KlBW!2{kmnM( z&@$N{WF;1>cyP*nhyNEi+IBvn#;(tj|WI3D2uWWni7__!;VKp^2lQSCJ#;$iLX zidI~;Apxrp6ls~v3HBO5GYf$=1?b0R5v*8EKL#N*Ekd+a7FboVfx#dki53($q6Fb0 zOf-Sh6v(ET#_>4RDemW=MkufA~|4%$@r3ubZyUAIhYTH!`WqziQ2FI>DG0{GA$| zL%#Y*)s_X^XX04hz85YY>?#`5`AFr*wf>g%JIk9sn>Xlhy}G1_>|JxAG{hZ%;C*57|I?XC9aT^E{%>Sr{*14&A6<@$^%{rO$fSL3rU zTcU|+3sL5{^U$_18rDvA34Adp>zwzeYvYyF%X1zCVj;#4j)YY2la3!72vN@X99kQ>vo_(Py zdz~;iLB~B;(Y!^!Cq)`Uk6Z+q%p8Du34u4IT%-DPRX@|jP`$bLm z;O864o3}4Ytt-_VGYWq6jQ&FRK=M{y_W@VVZnmi4b>@8j<&3tK+y7|3c4=Gk;hANd zpUhm5w7N|?8VodUY<{Tq{h@Pd`DeyX(|viPX3wfygg^D%#eeiZezNlFJx*QWsh6WA zJ?)kB=VNBRzHRjf@}GACi*<^P*qhINv!pdgW&H z)4`+C31-gkYvw-NF@NddPdwkOYWZu`u^w*K#=fd8_OT5o-=3UBrlvJEEIEp8YWODa zOaVUQg b-ZMC9(MKnv?WdRx+SiW5US{hou3GtTSr=X_ diff --git a/src/static/images/other/geolocation.png b/src/static/images/other/geolocation.png deleted file mode 100644 index 04efd5616f6acc87f0171a8390b339f79f98b5c8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1188 zcmV;V1Y7%wP)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D1UX4WK~z{r?Uq|e zRY4Sn_c_N4B9%Urgi!=#BHlF!x))Sb5S1DfL<(AvJ%|Pgy+%D45?I6$GI|KoE(8_b zZ&n25T|zCQ%z_F`f)vW5{r~K>Put_!7mc6?KNx1M*)wbYy=L~z)~0a4BlVip1Dsw5_le z)X~wAiw*i5rVnKZ_D1u;m!F>>6&Dw`0+TMJj|pE73=BMOXlVGUIsiF1Z@{Fe0PH1c zX=yLj!!X!>_SldoP*DsgD=~@j6c-nl#>dBhf|xt-PqENu8=tWcJ8}%i(gr?>X^CZq zknUxepi$gg*pwT@rkJyMwp;wLK*YJ)+S)xd>EolgiE2GE=U7k> zH=j|EUJh8mcBJb}y!zUfAlgiQ6~ zv!BPdP%M`WVjhW^gg~h@BrvZ6x73)#Fh&w>Q|xhA3c_2|i%AYjtwl#iSK`@IaV=pV z_7#@|V_=Cch&c|+B||`OdwcsUm}I5=2^K6~M+tHlI2u0J+5w!FH! zdWa4DtW951H_oIKWNAeFF<1&B2($@-u8I3)czAeePEO7n@wJkYlDP=H1;UJ_=z)FJ z@Avnr4xp*2X+cCp1T#8|K3;4yUhF#UaM%hWSbIOJV%^g|W5PVEdngpS4==(@ppUVB zjAd3WaXg3c%gUSE$Wj6~Bt@dkm$q^68T+s=TqOovL#7UhZHZ%sNM~nf6qf5WCMc99 z!t3M(3jddkJ8BNf5|qU#!Pv-59mMQQO-&8{Ct2Z2kOAI04;BB7!=52wc!yGfbL^7t zIx`_bgxSJR);5?s@#ZU$wdwj39khLyr8;8C95=NcoEY?rOF{jABZmBypnt)CCujPx#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D1PMt*K~z{r?U!AM zRaF$n&%JjNLJVJ0qELh!2bu3t5Y#k74HKfY5fr2XOHiLQNa$7UAs+;l@FAlIVMZ+i z4K>TJ1VQI}kkLfUFG#^4kO7@L~+>&d!4<{@y1CZ zXYH8j>grmY&*x7|A08flx2dV=jJq&K^47jIj0cp)m};oIySs^I4YjjmWMpJE`b2lZ z^B(1LxhvGtpXhCkjg4J?cXlcYsI#-Pp-?F7LVeHgN#&1XvADIlxw*&eR5AzD)6=t! zW+zhRI(+^^I*_~0^1L^ZIml#Z`3xc38yXtE^UY*31;i5VVJSA5Db8c>92gil+1lD# zv_40BdwZd#re+ySxgB$%-|KnUds)KGX2y~l5W#${hnqQw2V|m)YsQHhXTEbT0jQTSCnjg6Oeb1na=XOzmapuV`qVFoWG$es3Q8jy1Kf{ z?qV4dUwaC2H;sbP?7oNuqlqZ}_mJO_eJ0Y?r)zGq8l$n&p9fn|Xqw zeBX49`4L&~tayGxK1D`k@KPM7SuULb7qWDJNiQWfLj<$VRI$Tau@y-RM(7^h8xZ;^ z@%;=*OBU=kck1iwgV{_%zVX#e=w6NS%*P$+5WA0sOQzy8)uZrKH>!sm>1`*yYup9T zTa6s@)y#Z?9F{^Ew2_@wR4wCDUXpOj2fUE3Dv&qu1+0PIgkigLgI67+P{h^_u`7JFi*Uc8H zu@4cq6mSjs6}jas=;-KpyQ->6*_tAoiLGQ}$E24dF(AR(YqlGF%lntit&Tf~O0n}$ zo8na2jNQ*zWi^)6eTYw1o!mi|rm7$nBr?PLMxkrE$BNel14qf!7Sm%%&5-Et@1MYO zea{0vvo4|S{}Q6_rQnQu3z>(^MMB?X<^aKLtgWpLzmwv~6i@)O)>)=vF1v@6;U=<; zee8&Drc+TsLfPUY>tocl%zP19d(FS%L*M_lR0mn^pR;5oO2q+1;o&*5_7MTN3}CM! z8^2(?kFv3lTGUDC!SK*b1@!QFf)URZ(2L-u3uv5VJnt!?7k6_841{d}0000{2E#TeP!dZ2&oM143kJh1&+?QVEm#wQktBmL(ZRIL7==aC84L?^ zb$A37Aq_?_y_SP2^m3eI)DkqB!C35LBv33CrLRY%)nlp+>fjbRX3q!AZ=ZCMdOD z$24g~<@$I^#G*YNqClyY|Iw=>N8&_B3^3gQ5C=4o8fL2{#yl8@G@`^fpaL7i5%C7i zke><+z%{rQ*HI*mg~pT;Dm|qqRr=phJ$60L0G$P?bj-$kebH*iOpug+0?l|NcJD=# z;YkDzgyN(=-hko$3ACH;ET$;}$O?m+mQYjtL@dY@3jz@vaFoI&^b)b?BnsIej}1ZL zkN|-|gimMF69K_ts8p{|E0f*_YSE{rWseES4GcO0|Z*AVUNEm_hzx0VEU%*j&yqGpQ5_(vcLZ!|))9 zh(&YdsMQL@6ISqG7!t6BLLSD3F<8Jxg#s@&7v%EzD9#hYUhoiI`n^Pt#hcPR`d&f5 zm&2HX%g4RgpwbIw!+hM6El2qZHjaaG*pm;_eIOWpPsf|?NL2HO^_cq9R3Av7nxyoG zq>)Yv!{bJF8Z~pMBoGuE&MJu};0o5rq53`if1m%uN)uH$T`1$Lg6oO!c%!KvhLL(D zm53T}-xxYpqtik*+6+vwm^ojHVrreKIXp&v!&Im)2B)VKzykg-nctVp-zGDzZvTH| z0Ml?9P7?6n`5U_X)gYs}4;|6d(X<^K%d{{y*l`^lCj&jUfBF3RrwqmiOM)c6;Sm{U z@2x$x&wr|APh0DQOU@6UDf80&Wk)<#&v&y*TXH6g>9ukNS~R6|cJdBguCK&;^4-k& zX)}+`Jf3z=vXa{J@(U~LuWr|U;12f&CKPAwkGnPax~sfzrBg~udCc9C-oyOZ!Sb)A zEAG48(I!k%eXm^PQE@3^@rzvEfljB#yOQqhj+V}M;A@%(%4>*(rYotcAEt5{L8S-4 zNr5M2nv*fDVDQ<(OCe3IMIgJrvgVWgZnN7xa>J_{@WfwwR)t1Ihjnh-)aPVw*KYsI zn(5Y&Z~i53$F0|H-gP#?c3!Xi`^s&~;6wGHUG-u76ln=zBRHP@gXl)+g9h%oO3eOi z=O1cT{5-9h-?Nv+qu^zGTi>h zi}lE{Tt6#uwK6h8+F{gKRJhOl?&H$n;BdOYQpx)uMzR|7yj$?k)v}Vgg zR}R+iTu$W^aK0Id0CCM6YCZzDlSeAQ)MWZc0M!;4?OtHQWu28 zmaTVK>CoI}bnwZ3UJ8jMp>M9k75dY) zb6y47n{iD@_xo|vA?%Pj7-N(%uv*3-cH8B@=y$`)uOYlJdjwlID9af3oX8arD6Mmk;D@a8_c`BIev9Zd+5rDyOzx z&v+adydx<=ayhs1#l!vNnSTVB{Ty<6*0X2nfHl+PM5$t#hqgOLk&;_xZST{;1tqnb*wsGSFDfjVT`<-sg9C$Z*=l!R`2`(08 ktIkieOYgWC;%7BzdA-i2*KUm>+VuSx|k1|%Oc%$NbB7>k44ofy`glX(f`a29w(7Bet# z3xhBt!>l_)Q7iwV z%v7MwAoJ}EZNMr~#Gv-r=z}araty?$U{Rn~?YM08;lXCd<#R|ql7WFm+SA1`B!YA6 z^yrS>K#}8j-A@Pzdl|>YoLVa&6c{wG=S;VOlU`ul#|2M(HCLXT{i#h!v$AkD`_qff z-cPQ%tlKHP^Y{OA+yAFuvn)PmQ9S48_5ab;>-T<(dL8}y)7;D3{lC6gzOt6Ne%0UQ zRpnRDsx&q#ot(n-_bORggSp3Xbr^Pm2l`{lT5uh;g* zx7w5Z(t6h zzbbgL=XyVWuX23J$64=X!_${A?eSx@jAUc1opXMrZ);vfz@^X4t9C8BeI{kWcG+(DWmA zNs$i!hfRCV?%L@!N#wDdxARr*NgFqx%-Y<3V5^UaO{{NT<^>J2r%`k5g0@!)3M(}n z+{nJpFUR`k!63aoHeT`SU`)tw1D{JE_Bp0W=C2x~nG@wLB4>=+CV_dC8m|vs4@7xdJbpJH;khu*CDj433z+Yp(w+c^YB$ z=Am-ot_%b5r0h%I)xFNj$lU+S^i=h0!M~6%$G7b8^6A+1`IWV%-bLm`(*w>hVz3@_5YjusHm`Uhf9uVe@Be=jarePn;9N? zKeIk;^1MXlI$e6+{GQ9NviQQ?mj@?m_vCh; zuop0^D-6o{mA|O|->-xpM@_GE&UmQ1@@-DC(a-BIJ3S3syUV8S2)d)7C)VHft=c_T z`Peo+_1Y>6-FY1)K@uO$Qe`uGj=XU{YvXZd>cQpaC7QSYs7UF>Z@y)yIqiJ%4Z#}+ z3$vp7c~=PEJaJKcpW~P5nWt=WBj$v(zuDfkPw14-`IYT2c6vUkD>8~%qwVrQB2wRW zx3;ZtUB~4klOKg#tXOu;_2a4&=O>mcJ-9YcLr-LS=VRB$+oV>zep_B!y(a0TOY`OD z+6VJ$|qi_1%GCly{U^e?Q86WZyV^x(#_q_>awHpZDO(tpjBQtmCr zyD@q5rWUtmso*1S6BpdrBR;>Q+*K<%VCsA&&+8(7I`=L-WlC^Xn!2y#oPU2$x03LL ipK6B-Z(0P}9*a*u`1O$Pi;!!e64%q!&t;ucLK6TBN0z++ diff --git a/src/static/index.ts b/src/static/index.ts index 3ef3bf3e..1f6c6f00 100644 --- a/src/static/index.ts +++ b/src/static/index.ts @@ -14,14 +14,6 @@ import rover from './images/markers/vehicles/rover.png'; import draggable_selector from './images/markers/draggable_selector.png'; - -import fullmoon_hover from './images/other/fullmoon_hover.png'; -import fullmoon from './images/other/fullmoon.png'; -import geolocation_img_hover from './images/other/geolocation_hover.png'; -import geolocation_img from './images/other/geolocation.png'; -import moon_hover from './images/other/moon_hover.png'; -import moon from './images/other/moon.png'; - import arrow from './images/arrow.png'; import icon from './images/icon.png'; @@ -166,14 +158,6 @@ export const imageConfig: RecursiveImageSignature = { rover, }, }, - other: { - fullmoon_hover, - fullmoon, - geolocation_hover: geolocation_img_hover, - geolocation: geolocation_img, - moon_hover, - moon, - }, }; export default { From d7989f66fb3cfa74599d3815ca3ed4451a1032e0 Mon Sep 17 00:00:00 2001 From: Paul Matthew Barrameda Date: Wed, 1 May 2019 00:34:34 -0700 Subject: [PATCH 139/152] Changed isNaN to isInteger/isFinite Co-Authored-By: Maxence Weyrich --- src/types/message.ts | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/types/message.ts b/src/types/message.ts index c7951dea..fddfcc95 100644 --- a/src/types/message.ts +++ b/src/types/message.ts @@ -148,17 +148,17 @@ export interface UpdateMessage extends MessageBase { */ function isUpdateMessage(message: Message): boolean { const mandatoryCheck = message.type === 'update' - && !Number.isNaN(message.lat) - && !Number.isNaN(message.lng) + && Number.isFinite(message.lat) + && Number.isFinite(message.lng) && isVehicleStatus(message.status); if (!mandatoryCheck) return false; const updateMessage = message as UpdateMessage; - if (updateMessage.alt && Number.isNaN(updateMessage.alt)) return false; - if (updateMessage.heading && Number.isNaN(updateMessage.heading)) return false; - if (updateMessage.battery && Number.isNaN(updateMessage.battery)) return false; + if (updateMessage.alt && !Number.isFinite(updateMessage.alt)) return false; + if (updateMessage.heading && !Number.isFinite(updateMessage.heading)) return false; + if (updateMessage.battery && !Number.isFinite(updateMessage.battery)) return false; return true; } @@ -182,8 +182,8 @@ export interface POIMessage extends MessageBase { */ function isPOIMessage(message: Message): boolean { return message.type === 'poi' - && !Number.isNaN(message.lat) - && !Number.isNaN(message.lng); + && Number.isFinite(message.lat) + && Number.isFinite(message.lng); } export interface CompleteMessage extends MessageBase { @@ -233,7 +233,7 @@ export interface AcknowledgementMessage extends MessageBase { */ export function isAcknowledgementMessage(message: Message): boolean { return message.type === 'ack' - && !Number.isNaN(message.ackid); + && Number.isInteger(message.ackid); } export interface BadMessage extends MessageBase { @@ -304,14 +304,14 @@ export function isJSONMessage(message: { [key: string]: any }): boolean { if (!isMessage(message)) return false; // Check if id is a number. - const idCheck = message.id && typeof message.id !== 'boolean' && !Number.isNaN(message.id); + const idCheck = message.id && typeof message.id !== 'boolean' && Number.isInteger(message.id); // Check if tid is a number and is valid. - const tidCheck = !Number.isNaN(message.tid) + const tidCheck = Number.isInteger(message.tid) && vehicleConfig.vehicleInfos[message.tid] !== undefined; // Check if sid is a number and is valid. - const sidCheck = !Number.isNaN(message.sid) + const sidCheck = Number.isInteger(message.sid) && vehicleConfig.vehicleInfos[message.sid] !== undefined; return idCheck && tidCheck && sidCheck; From 1d440015ef34cd298ee1f76f977a3235e760094a Mon Sep 17 00:00:00 2001 From: Paul Matthew Barrameda Date: Wed, 1 May 2019 01:17:10 -0700 Subject: [PATCH 140/152] Version bump --- package.json | 11 ++++------- src/renderer/mainWindow/log/LogContainer.tsx | 4 ++-- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/package.json b/package.json index bd1b6351..507ff71e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "GCS", - "version": "0.7.2", + "version": "0.7.3", "license": "MIT", "author": "Northrop Grumman Collaboration Project", "description": "Ground Control Station for autonomous vehicle platforms in NGCP", @@ -21,7 +21,6 @@ "leaflet.offline": "^1.0.4", "moment": "^2.24.0", "msgpack-lite": "^0.1.26", - "pouchdb": "^7.0.0", "prop-types": "^15.7.2", "rc-slider": "^8.6.9", "react": "^16.8.6", @@ -29,7 +28,7 @@ "react-leaflet": "^2.2.1", "react-leaflet-control": "^2.1.1", "react-virtualized": "^9.21.0", - "serialport": "^7.1.4", + "serialport": "^7.1.5", "source-map-support": "^0.5.12", "xbee-api": "^0.6.0" }, @@ -38,14 +37,12 @@ "@types/chai": "^4.1.7", "@types/mocha": "^5.2.6", "@types/msgpack-lite": "^0.1.6", - "@types/node": "^11.13.8", - "@types/pouchdb": "^6.3.3", "@types/rc-slider": "^8.6.3", - "@types/react": "^16.8.14", + "@types/react": "^16.8.15", "@types/react-dom": "^16.8.4", "@types/react-leaflet": "^2.2.1", "@types/react-virtualized": "^9.21.1", - "@types/serialport": "^7.0.2", + "@types/serialport": "^7.0.3", "@typescript-eslint/eslint-plugin": "^1.7.0", "@typescript-eslint/parser": "^1.7.0", "babel-eslint": "^10.0.1", diff --git a/src/renderer/mainWindow/log/LogContainer.tsx b/src/renderer/mainWindow/log/LogContainer.tsx index 370ea358..29aa89f7 100644 --- a/src/renderer/mainWindow/log/LogContainer.tsx +++ b/src/renderer/mainWindow/log/LogContainer.tsx @@ -59,7 +59,7 @@ export default class LogContainer extends Component {}, 200);; + private scrollFromUserTimer: NodeJS.Timeout = setTimeout((): void => {}, 200); /** * Cache that stores the height for all log messages. Allows the messages to have proper height. @@ -67,7 +67,7 @@ export default class LogContainer extends Component Date: Wed, 1 May 2019 23:07:57 -0700 Subject: [PATCH 141/152] missionWindow CSS adding the missionWindow CSS. Currently there is only css for dark theme. There is no switch theme implemented yet. --- src/renderer/missionWindow/MissionWindow.tsx | 2 +- .../extra/CreateBoundingBoxButton.tsx | 2 +- .../extra/CreateWaypointButton.tsx | 4 +- src/renderer/missionWindow/mission.css | 34 ++++++++++++-- .../missionWindow/parameter/ISRSearch.tsx | 45 ++++++++++--------- .../missionWindow/parameter/PayloadDrop.tsx | 43 +++++++++--------- .../missionWindow/parameter/UGVRescue.tsx | 10 +++-- .../missionWindow/parameter/VTOLSearch.tsx | 10 +++-- .../missionWindow/parameter/parameters.css | 7 +++ 9 files changed, 102 insertions(+), 55 deletions(-) diff --git a/src/renderer/missionWindow/MissionWindow.tsx b/src/renderer/missionWindow/MissionWindow.tsx index 04619ce4..154ce8a9 100644 --- a/src/renderer/missionWindow/MissionWindow.tsx +++ b/src/renderer/missionWindow/MissionWindow.tsx @@ -349,7 +349,7 @@ export default class MissionWindow extends Component { />
-

{`${title[missionName]} Mission Parameters`}

+

{`${title[missionName]} Mission Parameters`}

{status === 'ready' && } {status !== 'ready' &&

:)

}
diff --git a/src/renderer/missionWindow/extra/CreateBoundingBoxButton.tsx b/src/renderer/missionWindow/extra/CreateBoundingBoxButton.tsx index e5688ded..f2590f60 100644 --- a/src/renderer/missionWindow/extra/CreateBoundingBoxButton.tsx +++ b/src/renderer/missionWindow/extra/CreateBoundingBoxButton.tsx @@ -28,6 +28,6 @@ export default class CreateBoundingBoxButton extends PureComponentCreate Box; + return ; } } diff --git a/src/renderer/missionWindow/extra/CreateWaypointButton.tsx b/src/renderer/missionWindow/extra/CreateWaypointButton.tsx index 46feceed..11eccc7e 100644 --- a/src/renderer/missionWindow/extra/CreateWaypointButton.tsx +++ b/src/renderer/missionWindow/extra/CreateWaypointButton.tsx @@ -1,5 +1,7 @@ import React, { PureComponent, ReactNode } from 'react'; +import '../mission.css'; + import ipc from '../../../util/ipc'; export interface CreateWaypointButtonProps { @@ -32,6 +34,6 @@ export default class CreateWaypointButton extends PureComponentCreate Pin; + return ; } } diff --git a/src/renderer/missionWindow/mission.css b/src/renderer/missionWindow/mission.css index 71bc39aa..997aead0 100644 --- a/src/renderer/missionWindow/mission.css +++ b/src/renderer/missionWindow/mission.css @@ -6,29 +6,35 @@ height: 100%; width: 100; grid-template-areas: - 'selector selector selector selector' + 'selector selector selector mapping' 'parameter parameter parameter mapping' 'parameter parameter parameter options' - 'button button button button'; + 'button button button options'; grid-template-rows: 25px 1fr 1fr 25px; grid-template-columns: repeat(4, 1fr); } .selectorContainer { grid-area: selector; - margin: auto; + margin-left: 60%; } .parameterContainer { grid-area: parameter; + padding-left: 20px; } .mappingContainer { grid-area: mapping; + padding-left: 20px; + background-color: #424242; } .optionsContainer { grid-area: options; + padding-top: 25px; + padding-left: 20px; + background-color: #424242; } .buttonContainer { @@ -60,3 +66,25 @@ width: 200px !important; display: inline-block; } + +.waypointButton, +.boundingBoxButton, +.selectorButton { + color: #bcbcbc; + background: transparent; + border: 2px; + border-style: solid; + border-color: #bcbcbc; + border-radius: 5px; +} + +.waypointButton:hover, +.boundingBoxButton:hover, +.selectorButton:hover { + color: #fff; + border-color: #fff; +} + +.title { + text-align: center; +} diff --git a/src/renderer/missionWindow/parameter/ISRSearch.tsx b/src/renderer/missionWindow/parameter/ISRSearch.tsx index be18f5cc..3f8d531d 100644 --- a/src/renderer/missionWindow/parameter/ISRSearch.tsx +++ b/src/renderer/missionWindow/parameter/ISRSearch.tsx @@ -1,6 +1,8 @@ import { Event, ipcRenderer } from 'electron'; import React, { Component, ReactNode } from 'react'; +import './parameters.css'; + import { missionName } from '../../../common/missions/ISRSearch'; import { Location } from '../../../static/index'; @@ -312,44 +314,45 @@ export class ISRSearch extends Component<{}, State> { return (

Takeoff Coordinates

- - - + + +

Loiter Coordinates

- - - - - + + + +
+ +

ISR Search Waypoints

- +
- - + +
- - + +
- - + +

Land Waypoints

- - - + + +

- - - + + +
diff --git a/src/renderer/missionWindow/parameter/PayloadDrop.tsx b/src/renderer/missionWindow/parameter/PayloadDrop.tsx index e7551c05..8c28b685 100644 --- a/src/renderer/missionWindow/parameter/PayloadDrop.tsx +++ b/src/renderer/missionWindow/parameter/PayloadDrop.tsx @@ -1,6 +1,8 @@ import { Event, ipcRenderer } from 'electron'; import React, { Component, ReactNode } from 'react'; +import './parameters.css'; + import { missionName } from '../../../common/missions/PayloadDrop'; import { Location } from '../../../static/index'; @@ -286,40 +288,41 @@ export class PayloadDrop extends Component { return (

Takeoff Coordinates

- - - + + +

Loiter Coordinates

- - - - - + + + +
+ +

Payload Drop Coordinates

- - - + + +
- - - + + +

Land Waypoints

- - - + + +

- - - + + +
diff --git a/src/renderer/missionWindow/parameter/UGVRescue.tsx b/src/renderer/missionWindow/parameter/UGVRescue.tsx index 0855bac0..803cbecd 100644 --- a/src/renderer/missionWindow/parameter/UGVRescue.tsx +++ b/src/renderer/missionWindow/parameter/UGVRescue.tsx @@ -1,6 +1,8 @@ import { Event, ipcRenderer } from 'electron'; import React, { Component, ReactNode } from 'react'; +import './parameters.css'; + import { missionName } from '../../../common/missions/UGVRescue'; import { Location } from '../../../static/index'; @@ -186,13 +188,13 @@ export class UGVRescue extends Component { return (

UGV Retrieve Target

- - + +

UGV Deliver Target

- - + +
); diff --git a/src/renderer/missionWindow/parameter/VTOLSearch.tsx b/src/renderer/missionWindow/parameter/VTOLSearch.tsx index 6caa5308..f65f6274 100644 --- a/src/renderer/missionWindow/parameter/VTOLSearch.tsx +++ b/src/renderer/missionWindow/parameter/VTOLSearch.tsx @@ -1,6 +1,8 @@ import { Event, ipcRenderer } from 'electron'; import React, { Component, ReactNode } from 'react'; +import './parameters.css'; + import { BoundingBoxBounds } from '../../../types/componentStyle'; import { missionName } from '../../../common/missions/VTOLSearch'; @@ -223,13 +225,13 @@ export class VTOLSearch extends Component { return (

Quick Scan

- +
- +
- +
- +
); diff --git a/src/renderer/missionWindow/parameter/parameters.css b/src/renderer/missionWindow/parameter/parameters.css index e69de29b..9fc7d3eb 100644 --- a/src/renderer/missionWindow/parameter/parameters.css +++ b/src/renderer/missionWindow/parameter/parameters.css @@ -0,0 +1,7 @@ + +.inputFields { + background-color: #bcbcbc; + border: 2px solid #000; + width: 150px; + height: 15px; +} From 7ced3765cd0fde7327e77e54c2d1f0c69472c76b Mon Sep 17 00:00:00 2001 From: Paul Matthew Barrameda Date: Fri, 3 May 2019 00:26:29 -0700 Subject: [PATCH 142/152] Small changes Was going to be bigger but I realized I was doing something I planned for next year instead. See #69 Instead here are changes: 1. Allow selecting text in log container (thanks @maxwey) 2. Fixed bug of not all missions are processed properly in Orchestrator if more than one mission was selected. Orchestrator will automatically assign parameters of any missions after the first mission. The user will only need to put parameters for the first mission executed. 3. Added support for UAV's two planes. --- src/common/missions/ISRSearch.ts | 2 ++ src/common/missions/PayloadDrop.ts | 2 ++ src/common/missions/UGVRescue.ts | 2 ++ src/common/missions/UUVRescue.ts | 4 +++ src/common/missions/VTOLSearch.ts | 2 ++ src/renderer/mainWindow/log/log.css | 27 ++++++++------------ src/renderer/missionWindow/MissionWindow.tsx | 23 ++++++++++------- src/static/vehicle.json | 7 ++++- src/types/missionInformation.ts | 14 ++++++---- 9 files changed, 51 insertions(+), 32 deletions(-) diff --git a/src/common/missions/ISRSearch.ts b/src/common/missions/ISRSearch.ts index e969b70c..7a45cae3 100644 --- a/src/common/missions/ISRSearch.ts +++ b/src/common/missions/ISRSearch.ts @@ -31,6 +31,8 @@ export class ISRSearch extends Mission { const missionParameters = information.parameters; const tasks = new DictionaryList(); + if (!missionParameters) return undefined; + if (!this.options.isrSearch.noTakeoff) { tasks.push('isrSearch', { taskType: 'takeoff', diff --git a/src/common/missions/PayloadDrop.ts b/src/common/missions/PayloadDrop.ts index e442e629..f0ecc0d5 100644 --- a/src/common/missions/PayloadDrop.ts +++ b/src/common/missions/PayloadDrop.ts @@ -30,6 +30,8 @@ export class PayloadDrop extends Mission { const missionParameters = information.parameters; const tasks = new DictionaryList(); + if (!missionParameters) return undefined; + if (!this.options.payloadDrop.noTakeoff) { tasks.push('payloadDrop', { taskType: 'takeoff', diff --git a/src/common/missions/UGVRescue.ts b/src/common/missions/UGVRescue.ts index 0440b7c0..4f214654 100644 --- a/src/common/missions/UGVRescue.ts +++ b/src/common/missions/UGVRescue.ts @@ -22,6 +22,8 @@ export class UGVRescue extends Mission { const missionParameters = information.parameters; const tasks = new DictionaryList(); + if (!missionParameters) return undefined; + tasks.push('ugvRescue', { taskType: 'retrieveTarget', ...missionParameters.retrieveTarget, diff --git a/src/common/missions/UUVRescue.ts b/src/common/missions/UUVRescue.ts index ce352735..c920d85a 100644 --- a/src/common/missions/UUVRescue.ts +++ b/src/common/missions/UUVRescue.ts @@ -19,8 +19,12 @@ export class UUVRescue extends Mission { // eslint-disable-next-line class-methods-use-this protected generateTasks(): DictionaryList | undefined { + const information = this.information as MissionInformation.UUVRescueInformation; + const missionParameters = information.parameters; const tasks = new DictionaryList(); + if (!missionParameters) return undefined; + tasks.push('uuvRescue', { taskType: 'retrieveTarget', }); diff --git a/src/common/missions/VTOLSearch.ts b/src/common/missions/VTOLSearch.ts index 9fd6dec6..1b7f89a3 100644 --- a/src/common/missions/VTOLSearch.ts +++ b/src/common/missions/VTOLSearch.ts @@ -68,6 +68,8 @@ export class VTOLSearch extends Mission { const missionParameters = information.parameters; const tasks = new DictionaryList(); + if (!missionParameters) return undefined; + tasks.push('quickScan', { taskType: 'quickScan', ...missionParameters.quickScan, diff --git a/src/renderer/mainWindow/log/log.css b/src/renderer/mainWindow/log/log.css index d8e79f96..b8c1007f 100644 --- a/src/renderer/mainWindow/log/log.css +++ b/src/renderer/mainWindow/log/log.css @@ -5,9 +5,8 @@ margin: 0; } -.logContainer .messages { - background-color: #fff; - color: #000; +.logContainer .messages, +.logContainer .messages_dark { font-family: Consolas, Menlo, @@ -20,6 +19,7 @@ monospace, serif; font-size: 14px; + user-select: text; width: 100%; height: calc(100% - 25px); } @@ -28,23 +28,16 @@ margin-right: 2em; } +/* Light theme */ + +.logContainer .messages { + background-color: #fff; + color: #000; +} + /* Dark theme */ .logContainer .messages_dark { background-color: #141414; color: #bdbdbd; - font-family: - Consolas, - Menlo, - Monaco, - Lucida Console, - Liberation Mono, - DejaVu Sans Mono, - Bitstream Vera Sans Mono, - Courier New, - monospace, - serif; - font-size: 14px; - width: 100%; - height: calc(100% - 25px); } diff --git a/src/renderer/missionWindow/MissionWindow.tsx b/src/renderer/missionWindow/MissionWindow.tsx index 154ce8a9..f51d9e48 100644 --- a/src/renderer/missionWindow/MissionWindow.tsx +++ b/src/renderer/missionWindow/MissionWindow.tsx @@ -36,6 +36,16 @@ const layouts: { [key: string]: MissionLayout[] } = { underwater: [ISRSearch, VTOLSearch, PayloadDrop, UUVRescue], }; +const initialInformation: { + [missionName in MissionInformation.MissionName]: MissionInformation.Information; +} = { + isrSearch: { missionName: 'isrSearch' }, + vtolSearch: { missionName: 'vtolSearch' }, + payloadDrop: { missionName: 'payloadDrop' }, + ugvRescue: { missionName: 'ugvRescue' }, + uuvRescue: { missionName: 'uuvRescue' }, +}; + const title: { [missionName in MissionInformation.MissionName]: string } = { isrSearch: 'ISR Search', vtolSearch: 'VTOL Search', @@ -97,7 +107,7 @@ interface State { /** * Information passed to Orchestrator for Mission to start. */ - information: { [missionName: string]: undefined | MissionInformation.Information }; + information: { [missionName in MissionInformation.MissionName]: MissionInformation.Information }; } /** @@ -131,7 +141,7 @@ export default class MissionWindow extends Component { noLand: false, }, }, - information: {}, + information: initialInformation, }; this.onSliderChange = this.onSliderChange.bind(this); @@ -263,12 +273,7 @@ export default class MissionWindow extends Component { // Gets all relevant mission information for the missions being performed for the mission type. const missionInformation = layouts[missionType].slice(startMissionIndex, endMissionIndex + 1) - .map(({ missionName }): MissionInformation.Information => { - if (!information[missionName]) { - throw new Error(`Undefined mission information for ${missionName}`); - } - return information[missionName] as MissionInformation.Information; - }); + .map(({ missionName }): MissionInformation.Information => information[missionName]); ipc.postStartMissions(missionInformation, activeVehicleMapping, options, requireConfirmation); this.setState({ status: 'running' }); @@ -334,7 +339,7 @@ export default class MissionWindow extends Component { * Start button will not appear unless all mission information is filled out * (and no mission is running). */ - const readyToStart = information[missionName] !== undefined; + const readyToStart = information[missionName].parameters !== undefined; return (
diff --git a/src/static/vehicle.json b/src/static/vehicle.json index 2d8166d0..3792747d 100644 --- a/src/static/vehicle.json +++ b/src/static/vehicle.json @@ -16,7 +16,12 @@ }, "100": { "macAddress": "", - "name": "UAV", + "name": "Skywalker", + "type": "plane" + }, + "101": { + "macAddress": "", + "name": "Piper Club", "type": "plane" }, "200": { diff --git a/src/types/missionInformation.ts b/src/types/missionInformation.ts index 5202d17f..3a3f17b6 100644 --- a/src/types/missionInformation.ts +++ b/src/types/missionInformation.ts @@ -55,14 +55,17 @@ interface InformationBase { /** * Parameters for the mission. Both user and mission generated. + * Optional, but user must put parameters for first mission. Orchestrator + * will attach parameters provided from mission completion to the next mission + * automatically (if more than one mission is run). */ - parameters: {}; + parameters?: {}; } // eslint-disable-next-line @typescript-eslint/interface-name-prefix export interface ISRSearchInformation extends InformationBase { missionName: 'isrSearch'; - parameters: { + parameters?: { takeoff: Task.TakeoffTaskParameters; isrSearch: Task.ISRSearchTaskParameters; land: Task.LandTaskParameters; @@ -78,7 +81,7 @@ function isISRSearchInformation(information: Information): boolean { export interface VTOLSearchInformation extends InformationBase { missionName: 'vtolSearch'; - parameters: { + parameters?: { quickScan: Task.QuickScanTaskParameters; }; } @@ -95,7 +98,7 @@ function isVTOLSearchInformation(information: Information): boolean { */ export interface PayloadDropInformation extends InformationBase { missionName: 'payloadDrop'; - parameters: { + parameters?: { takeoff: Task.TakeoffTaskParameters; payloadDrop: Task.PayloadDropTaskParameters; land: Task.LandTaskParameters; @@ -108,7 +111,7 @@ function isPayloadDropInformation(information: Information): boolean { export interface UGVRescueInformation extends InformationBase { missionName: 'ugvRescue'; - parameters: { + parameters?: { retrieveTarget: Task.UGVRetrieveTargetTaskParameters; deliverTarget: Task.DeliverTargetTaskParameters; }; @@ -120,6 +123,7 @@ function isUGVRetreiveInformation(information: Information): boolean { export interface UUVRescueInformation extends InformationBase { missionName: 'uuvRescue'; + parameters?: {}; } function isUUVRetrieveInformation(information: Information): boolean { From 020276fa1ae109b67a6d26403faeb76fc4dcbdc6 Mon Sep 17 00:00:00 2001 From: tnleang Date: Fri, 3 May 2019 12:57:54 -0700 Subject: [PATCH 143/152] Added light theme mission window --- src/renderer/missionWindow/MissionWindow.tsx | 31 +++++------- .../extra/ActiveVehicleMapping.tsx | 4 +- src/renderer/missionWindow/extra/Checkbox.tsx | 2 + .../extra/CreateBoundingBoxButton.tsx | 14 +++++- .../extra/CreateWaypointButton.tsx | 14 +++++- src/renderer/missionWindow/mission.css | 48 +++++++++++++++---- .../missionWindow/parameter/ISRSearch.tsx | 21 ++++---- .../missionWindow/parameter/PayloadDrop.tsx | 17 ++++--- .../missionWindow/parameter/UGVRescue.tsx | 9 ++-- .../missionWindow/parameter/VTOLSearch.tsx | 7 +-- .../missionWindow/parameter/parameters.css | 7 +-- 11 files changed, 117 insertions(+), 57 deletions(-) diff --git a/src/renderer/missionWindow/MissionWindow.tsx b/src/renderer/missionWindow/MissionWindow.tsx index f51d9e48..24babf5f 100644 --- a/src/renderer/missionWindow/MissionWindow.tsx +++ b/src/renderer/missionWindow/MissionWindow.tsx @@ -36,16 +36,6 @@ const layouts: { [key: string]: MissionLayout[] } = { underwater: [ISRSearch, VTOLSearch, PayloadDrop, UUVRescue], }; -const initialInformation: { - [missionName in MissionInformation.MissionName]: MissionInformation.Information; -} = { - isrSearch: { missionName: 'isrSearch' }, - vtolSearch: { missionName: 'vtolSearch' }, - payloadDrop: { missionName: 'payloadDrop' }, - ugvRescue: { missionName: 'ugvRescue' }, - uuvRescue: { missionName: 'uuvRescue' }, -}; - const title: { [missionName in MissionInformation.MissionName]: string } = { isrSearch: 'ISR Search', vtolSearch: 'VTOL Search', @@ -107,7 +97,7 @@ interface State { /** * Information passed to Orchestrator for Mission to start. */ - information: { [missionName in MissionInformation.MissionName]: MissionInformation.Information }; + information: { [missionName: string]: undefined | MissionInformation.Information }; } /** @@ -141,7 +131,7 @@ export default class MissionWindow extends Component { noLand: false, }, }, - information: initialInformation, + information: {}, }; this.onSliderChange = this.onSliderChange.bind(this); @@ -273,7 +263,12 @@ export default class MissionWindow extends Component { // Gets all relevant mission information for the missions being performed for the mission type. const missionInformation = layouts[missionType].slice(startMissionIndex, endMissionIndex + 1) - .map(({ missionName }): MissionInformation.Information => information[missionName]); + .map(({ missionName }): MissionInformation.Information => { + if (!information[missionName]) { + throw new Error(`Undefined mission information for ${missionName}`); + } + return information[missionName] as MissionInformation.Information; + }); ipc.postStartMissions(missionInformation, activeVehicleMapping, options, requireConfirmation); this.setState({ status: 'running' }); @@ -339,12 +334,12 @@ export default class MissionWindow extends Component { * Start button will not appear unless all mission information is filled out * (and no mission is running). */ - const readyToStart = information[missionName].parameters !== undefined; + const readyToStart = information[missionName] !== undefined; return (
- + {

{`${title[missionName]} Mission Parameters`}

- {status === 'ready' && } + {status === 'ready' && } {status !== 'ready' &&

:)

}
-
+

Vehicle Mapping

-
+

Options

diff --git a/src/renderer/missionWindow/extra/ActiveVehicleMapping.tsx b/src/renderer/missionWindow/extra/ActiveVehicleMapping.tsx index 22395a3d..a6128432 100644 --- a/src/renderer/missionWindow/extra/ActiveVehicleMapping.tsx +++ b/src/renderer/missionWindow/extra/ActiveVehicleMapping.tsx @@ -1,5 +1,7 @@ import React, { PureComponent, Fragment, ReactNode } from 'react'; +import '../mission.css'; + import missionObject from '../../../common/missions/index'; import { vehicleConfig, VehicleInfo } from '../../../static/index'; @@ -43,7 +45,6 @@ export default class ActiveVehicleMapping extends PureComponent - {`${jobType}: `} - +

Loiter Coordinates

@@ -326,34 +329,34 @@ export class ISRSearch extends Component<{}, State> {
- +

ISR Search Waypoints


- +
- +
- +

Land Waypoints

- +

- +
); diff --git a/src/renderer/missionWindow/parameter/PayloadDrop.tsx b/src/renderer/missionWindow/parameter/PayloadDrop.tsx index 8c28b685..79f16c28 100644 --- a/src/renderer/missionWindow/parameter/PayloadDrop.tsx +++ b/src/renderer/missionWindow/parameter/PayloadDrop.tsx @@ -3,6 +3,8 @@ import React, { Component, ReactNode } from 'react'; import './parameters.css'; +import { ThemeProps } from '../../../types/componentStyle'; + import { missionName } from '../../../common/missions/PayloadDrop'; import { Location } from '../../../static/index'; @@ -65,7 +67,7 @@ const lockedCache: Locked = { }; // eslint-disable-next-line @typescript-eslint/interface-name-prefix -export interface PayloadDropProps { +export interface PayloadDropProps extends ThemeProps { vehicles: { [vehicleId: number]: VehicleObject }; } @@ -285,13 +287,14 @@ export class PayloadDrop extends Component { public render(): ReactNode { const { checklist, locked } = this.state; + const { theme } = this.props; return (

Takeoff Coordinates

- +

Loiter Coordinates

@@ -300,30 +303,30 @@ export class PayloadDrop extends Component {
- +

Payload Drop Coordinates

- +
- +

Land Waypoints

- +

- +
); diff --git a/src/renderer/missionWindow/parameter/UGVRescue.tsx b/src/renderer/missionWindow/parameter/UGVRescue.tsx index 803cbecd..d257710b 100644 --- a/src/renderer/missionWindow/parameter/UGVRescue.tsx +++ b/src/renderer/missionWindow/parameter/UGVRescue.tsx @@ -3,6 +3,8 @@ import React, { Component, ReactNode } from 'react'; import './parameters.css'; +import { ThemeProps } from '../../../types/componentStyle'; + import { missionName } from '../../../common/missions/UGVRescue'; import { Location } from '../../../static/index'; @@ -35,7 +37,7 @@ const lockedCache: Locked = { deliverTarget: true, }; -export interface UGVRescueProps { +export interface UGVRescueProps extends ThemeProps { vehicles: { [vehicleId: number]: VehicleObject }; } @@ -184,18 +186,19 @@ export class UGVRescue extends Component { public render(): ReactNode { const { checklist, locked } = this.state; + const { theme } = this.props; return (

UGV Retrieve Target

- +

UGV Deliver Target

- +
); } diff --git a/src/renderer/missionWindow/parameter/VTOLSearch.tsx b/src/renderer/missionWindow/parameter/VTOLSearch.tsx index f65f6274..71ea5c7b 100644 --- a/src/renderer/missionWindow/parameter/VTOLSearch.tsx +++ b/src/renderer/missionWindow/parameter/VTOLSearch.tsx @@ -3,7 +3,7 @@ import React, { Component, ReactNode } from 'react'; import './parameters.css'; -import { BoundingBoxBounds } from '../../../types/componentStyle'; +import { BoundingBoxBounds, ThemeProps } from '../../../types/componentStyle'; import { missionName } from '../../../common/missions/VTOLSearch'; @@ -34,7 +34,7 @@ const lockedCache: Locked = { }; // eslint-disable-next-line @typescript-eslint/interface-name-prefix -export interface VTOLSearchProps { +export interface VTOLSearchProps extends ThemeProps { vehicles: { [vehicleId: number]: VehicleObject }; } @@ -222,6 +222,7 @@ export class VTOLSearch extends Component { public render(): ReactNode { const { checklist, locked } = this.state; + const { theme } = this.props; return (

Quick Scan

@@ -232,7 +233,7 @@ export class VTOLSearch extends Component {
- +
); } diff --git a/src/renderer/missionWindow/parameter/parameters.css b/src/renderer/missionWindow/parameter/parameters.css index 9fc7d3eb..a2540896 100644 --- a/src/renderer/missionWindow/parameter/parameters.css +++ b/src/renderer/missionWindow/parameter/parameters.css @@ -1,7 +1,8 @@ .inputFields { background-color: #bcbcbc; - border: 2px solid #000; - width: 150px; - height: 15px; + border: none; + margin: 2px; + width: 200px; + height: 20px; } From d8903c412f57e55566b26258e826fe209bbac1b7 Mon Sep 17 00:00:00 2001 From: Marlon Aquino Date: Fri, 3 May 2019 14:06:23 -0700 Subject: [PATCH 144/152] Fixed Buttons --- src/renderer/missionWindow/extra/CreateBoundingBoxButton.tsx | 2 +- src/renderer/missionWindow/extra/CreateWaypointButton.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/renderer/missionWindow/extra/CreateBoundingBoxButton.tsx b/src/renderer/missionWindow/extra/CreateBoundingBoxButton.tsx index 8a0cd5a6..529db228 100644 --- a/src/renderer/missionWindow/extra/CreateBoundingBoxButton.tsx +++ b/src/renderer/missionWindow/extra/CreateBoundingBoxButton.tsx @@ -36,7 +36,7 @@ export default class CreateBoundingBoxButton extends PureComponent - Create Box + {'Create Box'} ); } diff --git a/src/renderer/missionWindow/extra/CreateWaypointButton.tsx b/src/renderer/missionWindow/extra/CreateWaypointButton.tsx index 223dd630..eb330ea5 100644 --- a/src/renderer/missionWindow/extra/CreateWaypointButton.tsx +++ b/src/renderer/missionWindow/extra/CreateWaypointButton.tsx @@ -42,7 +42,7 @@ export default class CreateWaypointButton extends PureComponent - Create Pin + {'Create Pin'} ); } From 20bdcbb7bfc9383b63c5fb14bcd971da722f39fc Mon Sep 17 00:00:00 2001 From: tnleang Date: Fri, 3 May 2019 14:16:21 -0700 Subject: [PATCH 145/152] Readded info readded info back to missionWindow.tsx --- src/renderer/missionWindow/MissionWindow.tsx | 25 ++++++++++++-------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/src/renderer/missionWindow/MissionWindow.tsx b/src/renderer/missionWindow/MissionWindow.tsx index 24babf5f..e6348ff1 100644 --- a/src/renderer/missionWindow/MissionWindow.tsx +++ b/src/renderer/missionWindow/MissionWindow.tsx @@ -36,6 +36,16 @@ const layouts: { [key: string]: MissionLayout[] } = { underwater: [ISRSearch, VTOLSearch, PayloadDrop, UUVRescue], }; +const initialInformation: { + [missionName in MissionInformation.MissionName]: MissionInformation.Information; +} = { + isrSearch: { missionName: 'isrSearch' }, + vtolSearch: { missionName: 'vtolSearch' }, + payloadDrop: { missionName: 'payloadDrop' }, + ugvRescue: { missionName: 'ugvRescue' }, + uuvRescue: { missionName: 'uuvRescue' }, +}; + const title: { [missionName in MissionInformation.MissionName]: string } = { isrSearch: 'ISR Search', vtolSearch: 'VTOL Search', @@ -97,7 +107,7 @@ interface State { /** * Information passed to Orchestrator for Mission to start. */ - information: { [missionName: string]: undefined | MissionInformation.Information }; + information: { [missionName in MissionInformation.MissionName]: MissionInformation.Information }; } /** @@ -131,7 +141,7 @@ export default class MissionWindow extends Component { noLand: false, }, }, - information: {}, + information: initialInformation, }; this.onSliderChange = this.onSliderChange.bind(this); @@ -263,12 +273,7 @@ export default class MissionWindow extends Component { // Gets all relevant mission information for the missions being performed for the mission type. const missionInformation = layouts[missionType].slice(startMissionIndex, endMissionIndex + 1) - .map(({ missionName }): MissionInformation.Information => { - if (!information[missionName]) { - throw new Error(`Undefined mission information for ${missionName}`); - } - return information[missionName] as MissionInformation.Information; - }); + .map(({ missionName }): MissionInformation.Information => information[missionName]); ipc.postStartMissions(missionInformation, activeVehicleMapping, options, requireConfirmation); this.setState({ status: 'running' }); @@ -334,12 +339,12 @@ export default class MissionWindow extends Component { * Start button will not appear unless all mission information is filled out * (and no mission is running). */ - const readyToStart = information[missionName] !== undefined; + const readyToStart = information[missionName].parameters !== undefined; return (
- + Date: Fri, 3 May 2019 15:22:56 -0700 Subject: [PATCH 146/152] update css --- src/renderer/missionWindow/MissionWindow.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/renderer/missionWindow/MissionWindow.tsx b/src/renderer/missionWindow/MissionWindow.tsx index e6348ff1..caa17a6b 100644 --- a/src/renderer/missionWindow/MissionWindow.tsx +++ b/src/renderer/missionWindow/MissionWindow.tsx @@ -344,7 +344,7 @@ export default class MissionWindow extends Component { return (
- + Date: Fri, 3 May 2019 21:07:10 -0700 Subject: [PATCH 147/152] Pre-test updates Decided will not fix #65 and #70 as those are actually more difficult to implement than expected. Will test this branch more to see basic functionality and then will merge to dev-2018 and then to master --- src/common/MessageHandler.ts | 1 + src/common/Orchestrator.ts | 22 ++++++---- src/common/Xbee.ts | 2 +- src/common/missions/PayloadDrop.ts | 40 ++----------------- src/common/struct/Mission.ts | 6 ++- .../missionWindow/parameter/PayloadDrop.tsx | 4 +- src/static/location.json | 7 +++- src/static/vehicle.json | 4 +- 8 files changed, 33 insertions(+), 53 deletions(-) diff --git a/src/common/MessageHandler.ts b/src/common/MessageHandler.ts index 0ca60175..01352cd8 100644 --- a/src/common/MessageHandler.ts +++ b/src/common/MessageHandler.ts @@ -113,6 +113,7 @@ class MessageHandler { const onDisconnect = (): void => { clearInterval(expiry); this.sending.delete(jsonMessage.tid); + this.outbox.clear(`${jsonMessage.tid}`); ipc.postDisconnectFromVehicle(jsonMessage.tid); }; diff --git a/src/common/Orchestrator.ts b/src/common/Orchestrator.ts index da89f2b7..88b124d7 100644 --- a/src/common/Orchestrator.ts +++ b/src/common/Orchestrator.ts @@ -122,7 +122,10 @@ class Orchestrator { status: 'ready', }); - this.ping(this.vehicles[jsonMessage.sid]); + setTimeout( + (): void => this.ping(this.vehicles[jsonMessage.sid]), + config.vehicleDisconnectionTime * 1000, + ); } else { this.vehicles[jsonMessage.sid].connect(); } @@ -142,11 +145,14 @@ class Orchestrator { * @param vehicle Vehicle to "ping". */ private ping(vehicle: Vehicle): void { - const delta = Date.now() - vehicle.getLastConnectionTime(); + const delta = Math.abs(Date.now() - vehicle.getLastConnectionTime()); if (delta >= 0 && delta <= config.vehicleDisconnectionTime * 1000) { // Handler that expires and creates itself everytime it "pings" the vehicle. - setTimeout((): void => { this.ping(vehicle); }, delta); + setTimeout( + (): void => { this.ping(vehicle); }, + config.vehicleDisconnectionTime * 1000 - delta, + ); } else { this.disconnectFromVehicle(vehicle.getVehicleId()); } @@ -358,12 +364,12 @@ class Orchestrator { this.stopMissions(); } else { this.missions[this.currentMissionIndex + 1].parameters = completionParameters; - } - if (this.requireConfirmation) { - ipc.postConfirmCompleteMission(); // Start next mission on "startNextMission" notification. - } else { - this.startNextMission(); + if (this.requireConfirmation) { + ipc.postConfirmCompleteMission(); // Start next mission on "startNextMission" notification. + } else { + this.startNextMission(); + } } } diff --git a/src/common/Xbee.ts b/src/common/Xbee.ts index 107b02d7..2f52dcd7 100644 --- a/src/common/Xbee.ts +++ b/src/common/Xbee.ts @@ -9,7 +9,7 @@ import { JSONMessage } from '../types/message'; import ipc from '../util/ipc'; // TODO: Add a feature with Vehicle container to change this dynamically and reconnect. -const port: string | undefined = 'COM5'; +const port: string | undefined = '/dev/tty.SLAB_USBtoUART'; const serialport = new SerialPort(port, { baudRate: 57600 }, (error): void => { if (error) { diff --git a/src/common/missions/PayloadDrop.ts b/src/common/missions/PayloadDrop.ts index f0ecc0d5..4866cef3 100644 --- a/src/common/missions/PayloadDrop.ts +++ b/src/common/missions/PayloadDrop.ts @@ -1,14 +1,11 @@ import DictionaryList from '../struct/DictionaryList'; import Mission from '../struct/Mission'; -import { JobType, Location } from '../../static/index'; +import { JobType } from '../../static/index'; -import * as Message from '../../types/message'; import * as MissionInformation from '../../types/missionInformation'; import { Task, TaskParameters } from '../../types/task'; -import ipc from '../../util/ipc'; - export const missionName: MissionInformation.MissionName = 'payloadDrop'; export const jobTypes: JobType[] = ['payloadDrop']; @@ -20,11 +17,6 @@ export class PayloadDrop extends Mission { protected addTaskCompare = {}; - /** - * Point of interest. - */ - private missionData: Location | null = null; - protected generateTasks(): DictionaryList | undefined { const information = this.information as MissionInformation.PayloadDropInformation; const missionParameters = information.parameters; @@ -54,35 +46,9 @@ export class PayloadDrop extends Mission { return tasks; } - public update(jsonMessage: Message.JSONMessage): void { - super.update(jsonMessage); - - if (Message.TypeGuard.isPOIMessage(jsonMessage) - && this.activeVehicleMapping[this.missionName][jsonMessage.sid] === 'payloadDrop') { - const poiMessage = jsonMessage as Message.POIMessage; - this.missionData = { - lat: poiMessage.lat, - lng: poiMessage.lng, - }; - } - } - + // eslint-disable-next-line class-methods-use-this protected generateCompletionParameters(): { [key: string]: TaskParameters } | undefined { - if (!this.missionData) { - ipc.postLogMessages({ - type: 'failure', - message: 'No target location was provided in payload drop', - }); - return undefined; // Mission will stop from this, not complete. - } - - return { - retrieveTarget: { - lat: this.missionData.lat, - lng: this.missionData.lng, - }, - // TODO: generate data for deliverTarget - }; + return {}; } } diff --git a/src/common/struct/Mission.ts b/src/common/struct/Mission.ts index 8fd69772..10db02a6 100644 --- a/src/common/struct/Mission.ts +++ b/src/common/struct/Mission.ts @@ -289,8 +289,8 @@ export default abstract class Mission { this.waitingVehicles.push(jobType, vehicle); }); - const allVehiclesWaiting = pendingAssignVehicleIds.some( - (vehicleId): boolean => this.vehicles[vehicleId].getStatus() !== 'waiting', + const allVehiclesWaiting = pendingAssignVehicleIds.every( + (vehicleId): boolean => this.vehicles[vehicleId].getStatus() === 'waiting', ); if (!allVehiclesWaiting) { @@ -333,6 +333,8 @@ export default abstract class Mission { if (Message.TypeGuard.isCompleteMessage(jsonMessage)) { const jobType = this.activeVehicleMapping[this.missionName][jsonMessage.sid]; + ipc.postLogMessages({ message: `Finished a task for ${this.missionName}` }); + /* * Mission is not yet finished, continue to assign tasks. One of the following will happen: * 1. Assigns next task to vehicle. diff --git a/src/renderer/missionWindow/parameter/PayloadDrop.tsx b/src/renderer/missionWindow/parameter/PayloadDrop.tsx index 79f16c28..620fbd26 100644 --- a/src/renderer/missionWindow/parameter/PayloadDrop.tsx +++ b/src/renderer/missionWindow/parameter/PayloadDrop.tsx @@ -308,12 +308,12 @@ export class PayloadDrop extends Component {

Payload Drop Coordinates

- +
- +

Land Waypoints

diff --git a/src/static/location.json b/src/static/location.json index c26d5912..5be75db1 100644 --- a/src/static/location.json +++ b/src/static/location.json @@ -1,5 +1,5 @@ { - "startLocation": "Cal Poly SLO", + "startLocation": "Prado Airpark", "locations": { "Cal Poly SLO": { "lat": 35.306205, @@ -10,6 +10,11 @@ "lat": 34.055869, "lng": -117.819964, "zoom": 18 + }, + "Prado Airpark": { + "lat": 33.9325635, + "lng": -117.6288792, + "zoom": 18 } } } diff --git a/src/static/vehicle.json b/src/static/vehicle.json index 3792747d..9e0ac3ac 100644 --- a/src/static/vehicle.json +++ b/src/static/vehicle.json @@ -15,13 +15,13 @@ "type": "station" }, "100": { - "macAddress": "", + "macAddress": "0013A200419477B6", "name": "Skywalker", "type": "plane" }, "101": { "macAddress": "", - "name": "Piper Club", + "name": "Piper Cub", "type": "plane" }, "200": { From d0a49a0a83821d0d4bb22e61c992d1f384891d0a Mon Sep 17 00:00:00 2001 From: Paul Matthew Barrameda Date: Sat, 4 May 2019 13:32:32 -0700 Subject: [PATCH 148/152] No more FOSSA --- .travis.yml | 7 ------- README.md | 5 ----- src/types/message.ts | 2 +- 3 files changed, 1 insertion(+), 13 deletions(-) diff --git a/.travis.yml b/.travis.yml index b49db0a1..a04dfe76 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,10 +2,6 @@ language: node_js -# Install fossa-cli -before_script: - - "curl -H 'Cache-Control: no-cache' https://raw.githubusercontent.com/fossas/fossa-cli/master/install.sh | sudo bash" - # Use latest version of Node.js node_js: - lts/* @@ -19,6 +15,3 @@ script: - npm run test:lint - npm run test:types - npm run test:unit - - - fossa init - - fossa analyze diff --git a/README.md b/README.md index 907af015..c9bc5eb3 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,6 @@ [![Build Status](https://travis-ci.com/NGCP/GCS.svg?branch=dev-2018)](https://travis-ci.com/NGCP/GCS) [![dependencies Status](https://david-dm.org/NGCP/GCS/status.svg)](https://david-dm.org/NGCP/GCS) [![devDependencies Status](https://david-dm.org/NGCP/GCS/dev-status.svg)](https://david-dm.org/NGCP/GCS?type=dev) -[![FOSSA Status](https://app.fossa.com/api/projects/custom%2B9570%2Fgithub.com%2FNGCP%2FGCS.svg?type=shield)](https://app.fossa.com/projects/custom%2B9570%2Fgithub.com%2FNGCP%2FGCS?ref=badge_shield) The [Northrop Grumman Collaboration Project] presents the Ground Control Station. This project's objective is to view and set missions for all autonomous vehicle platforms in the project. @@ -38,10 +37,6 @@ npm start ## License [MIT](https://github.com/NGCP/GCS/blob/dev-2018/LICENSE) -Licences of this project's dependencies are tracked by FOSSA: - -[![FOSSA Status](https://app.fossa.com/api/projects/custom%2B9570%2Fgithub.com%2FNGCP%2FGCS.svg?type=large)](https://app.fossa.com/projects/custom%2B9570%2Fgithub.com%2FNGCP%2FGCS?ref=badge_large) - [Northrop Grumman Collaboration Project]: http://www.ngcpcalpoly.com/about.html [Node.js]: https://github.com/nodejs/node [React]: https://github.com/facebook/react diff --git a/src/types/message.ts b/src/types/message.ts index fddfcc95..ae4a26ec 100644 --- a/src/types/message.ts +++ b/src/types/message.ts @@ -304,7 +304,7 @@ export function isJSONMessage(message: { [key: string]: any }): boolean { if (!isMessage(message)) return false; // Check if id is a number. - const idCheck = message.id && typeof message.id !== 'boolean' && Number.isInteger(message.id); + const idCheck = Number.isInteger(message.id); // Check if tid is a number and is valid. const tidCheck = Number.isInteger(message.tid) From 0124fae15db6cacb3611c9a1ae3e9949389c7015 Mon Sep 17 00:00:00 2001 From: Paul Matthew Barrameda Date: Sat, 4 May 2019 14:03:58 -0700 Subject: [PATCH 149/152] Version bump --- package.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 507ff71e..57c7cf71 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "GCS", - "version": "0.7.3", + "version": "0.7.4", "license": "MIT", "author": "Northrop Grumman Collaboration Project", "description": "Ground Control Station for autonomous vehicle platforms in NGCP", @@ -38,7 +38,7 @@ "@types/mocha": "^5.2.6", "@types/msgpack-lite": "^0.1.6", "@types/rc-slider": "^8.6.3", - "@types/react": "^16.8.15", + "@types/react": "^16.8.16", "@types/react-dom": "^16.8.4", "@types/react-leaflet": "^2.2.1", "@types/react-virtualized": "^9.21.1", @@ -48,7 +48,7 @@ "babel-eslint": "^10.0.1", "chai": "^4.2.0", "dotenv-webpack": "^1.7.0", - "electron": "^3.1.8", + "electron": "^3.1.9", "electron-builder": "^20.40.2", "electron-rebuild": "^1.8.4", "electron-webpack": "^2.6.2", @@ -59,7 +59,7 @@ "eslint-plugin-chai-friendly": "^0.4.1", "eslint-plugin-import": "^2.17.2", "eslint-plugin-jsx-a11y": "^6.2.1", - "eslint-plugin-react": "^7.12.4", + "eslint-plugin-react": "^7.13.0", "mocha": "^6.1.4", "stylelint": "^10.0.1", "stylelint-config-standard": "^18.3.0", From 1770340a4c486f06cffa7868824bfc0e9a06a38a Mon Sep 17 00:00:00 2001 From: Paul Matthew Barrameda Date: Sat, 4 May 2019 14:42:48 -0700 Subject: [PATCH 150/152] Change in logic --- src/common/Orchestrator.ts | 12 +++++------- src/static/vehicle.json | 2 +- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/src/common/Orchestrator.ts b/src/common/Orchestrator.ts index 88b124d7..8435d888 100644 --- a/src/common/Orchestrator.ts +++ b/src/common/Orchestrator.ts @@ -112,8 +112,6 @@ class Orchestrator { * @param jsonMessage The connect message. */ private connectToVehicle(jsonMessage: Message.JSONMessage, newMessage: boolean): void { - if (this.vehicles[jsonMessage.sid] && this.vehicles[jsonMessage.sid].getStatus() !== 'disconnected') return; - if (newMessage) { if (!this.vehicles[jsonMessage.sid]) { this.vehicles[jsonMessage.sid] = new Vehicle({ @@ -163,7 +161,7 @@ class Orchestrator { * @param vehicleId ID of vehicle to disconnect from. */ private disconnectFromVehicle(vehicleId: number): void { - if (!this.vehicles[vehicleId] || this.vehicles[vehicleId].getStatus() === 'disconnected') return; + if (this.vehicles[vehicleId].getStatus() === 'disconnected') return; this.vehicles[vehicleId].disconnect(); ipc.postUpdateVehicles(this.vehicles[vehicleId].toObject()); @@ -195,7 +193,7 @@ class Orchestrator { * @param jsonMessage Message from vehicle. */ private handleBadMessage(jsonMessage: Message.JSONMessage, newMessage: boolean): void { - if (!this.vehicles[jsonMessage.sid] || this.vehicles[jsonMessage.sid].getStatus() === 'disconnected') return; + if (this.vehicles[jsonMessage.sid].getStatus() === 'disconnected') return; if (!newMessage) return; this.vehicles[jsonMessage.sid].update(jsonMessage); @@ -212,7 +210,7 @@ class Orchestrator { * @param jsonMessage Message from vehicle. */ private handleUpdateMessage(jsonMessage: Message.JSONMessage, newMessage: boolean): void { - if (!this.vehicles[jsonMessage.sid] || this.vehicles[jsonMessage.sid].getStatus() === 'disconnected') return; + if (this.vehicles[jsonMessage.sid].getStatus() === 'disconnected') return; if (newMessage) { this.vehicles[jsonMessage.sid].update(jsonMessage); @@ -236,7 +234,7 @@ class Orchestrator { * @param jsonMessage Message from vehicle. */ private handlePOIMessage(jsonMessage: Message.JSONMessage, newMessage: boolean): void { - if (!this.vehicles[jsonMessage.sid] || this.vehicles[jsonMessage.sid].getStatus() === 'disconnected') return; + if (this.vehicles[jsonMessage.sid].getStatus() === 'disconnected') return; if (newMessage) { if (this.currentMission && !this.currentMission.getVehicles()[jsonMessage.sid]) { @@ -259,7 +257,7 @@ class Orchestrator { * @param jsonMessage Message from vehicle. */ private handleCompleteMessage(jsonMessage: Message.JSONMessage, newMessage: boolean): void { - if (!this.vehicles[jsonMessage.sid] || this.vehicles[jsonMessage.sid].getStatus() === 'disconnected') return; + if (this.vehicles[jsonMessage.sid].getStatus() === 'disconnected') return; if (newMessage) { if (!this.currentMission) { diff --git a/src/static/vehicle.json b/src/static/vehicle.json index 9e0ac3ac..29f4c70e 100644 --- a/src/static/vehicle.json +++ b/src/static/vehicle.json @@ -25,7 +25,7 @@ "type": "plane" }, "200": { - "macAddress": "0013A2004194754E", + "macAddress": "0013A200419475BB", "name": "UGV", "type": "rover" }, From 6046d40c67714397b76d1be5e37683718f30316b Mon Sep 17 00:00:00 2001 From: Paul Matthew Barrameda Date: Sat, 4 May 2019 16:50:34 -0700 Subject: [PATCH 151/152] Small update logic --- src/common/MessageHandler.ts | 4 +++- src/common/Orchestrator.ts | 16 +++++++++++----- src/common/struct/Vehicle.ts | 4 ++-- src/util/ipc.ts | 8 ++++++-- 4 files changed, 22 insertions(+), 10 deletions(-) diff --git a/src/common/MessageHandler.ts b/src/common/MessageHandler.ts index 01352cd8..93affcd5 100644 --- a/src/common/MessageHandler.ts +++ b/src/common/MessageHandler.ts @@ -166,7 +166,9 @@ class MessageHandler { || this.receivedMessageId[jsonMessage.sid] as number < jsonMessage.id; if (Message.TypeGuard.isConnectMessage(jsonMessage)) { - ipc.postConnectToVehicle(jsonMessage, newMessage); + const shouldAcknowledge = !this.receivedMessageId[jsonMessage.sid] + || this.receivedMessageId[jsonMessage.sid] as number <= jsonMessage.id; + ipc.postConnectToVehicle(jsonMessage, newMessage, shouldAcknowledge); } else if (Message.TypeGuard.isCompleteMessage(jsonMessage)) { ipc.postHandleCompleteMessage(jsonMessage, newMessage); } else if (Message.TypeGuard.isPOIMessage(jsonMessage)) { diff --git a/src/common/Orchestrator.ts b/src/common/Orchestrator.ts index 8435d888..cdcca69f 100644 --- a/src/common/Orchestrator.ts +++ b/src/common/Orchestrator.ts @@ -84,7 +84,7 @@ class Orchestrator { private options: MissionInformation.MissionOptions | null = null; public constructor() { - ipcRenderer.on('connectToVehicle', (_: Event, jsonMessage: Message.JSONMessage, newMessage: boolean): void => this.connectToVehicle(jsonMessage, newMessage)); + ipcRenderer.on('connectToVehicle', (_: Event, jsonMessage: Message.JSONMessage, newMessage: boolean, shouldAcknowledge: boolean): void => this.connectToVehicle(jsonMessage, newMessage, shouldAcknowledge)); ipcRenderer.on('disconnectFromVehicle', (_: Event, vehicleId: number): void => this.disconnectFromVehicle(vehicleId)); ipcRenderer.on('handleAcknowledgementMessage', (_: Event, jsonMessage: Message.JSONMessage, newMessage: boolean): void => this.handleAcknowledgementMessage(jsonMessage, newMessage)); @@ -111,7 +111,11 @@ class Orchestrator { * Connects to vehicle specified by the connect message from message handler. * @param jsonMessage The connect message. */ - private connectToVehicle(jsonMessage: Message.JSONMessage, newMessage: boolean): void { + private connectToVehicle( + jsonMessage: Message.JSONMessage, + newMessage: boolean, + shouldAcknowledge: boolean, + ): void { if (newMessage) { if (!this.vehicles[jsonMessage.sid]) { this.vehicles[jsonMessage.sid] = new Vehicle({ @@ -134,7 +138,7 @@ class Orchestrator { }); } - ipc.postSendMessage(jsonMessage.sid, { type: 'connectionAck' }); + if (shouldAcknowledge) ipc.postSendMessage(jsonMessage.sid, { type: 'connectionAck' }); } /** @@ -143,9 +147,11 @@ class Orchestrator { * @param vehicle Vehicle to "ping". */ private ping(vehicle: Vehicle): void { - const delta = Math.abs(Date.now() - vehicle.getLastConnectionTime()); + const delta = Math.max(0, Date.now() - vehicle.getLastConnectionTime()); + + ipc.postLogMessages({ message: `Pinging vehicle with delta = ${delta}` }); - if (delta >= 0 && delta <= config.vehicleDisconnectionTime * 1000) { + if (delta <= config.vehicleDisconnectionTime * 1000) { // Handler that expires and creates itself everytime it "pings" the vehicle. setTimeout( (): void => { this.ping(vehicle); }, diff --git a/src/common/struct/Vehicle.ts b/src/common/struct/Vehicle.ts index 5e4c55f5..9de2ddb6 100644 --- a/src/common/struct/Vehicle.ts +++ b/src/common/struct/Vehicle.ts @@ -115,8 +115,8 @@ export default class Vehicle { return false; }); - this.updateEventHandler.addHandler('time', (time): boolean => { - this.lastConnectionTime = time; + this.updateEventHandler.addHandler('time', (): boolean => { + this.lastConnectionTime = Date.now(); return false; }); diff --git a/src/util/ipc.ts b/src/util/ipc.ts index 6cdd1d96..cdd8e7a8 100644 --- a/src/util/ipc.ts +++ b/src/util/ipc.ts @@ -64,8 +64,12 @@ function postConfirmCompleteMission(): void { * Files that take this notification: * - common/Orchestrator */ -function postConnectToVehicle(jsonMessage: Message.JSONMessage, newMessage: boolean): void { - ipcRenderer.send('post', 'connectToVehicle', jsonMessage, newMessage); +function postConnectToVehicle( + jsonMessage: Message.JSONMessage, + newMessage: boolean, + shouldAcknowledge: boolean, +): void { + ipcRenderer.send('post', 'connectToVehicle', jsonMessage, newMessage, shouldAcknowledge); } /** From ce048e1e0adc69cf24ba94e567dd823e17d093aa Mon Sep 17 00:00:00 2001 From: Paul Matthew Barrameda Date: Sat, 4 May 2019 23:40:38 -0700 Subject: [PATCH 152/152] Fixed variable order (version update) + other updates --- src/common/Orchestrator.ts | 2 - src/renderer/App.tsx | 2 +- .../fixtures/updateVehiclesFixtures.ts | 2 +- src/renderer/mainWindow/log/LogContainer.tsx | 10 ++-- src/renderer/mainWindow/map/MapContainer.tsx | 10 ++-- .../mainWindow/map/shape/BoundingBox.tsx | 50 +++++++++---------- .../mainWindow/vehicle/VehicleTable.tsx | 10 ++-- src/static/location.json | 7 ++- src/static/vehicle.json | 4 +- 9 files changed, 50 insertions(+), 47 deletions(-) diff --git a/src/common/Orchestrator.ts b/src/common/Orchestrator.ts index cdcca69f..a51f3651 100644 --- a/src/common/Orchestrator.ts +++ b/src/common/Orchestrator.ts @@ -149,8 +149,6 @@ class Orchestrator { private ping(vehicle: Vehicle): void { const delta = Math.max(0, Date.now() - vehicle.getLastConnectionTime()); - ipc.postLogMessages({ message: `Pinging vehicle with delta = ${delta}` }); - if (delta <= config.vehicleDisconnectionTime * 1000) { // Handler that expires and creates itself everytime it "pings" the vehicle. setTimeout( diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index 17f91e0a..3d7d1ee8 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -25,7 +25,7 @@ export default class App extends Component<{}, State> { super(props); this.state = { - theme: 'dark', + theme: 'light', }; this.toggleTheme = this.toggleTheme.bind(this); diff --git a/src/renderer/fixtures/updateVehiclesFixtures.ts b/src/renderer/fixtures/updateVehiclesFixtures.ts index adafb796..5e14413f 100644 --- a/src/renderer/fixtures/updateVehiclesFixtures.ts +++ b/src/renderer/fixtures/updateVehiclesFixtures.ts @@ -81,7 +81,7 @@ function connectVehicles(): void { ipc.postConnectToVehicle(generateJSONMessage(fixture.sid, { type: 'connect', jobsAvailable: fixture.jobs, - }), true); + }), true, true); }); } diff --git a/src/renderer/mainWindow/log/LogContainer.tsx b/src/renderer/mainWindow/log/LogContainer.tsx index 29aa89f7..4ca0e340 100644 --- a/src/renderer/mainWindow/log/LogContainer.tsx +++ b/src/renderer/mainWindow/log/LogContainer.tsx @@ -61,6 +61,11 @@ export default class LogContainer extends Component {}, 200); + /** + * Timeout that will scroll to bottom when it times out. + */ + private scrollTimer: NodeJS.Timeout; + /** * Cache that stores the height for all log messages. Allows the messages to have proper height. */ @@ -135,11 +140,6 @@ export default class LogContainer extends Component { + /** + * Reference to map (to access its leaflet element). + */ + private ref: React.RefObject; + public constructor(props: ThemeProps) { super(props); @@ -221,11 +226,6 @@ export default class MapContainer extends Component { this.onViewportChanged({ center: [lat, lng], zoom: zoom || viewport.zoom }); } - /** - * Reference to map (to access its leaflet element). - */ - private ref: React.RefObject; - /** * Callback whenever a "loadConfig" notification is received. */ diff --git a/src/renderer/mainWindow/map/shape/BoundingBox.tsx b/src/renderer/mainWindow/map/shape/BoundingBox.tsx index db736070..6c0d1d48 100644 --- a/src/renderer/mainWindow/map/shape/BoundingBox.tsx +++ b/src/renderer/mainWindow/map/shape/BoundingBox.tsx @@ -28,6 +28,31 @@ interface State { } export default class BoundingBox extends PureComponent { + /** + * Reference to marker 1 (to access its leaflet element). + */ + private ref1: React.RefObject; + + /** + * Reference to marker 2 (to access its leaflet element). + */ + private ref2: React.RefObject; + + /** + * Reference to marker 3 (to access its leaflet element). + */ + private ref3: React.RefObject; + + /** + * Reference to marker 4 (to access its leaflet element). + */ + private ref4: React.RefObject; + + /** + * Reference to rectangle (to access its leaflet element). + */ + private ref: React.RefObject; + public constructor(props: BoundingBoxProps) { super(props); @@ -173,31 +198,6 @@ export default class BoundingBox extends PureComponent this.ondrag(); } - /** - * Reference to marker 1 (to access its leaflet element). - */ - private ref1: React.RefObject; - - /** - * Reference to marker 2 (to access its leaflet element). - */ - private ref2: React.RefObject; - - /** - * Reference to marker 3 (to access its leaflet element). - */ - private ref3: React.RefObject; - - /** - * Reference to marker 4 (to access its leaflet element). - */ - private ref4: React.RefObject; - - /** - * Reference to rectangle (to access its leaflet element). - */ - private ref: React.RefObject; - public render(): ReactNode { const { locked, color, startingBounds } = this.props; diff --git a/src/renderer/mainWindow/vehicle/VehicleTable.tsx b/src/renderer/mainWindow/vehicle/VehicleTable.tsx index 36e33966..0316434b 100644 --- a/src/renderer/mainWindow/vehicle/VehicleTable.tsx +++ b/src/renderer/mainWindow/vehicle/VehicleTable.tsx @@ -64,6 +64,11 @@ export default class VehicleTable extends Component { return {vehicleStatus.message}; } + /** + * Width for columns in table. + */ + private width: WidthSignature; + public constructor(props: VehicleTableProps) { super(props); @@ -82,11 +87,6 @@ export default class VehicleTable extends Component { ipc.postCenterMapToVehicle(vehicles[info.index]); } - /** - * Width for columns in table. - */ - private width: WidthSignature; - /** * Callback to obtain data for a certain row in the table. */ diff --git a/src/static/location.json b/src/static/location.json index 5be75db1..552128db 100644 --- a/src/static/location.json +++ b/src/static/location.json @@ -1,5 +1,5 @@ { - "startLocation": "Prado Airpark", + "startLocation": "Educational Flight Range", "locations": { "Cal Poly SLO": { "lat": 35.306205, @@ -15,6 +15,11 @@ "lat": 33.9325635, "lng": -117.6288792, "zoom": 18 + }, + "Educational Flight Range": { + "lat": 35.328571, + "lng": -120.7521664, + "zoom": 18 } } } diff --git a/src/static/vehicle.json b/src/static/vehicle.json index 29f4c70e..b2e8ac34 100644 --- a/src/static/vehicle.json +++ b/src/static/vehicle.json @@ -15,12 +15,12 @@ "type": "station" }, "100": { - "macAddress": "0013A200419477B6", + "macAddress": "0013A2004194783A", "name": "Skywalker", "type": "plane" }, "101": { - "macAddress": "", + "macAddress": "0013A200419477B6", "name": "Piper Cub", "type": "plane" },