diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8ab5c35 --- /dev/null +++ b/.gitignore @@ -0,0 +1,139 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +## Node JS +node_modules/ +package-lock.json + +.DS_Store + +logbooks/ + +.idea/ diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..6a14adf --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,165 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + + This version of the GNU Lesser General Public License incorporates +the terms and conditions of version 3 of the GNU Lesser General Public +License, supplemented by the additional permissions listed below. + + 0. Additional Definitions. + + As used herein, "this License" refers to version 3 of the GNU Lesser +General Public License, and the "GNU GPL" refers to version 3 of the GNU +General Public License. + + "The Library" refers to a covered work governed by this License, +other than an Application or a Combined Work as defined below. + + An "Application" is any work that makes use of an interface provided +by the Library, but which is not otherwise based on the Library. +Defining a subclass of a class defined by the Library is deemed a mode +of using an interface provided by the Library. + + A "Combined Work" is a work produced by combining or linking an +Application with the Library. The particular version of the Library +with which the Combined Work was made is also called the "Linked +Version". + + The "Minimal Corresponding Source" for a Combined Work means the +Corresponding Source for the Combined Work, excluding any source code +for portions of the Combined Work that, considered in isolation, are +based on the Application, and not on the Linked Version. + + The "Corresponding Application Code" for a Combined Work means the +object code and/or source code for the Application, including any data +and utility programs needed for reproducing the Combined Work from the +Application, but excluding the System Libraries of the Combined Work. + + 1. Exception to Section 3 of the GNU GPL. + + You may convey a covered work under sections 3 and 4 of this License +without being bound by section 3 of the GNU GPL. + + 2. Conveying Modified Versions. + + If you modify a copy of the Library, and, in your modifications, a +facility refers to a function or data to be supplied by an Application +that uses the facility (other than as an argument passed when the +facility is invoked), then you may convey a copy of the modified +version: + + a) under this License, provided that you make a good faith effort to + ensure that, in the event an Application does not supply the + function or data, the facility still operates, and performs + whatever part of its purpose remains meaningful, or + + b) under the GNU GPL, with none of the additional permissions of + this License applicable to that copy. + + 3. Object Code Incorporating Material from Library Header Files. + + The object code form of an Application may incorporate material from +a header file that is part of the Library. You may convey such object +code under terms of your choice, provided that, if the incorporated +material is not limited to numerical parameters, data structure +layouts and accessors, or small macros, inline functions and templates +(ten or fewer lines in length), you do both of the following: + + a) Give prominent notice with each copy of the object code that the + Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the object code with a copy of the GNU GPL and this license + document. + + 4. Combined Works. + + You may convey a Combined Work under terms of your choice that, +taken together, effectively do not restrict modification of the +portions of the Library contained in the Combined Work and reverse +engineering for debugging such modifications, if you also do each of +the following: + + a) Give prominent notice with each copy of the Combined Work that + the Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the Combined Work with a copy of the GNU GPL and this license + document. + + c) For a Combined Work that displays copyright notices during + execution, include the copyright notice for the Library among + these notices, as well as a reference directing the user to the + copies of the GNU GPL and this license document. + + d) Do one of the following: + + 0) Convey the Minimal Corresponding Source under the terms of this + License, and the Corresponding Application Code in a form + suitable for, and under terms that permit, the user to + recombine or relink the Application with a modified version of + the Linked Version to produce a modified Combined Work, in the + manner specified by section 6 of the GNU GPL for conveying + Corresponding Source. + + 1) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (a) uses at run time + a copy of the Library already present on the user's computer + system, and (b) will operate properly with a modified version + of the Library that is interface-compatible with the Linked + Version. + + e) Provide Installation Information, but only if you would otherwise + be required to provide such information under section 6 of the + GNU GPL, and only to the extent that such information is + necessary to install and execute a modified version of the + Combined Work produced by recombining or relinking the + Application with a modified version of the Linked Version. (If + you use option 4d0, the Installation Information must accompany + the Minimal Corresponding Source and Corresponding Application + Code. If you use option 4d1, you must provide the Installation + Information in the manner specified by section 6 of the GNU GPL + for conveying Corresponding Source.) + + 5. Combined Libraries. + + You may place library facilities that are a work based on the +Library side by side in a single library together with other library +facilities that are not Applications and are not covered by this +License, and convey such a combined library under terms of your +choice, if you do both of the following: + + a) Accompany the combined library with a copy of the same work based + on the Library, uncombined with any other library facilities, + conveyed under the terms of this License. + + b) Give prominent notice with the combined library that part of it + is a work based on the Library, and explaining where to find the + accompanying uncombined form of the same work. + + 6. Revised Versions of the GNU Lesser General Public License. + + The Free Software Foundation may publish revised and/or new versions +of the GNU Lesser General Public License from time to time. Such new +versions will be similar in spirit to the present version, but may +differ in detail to address new problems or concerns. + + Each version is given a distinguishing version number. If the +Library as you received it specifies that a certain numbered version +of the GNU Lesser General Public License "or any later version" +applies to it, you have the option of following the terms and +conditions either of that published version or of any later version +published by the Free Software Foundation. If the Library as you +received it does not specify a version number of the GNU Lesser +General Public License, you may choose any version of the GNU Lesser +General Public License ever published by the Free Software Foundation. + + If the Library as you received it specifies that a proxy can decide +whether future versions of the GNU Lesser General Public License shall +apply, that proxy's public statement of acceptance of any version is +permanent authorization for you to choose that version for the +Library. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..ff311b3 --- /dev/null +++ b/README.md @@ -0,0 +1,117 @@ +# Swissloop Control Panel • ![Version 3.0.0](https://badgen.net/badge/Version/v3.0.0?color=blue) ![License LGPL-3.0](https://badgen.net/badge/License/LGPL-3.0) + +The control panel is a control and visualization software for the telemetry data of Swissloop prototypes. The control panel is implemented as an [Electron](https://www.electronjs.org/) application with [Node.js](https://nodejs.org/en/) as it provides modularity, flexibility, and is easy to use. + +**The most recent version is 3.0.0, developed during the Swissloop Season 2021/2022.** + +
+Table of Content + +- [Swissloop Control Panel • ](#swissloop-control-panel---) + - [Disclaimer](#disclaimer) + - [About The Project](#about-the-project) + - [Getting Started](#getting-started) + - [Misc](#misc) + - [Prerequisites](#prerequisites) + - [Installation](#installation) + - [Development](#development) + - [Technical Documentation](#technical-documentation) + - [Maintainer](#maintainer) + - [Additional Contributors](#additional-contributors) + - [Screenshot](#screenshot) +
+ +## Disclaimer +**Even though this software is open-source, and you are free to adapt everything to your need, we very much appreciate the credit to the original authors in the GUI. Hence, we kindly request you to not remove the *"Based on Swissloop Software"* comments in the code and GUI.** + +**Furthermore, we would like to encourage the open exchange of ideas and technologies with the whole Hyperloop community and ask you to join us by making part of your solution to the problems publicly available.** + +This software is by no means perfect and might still include some bugs or suboptimal code. Also, this software was developed specifically for our prototypes and should therefore be considered an implementation example rather than a finished framework. If you want to share some useful ideas and specific implementation details, don't hesitate to contact us. **However, we do not have the intention nor the resources to provide any support!** + +## About The Project +The control panel is used to display the telemetry data of the pod, configure parameters and display logged data. +It provides a clear illustration of all relevant data with different approaches. +This includes a graphical representation of the prototype, overlaid with colors indicating the states of the subsystems and the most important values. +Further, a color-coded table is used to quickly spot non-nominal values. +Additionally, the error module, displaying all error messages, proved very helpful during testing and drastically increases efficiency. +Finally, the standalone mock server can be used to emulate packets from a virtual prototype to develop and test new features in the control panel. + +## Getting Started + +### Misc +* The control panel assumes statically assigned IP addresses and fixed ports. The configuration is specified in `control_panel/config/config.js`. +* **You can emit an emergency at all time by quickly hitting 2x Space!** + +### Prerequisites + +*Please update node to the newest version.* Please refer to https://nodejs.org/en/ + +For Arch base Linux systems run: +```bash +pacman -S nodejs npm +``` +On Mac run: +```bash +brew install node +``` +On Ubuntu run: +```sh +sudo apt update +sudo apt install nodejs npm +``` + +### Installation +Download the repository, install all required dependencies and start the application. +```bash +git clone git@github.com:swissloop/ControlPanel.git +cd ControlPanel +npm install +npm start +``` + +If required, you can automatically download the latest telemetry frame definitions from a GitHub repository and generate the parser for the binary stream. + +```bash +npm run generate_parser +``` + +For this you need to configure API access to your GitHub repository containing the frame definition in `control_panel/config/config.js`. + +* `telemetry_frame_src`: URL to network_telemetry_frame.h in the form of https://api.github.com/repos/{owner}/{repo}/contents/{path} + +* `github_access_token`: Access token (fine-grained personal access tokens) + +## Development + +For testing without the VCU board, a mock server is included in the `mock_server` folder. +To use the mock server, change the "udp_host_send" field in `control_panel/config/config.js` to `127.0.0.1` + +Run the following command to start the mock server. + +```bash +npm run mock +``` + +There is also the possibility to generate and display random packets. This can be configured in `control_panel/config/config`. + +Most of the time, it is also helpful to enable the *Developer Tools*. For this, go *Viev* ► *Toggle Developer Tools* in the application toolbar. Alternatively press *Ctrl+Shift+I*. + +### Technical Documentation +For the technical documentation, please refer to [Technical.md](TECHNICAL.md). + +## Maintainer +**Philip Wiese** (ETHZ ETIT, Main Contributor) + *[philip.wiese@swissloop.ch](mailto:philip.wiese@swissloop.ch)* - [Xeratec](https://github.com/Xeratec) + +### Additional Contributors +**Hanno Hiss** (ETHZ ETIT, Season 21/22) + *[hanno.hiss@swissloop.ch](mailto:hanno.hiss@swissloop.ch)* - [hannohiss](https://github.com/hannohiss) + +**Roger Barton** (ETHZ INF, Season 21/22) + *[roger.barton@swissloop.ch](mailto:roger.barton@swissloop.ch)* - [rogerbarton](https://github.com/rogerbarton) + +**Yvan Bosshard** (ETHZ ETIT, Season 20/21) + *[yvan.bosshard@swissloop.ch](mailto:yvan.bosshard@swissloop.ch)* - [yvanbo](https://github.com/yvanbo) + +## Screenshot +![Screenshot](screenshot.png) \ No newline at end of file diff --git a/TECHNICAL.md b/TECHNICAL.md new file mode 100644 index 0000000..5991e22 --- /dev/null +++ b/TECHNICAL.md @@ -0,0 +1,48 @@ + +# Technical Documentation + +## Folder Structure +The main application is separated into config, CSS (Cascading Style Sheets), images, JavaScript, json objects, and riot-tags files. +```bash +. +├── control_panel # Main Application +│ ├── config # - Configuration files +│ ├── css # - CSS for the layout, colors, and fonts +│ ├── html # - HTML for the content and structure +│ ├── img # - Various images including pod visualization +│ ├── js # - JavaScript Code +│ ├── json # - Definitions such as states and errors +│ ├── mp3 # - Sound files +│ ├── riot-tags # - Riot tags for custom widgets +│ └── app.js # - Main application entry script +├── mock_server # Mock server emulating the pod +│ ├── config # - Configuration files +│ ├── css # - CSS for the layout, colors, and fonts +│ ├── js # - JavaScript Code +│ ├── json # - Definitions such as default values +│ ├── mockserver.html # - Mockserver HTML file +│ └── mockserver.js # - Mockserver entry point +├── package.json # +├── README.md +├── TECHNICAL.md +└── ... +``` + +**Before changing anything in the code make sure to update update `control_panel/config/mappings.js`!** +The mappings are separated into enumerations and errors. An enumerations takes only one values and describes states of FSMs as well as status signals or configuration modes. On the other side, errors are bitmask and can thus take on multiple values by combining the values with an bitwise AND operator. + +## Main Application +To parse and generate the binary data the [jBinary](https://github.com/jDataView/jBinary) library from jDataView was used. It allows the definition of so-called typesets, which describe the structure of the binary data and allow easy conversion and data handling. +The typeset of the telemetry frame is automatically generated from the `network_telemetry_frame.h` file header of the Vehicle Control Software with the `generator.js` script. However, the typeset of the control frame is hardcoded as it is easy to adopt. + +The `com.js` and `parser.js` modules receive and generate data and parse this data from a binary stream to a jBinary object and vice-versa. After receiving a complete frame, the `parser.js` module emits a telemetryFrame, which then gets processed and updated by the GUI modules. The `utils.js` contains utility functions as creating testing data. + +**The application is separated into a main, renderer & communication process ([Electron Process Model](https://www.electronjs.org/docs/latest/tutorial/process-model)).** +The renderer process is responsible for the visible window while the communication process is hidden and is responsible for receiving, parsing, and sending data to the VCU (Vehicle Control Unit). The code of the renderer process is located in the `control_panel/pod_gui` folder, while the code of the communication process in the `control_panel/communication` folder. The GUI is separated into several smaller submodules which are imported in the `control_panel/js/pod_gui/renderer.js` module. + +## Mock-Up Server +The mock server can be started by running +```bash +npm run mock +``` +The mock server as well as a control and telemetry frame for development purposes are located inside the `mock_server` folder. The folder structure is organized the same way as the main application. There is also a config folder where you can specify the log level, IP ports & addresses, and the frequency of the heartbeat. The css, js and json folders are self-explanatory. \ No newline at end of file diff --git a/control_panel/app.js b/control_panel/app.js new file mode 100644 index 0000000..3b8941d --- /dev/null +++ b/control_panel/app.js @@ -0,0 +1,232 @@ +/** + * @file app.js + * @brief Visualization of telemetry data for the Swissloop Pod + * + * @author Philip Wiese, philip.wiese@swissloop.ch + * + * @license + * Copyright (C) 2022 Swissloop + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + */ + +const electron = require('electron'); +// Module to control application life. +const app = electron.app; +// attach debugger command line port +app.commandLine.appendSwitch('remote-debugging-port', '9222') +// Module to create native browser window. +const BrowserWindow = electron.BrowserWindow; + +const path = require('path'); +const url = require('url'); + +const { ipcMain } = require('electron'); + +require('@electron/remote/main').initialize() + +const config = require('./config/config'); + +if (process.env.DEV === 'true') { + console.log("Dev Mode") + require('electron-reload')(__dirname, { + electron: path.join(__dirname, '../', 'node_modules', '.bin', 'electron'), + hardResetMethod: 'exit' + }); +} + +// Keep a global reference of the window object, if you don't, the window will +// be closed automatically when the JavaScript object is garbage collected. +let podControlWindow, logViewerWindow, communicationWorker; + +function createWindow() { + // Prevent from + app.commandLine.appendSwitch("disable-background-timer-throttling") + // Prevents Chromium from lowering the priority of invisible pages' renderer processes. + app.commandLine.appendSwitch("disable-renderer-backgrounding"); + + // Create the browser window. + communicationWorker = new BrowserWindow({ + show: false, + webPreferences: { + nodeIntegration: true, + enableRemoteModule: true, + contextIsolation: false, + webSecurity: false, + allowRunningInsecureContent: true + }, + parent: podControlWindow, + backgroundThrottling: false + }); + + podControlWindow = new BrowserWindow({ + width: 1440, + height: 840, + enableRemoteModule: true, + webPreferences: { + nodeIntegration: true, + contextIsolation: false, + webSecurity: false, + allowRunningInsecureContent: true, + enableRemoteModule: true, + }, + backgroundThrottling: false + }); + + + require('@electron/remote/main').enable(podControlWindow.webContents) + require('@electron/remote/main').enable(communicationWorker.webContents) + + podControlWindow.setIcon(path.join(__dirname, 'img', 'Icon.png')); + + if (process.env.DEV === 'true') { + // podControlWindow.openDevTools(); + communicationWorker.openDevTools(); + } + + podControlWindow.loadURL(url.format({ + pathname: path.join(__dirname, 'html/podControl.html'), + protocol: 'file:', + slashes: true, + })); + + communicationWorker.loadURL(url.format({ + pathname: path.join(__dirname, 'html/comWorker.html'), + protocol: 'file:', + slashes: true + })); + + podControlWindow.maximize(); + + // Emitted when the window is closed. + podControlWindow.on('closed', function () { + // Dereference the window object, usually you would store windows + // in an array if your app supports multi windows, this is the time + // when you should delete the corresponding element. + podControlWindow = null; + communicationWorker = null; + app.quit(); + }); + + if (config.testing.enabled) console.log("[INFO] Testing enabled"); + + ipcMain.on('startLogging', (_event, arg) => { + sendWindowMessage(communicationWorker, 'startLogging', arg); + }); + ipcMain.on('stopLogging', (_event, arg) => { + sendWindowMessage(communicationWorker, 'stopLogging', arg); + }); + ipcMain.on('sendCtrlFrame', (_event, arg) => { + sendWindowMessage(communicationWorker, 'sendCtrlFrame', arg); + }); + ipcMain.on('console_log', (_event, ...arg) => { + // console.log(...arg); + sendWindowMessage(podControlWindow, 'console_log', arg); + }); + ipcMain.on('error', (_event, arg) => { + sendWindowMessage(podControlWindow, 'error', arg); + }); + ipcMain.on('connected', (_event, arg) => { + sendWindowMessage(podControlWindow, 'connected', arg); + }); + ipcMain.on('missingHeartbeat', (_event, arg) => { + sendWindowMessage(podControlWindow, 'missingHeartbeat', arg); + }); + ipcMain.on('heartbeat', (_event, arg) => { + sendWindowMessage(podControlWindow, 'heartbeat', arg); + }); + ipcMain.on('packet', (_event, arg) => { + sendWindowMessage(podControlWindow, 'packet', arg); + }); + ipcMain.on('telemetryFrame', (_event, arg) => { + sendWindowMessage(podControlWindow, 'telemetryFrame', arg); + }); + ipcMain.on('openLogViewer', (_event, _arg) => { + if (logViewerWindow) { + logViewerWindow.show(); + } else { + createLogViewer(); + } + }); + + ipcMain.setMaxListeners(Infinity); +} + +function createLogViewer() { + logViewerWindow = new BrowserWindow({ + show: false, + webPreferences: { + nodeIntegration: true, + enableRemoteModule: true, + contextIsolation: false, + webSecurity: false, + allowRunningInsecureContent: true + }, + backgroundThrottling: true + }); + + require('@electron/remote/main').enable(logViewerWindow.webContents) + + + logViewerWindow.setIcon(path.join(__dirname, 'img', 'Icon.png')); + + logViewerWindow.loadURL(url.format({ + pathname: path.join(__dirname, 'html/logViewer.html'), + protocol: 'file:', + slashes: true, + })); + + logViewerWindow.on('closed', function () { + // Dereference the window object, usually you would store windows + // in an array if your app supports multi windows, this is the time + // when you should delete the corresponding element. + logViewerWindow = null; + }); + + logViewerWindow.maximize(); + + logViewerWindow.show(); +} + +// This method will be called when Electron has finished +// initialization and is ready to create browser windows. +// Some APIs can only be used after this event occurs. +app.on('ready', createWindow); + +// Quit when all windows are closed. +app.on('window-all-closed', function () { + if (process.platform != 'darwin') { + console.log("All Windows Closed") + app.quit(); + } +}); + +app.on('activate', function () { + // On macOS it's common to re-create a window in the app when the + // dock icon is clicked and there are no other windows open. + if (BrowserWindow.getAllWindows().length === 0) createWindow() +}); + +function sendWindowMessage(targetWindow, message, payload) { + if (typeof targetWindow === 'undefined') { + console.log('Target window does not exist'); + return; + } + if (targetWindow === null) { + console.log('Target window does not exist'); + return; + } + targetWindow.webContents.send(message, payload); +} diff --git a/control_panel/config/config.js b/control_panel/config/config.js new file mode 100644 index 0000000..8a7f79a --- /dev/null +++ b/control_panel/config/config.js @@ -0,0 +1,43 @@ +const config = { + "verbosity": 6, + "communication": { + "udp_port_listen" : 1338, + "udp_host_send" : "192.168.1.6", + "udp_port_send" : 1337, + "heartbeat_freq" : 200, + "heartbeat_timeout" : 500 + }, + "logging": { + "start_automatic": true, + "start_state": "Setup", + "start_delay": 0, + "stop_automatic": true, + "stop_state": "Idle", + "stop_delay": 1000 + }, + "battery": { + "lv_capacity": 6300 + }, + "github": { + "telemetry_frame_src": "https://api.github.com/repos/{owner}/{repo}/contents/{path}", + "github_access_token": "", + }, + "music": { + "waiting": true, + "onConnection": true, + "onSuccess": true, + "onEmergency": true + }, + "testing": { + "enabled" : false, + "interval": 400, + "random" : true + }, + "logviewer": { + "max_states": 9, + "reduce_series": false, + "padding-left": 130 + } +} + +module.exports = config diff --git a/control_panel/config/mappings.js b/control_panel/config/mappings.js new file mode 100644 index 0000000..cec978a --- /dev/null +++ b/control_panel/config/mappings.js @@ -0,0 +1,674 @@ +/** + * @file mappings.js + * @brief State and error mappings + * + * @author Philip Wiese, philip.wiese@swissloop.ch + * + * @license + * Copyright (C) 2022 Swissloop + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * @module Mappings + * @version 3.0.0 + * + */ + +class Mappings { + + /**************************************************************************/ + /************************ IMPORT MAPPINGS ********************************/ + /**************************************************************************/ + static typeset_telemetry_frame = require('../js/typesets/typeset_telemetry_frame.js').typeset; + static typeset_ctrl_frame = require('../js/typesets/typeset_ctrl_frame.js').typeset; + + // Errors + static main_error = require('../json/main_errors.json'); + static main_emergencies = require('../json/main_emergencies.json'); + static icu_error = require('../json/icu_mcu_errors.json'); + static bms_error = require('../json/bms_errors.json'); + static fpga_status = require('../json/fpga_status_bits.json'); + static gatedriver_status = require('../json/gatedriver.json'); + static bms_sys_fault = require('../json/bms_sys_fault.json'); + static bms_dev_fault = require('../json/bms_dev_fault.json'); + static bms_com_fault = require('../json/bms_com_fault.json'); + static bms_uv_ov_fault = require('../json/bms_uv_ov_fault.json'); + static bms_pl455_fault_summary = require('../json/bms_pl455_fault_summary.json'); + + // States (Enumeration) + static main_state = require('../json/main_state.json'); + static brake_state = require('../json/brake_state.json'); + static icu_state = require('../json/icu_state.json'); + static fpga_state = require('../json/fpga_state.json'); + static bms_state = require('../json/bms_state.json'); + + // Other Enumerations + static brake_status = require('../json/brake_status.json'); + static run_types = require('../json/run_modes.json'); + static true_false = require('../json/true_false.json'); + static pwm_Methods = require('../json/fpga_control_method.json'); + static fpga_control_status = require('../json/fpga_control_status.json'); + + /**************************************************************************/ + /**************************************************************************/ + /**************************************************************************/ + + + /**************************************************************************/ + /************************ GENERAL MAPPINGS *******************************/ + /** **/ + /** Include error and enumerations (FSM, Status & Modes) **/ + /** **/ + /**************************************************************************/ + + /** + * @typedef {object} Error + * @property {string} path - Path in TelemetryFrame. + * @property {string} name - Textual name description. + * @property {object} enum - Reference to enumeration object + */ + + /** + * @brief Array with all errors. + * @type Error[] + * + * @note + * Errors are bitmask and can thus take on multiple values by combining the values with an bitwise AND operator. + */ + static errors_list = [ + { + path: "State.VCU_EMERGENCY_REASON", + name: "VCU Emergency", + enum: this.main_emergencies + }, { + path: "State.VCU_ERRORS", + name: "VCU Error", + enum: this.main_error + }, { + path: "Inverter.ICUFPGASTATUS", + name: "FPGA Error", + enum: this.fpga_status + }, { + path: "Inverter.ICUMCUSTATUS", + name: "ICU Error", + enum: this.icu_error + }, { + path: "Inverter.GD_STATUS", + name: "GD Status", + enum: this.gatedriver_status + }, { + path: "HV_Left.HV_L_ERROR", + name: "HV Error (Left)", + enum: this.bms_error + }, { + path: "HV_Right.HV_R_ERROR", + name: "HV Error (Right)", + enum: this.bms_error + }, + ]; + + /** + * @typedef {object} Enumeration + * @property {string} path - Path in TelemetryFrame. + * @property {string} name - Textual name description. + * @property {object} enum - Reference to enumeration object + * @property {"FSM"?} type - Type of enumeration (FSM) + * @property {string?} ready - Corresponding ready signal for FSM enums. + * @property {number?} index - Index of array. + */ + + /** + * @brief Array with all enumerations. + * @type Enumeration[] + * + * @note + * Enumerations only take one value at the time! + */ + static enum_list = [ + { + path: "State.STATE", + name: "VCU State", + enum: this.main_state, + "type": "FSM", + }, { + path: "Inverter.ICUMCUSTATE", + name: "ICU State", + enum: this.icu_state, + "type": "FSM", + "ready": "Inverter.READY" + }, { + path: "Inverter.ICUFPGASTATE", + name: "FPGA State", + enum: this.fpga_state, + "type": "FSM", + "ready": "Inverter.ICUFPGAREADY" + }, { + path: "HV_Left.HV_L_STATE", + name: "HV State (Left)", + enum: this.bms_state, + "type": "FSM", + "ready": "HV_Left.HV_L_READY" + }, { + path: "HV_Right.HV_R_STATE", + name: "HV State (Right)", + enum: this.bms_state, + "type": "FSM", + "ready": "HV_Right.HV_R_READY" + }, { + path: "Brake.BRAKE_STATE", + name: "Brake Mode", + enum: this.brake_state, + }, { + path: "Brake.BRAKE_ENGAGED", + name: "Brake State", + enum: this.brake_status, + }, { + path: "FPGA.FPGA_CURRENT_STATUS", + name: "FPGA Control (Board 0)", + enum: this.fpga_control_status, + index: 0 + }, { + path: "FPGA.FPGA_CURRENT_STATUS", + name: "FPGA Control (Board 1)", + enum: this.fpga_control_status, + index: 1 + }, { + path: "FPGA.FPGA_CURRENT_STATUS", + name: "FPGA Control (Board 2)", + enum: this.fpga_control_status, + index: 2 + }, { + path: "FPGA.FPGA_CURRENT_STATUS", + name: "FPGA Control (Board 3)", + enum: this.fpga_control_status, + index: 3 + }, { + path: "FPGA.FPGA_CURRENT_STATUS", + name: "FPGA Control (Board 4)", + enum: this.fpga_control_status, + index: 4 + }, { + path: "FPGA.FPGA_CURRENT_STATUS", + name: "FPGA Control (Board 5)", + enum: this.fpga_control_status, + index: 5 + }, { + path: "FPGA.FPGA_CURRENT_STATUS", + name: "FPGA Control (Board 6)", + enum: this.fpga_control_status, + index: 6 + }, { + path: "FPGA.FPGA_CURRENT_STATUS", + name: "FPGA Control (Board 7)", + enum: this.fpga_control_status, + index: 7 + }, + ]; + + static typeset_list = [ + this.typeset_telemetry_frame, + this.typeset_ctrl_frame, + this.typeset_inverter_frame + ]; + + /**************************************************************************/ + /**************************************************************************/ + /**************************************************************************/ + + + /**************************************************************************/ + /************************ SPECIFIC MAPPINGS ******************************/ + /** **/ + /** Includes configuration for: **/ + /** - Protocol Table (displaying important values in table format) **/ + /** - Configuration Section (configure the pod/run parameter) **/ + /** **/ + /**************************************************************************/ + + // Content of Protocol Table + static protocol = [ + { + alias: "Run Timer", + path: "State.RUN_TIMER", + min: 0, + max: 65535, + unit: "ms", + precision: 0 + }, { + alias: "LV BAT1 Voltage", + path: "LV_Batteries.LV_BAT1_VOLTAGE", + min: 22, + max: 29.4, + unit: "V", + precision: 2 + }, { + alias: "LV BAT2 Voltage", + path: "LV_Batteries.LV_BAT2_VOLTAGE", + min: 22, + max: 29.4, + unit: "V", + precision: 2 + }, { + alias: "LV BAT1 Current", + path: "LV_Batteries.LV_BAT1_CURRENT", + min: 0.2, + max: 10, + unit: "A", + precision: 2 + }, { + alias: "LV BAT2 Current", + path: "LV_Batteries.LV_BAT2_CURRENT", + min: 0.2, + max: 10, + unit: "A", + precision: 2 + }, { + alias: "HV Isolation", + path: "HV_Batteries.HV_ISOLATION", + min: 1, + max: 1, + unit: "-", + precision: 0 + }, { + alias: "HV Left Current", + path: "HV_Left.HV_L_CURRENT", + min: -150, + max: 350, + unit: "A", + precision: 0 + }, { + alias: "HV Left Voltage", + path: "HV_Left.HV_L_VOLTAGE", + min: 236, + max: 268, + unit: "V", + precision: 1 + }, { + alias: "HV Left Min Cell Voltage", + path: "HV_Left.HV_L_MIN_CELL_VOLTAGE", + min: 2.5, + max: 5, + unit: "V", + precision: 2 + }, { + alias: "HV Left Max Cell Voltage", + path: "HV_Left.HV_L_MAX_CELL_VOLTAGE", + min: 2.5, + max: 5, + unit: "V", + precision: 2 + }, { + alias: "HV Left Max Cell Temp", + path: "HV_Left.HV_L_MAX_CELL_TEMP", + min: 10, + max: 60, + unit: "°C", + precision: 0 + }, { + alias: "HV Right Current", + path: "HV_Right.HV_R_CURRENT", + min: -150, + max: 350, + unit: "A", + precision: 0 + }, { + alias: "HV Right Voltage", + path: "HV_Right.HV_R_VOLTAGE", + min: 236, + max: 268, + unit: "V", + precision: 1 + }, { + alias: "HV Right Min Cell Voltage", + path: "HV_Right.HV_R_MIN_CELL_VOLTAGE", + min: 2.5, + max: 5, + unit: "V", + precision: 2 + }, { + alias: "HV Right Max Cell Voltage", + path: "HV_Right.HV_R_MAX_CELL_VOLTAGE", + min: 2.5, + max: 5, + unit: "V", + precision: 2 + }, { + alias: "HV Right Max Cell Temp.", + path: "HV_Right.HV_R_MAX_CELL_TEMP", + min: 10, + max: 60, + unit: "°C", + precision: 0 + }, { + alias: "Brake Tank Pressure", + path: "Brake.BRAKE_TANK", + min: 30, + max: 250, + unit: "bar", + precision: 0 + }, { + alias: "Brake Left Action", + path: "Brake.BRAKE_LEFT_ACTION", + min: 4, + max: 10, + unit: "bar", + precision: 3 + }, { + alias: "Brake Left Release", + path: "Brake.BRAKE_LEFT_RELEASE", + min: 4, + max: 10, + unit: "bar", + precision: 3 + }, { + alias: "Brake Right Action", + path: "Brake.BRAKE_RIGHT_ACTION", + min: 4, + max: 10, + unit: "bar", + precision: 3 + }, { + alias: "Brake Right Release", + path: "Brake.BRAKE_RIGHT_RELEASE", + min: 4, + max: 10, + unit: "bar", + precision: 3 + }, { + alias: "Brake Pressure Regulator Out", + path: "Brake.BRAKE_PRESSUREREGULATORIN", + min: 0, + max: 10, + unit: "bar", + precision: 3 + }, { + alias: "Max. Motor Temp (PT100)", + path: "Motor_Temperatures.TEMP_PT100_MAX", + min: 0, + max: 80, + unit: "°C", + precision: 1 + }, { + alias: "Max. Motor Trip (PTC)", + path: "Motor_Temperatures.TEMP_PTC_MAX", + min: 0, + max: 1, + unit: "kOhm", + precision: 2 + }, { + alias: "Max. Inverter Temp", + path: "Inverter.MAX_TEMP", + min: 0, + max: 80, + unit: "°C", + precision: 1 + }, + ]; + + /** + * @typedef {object} ConfigEntry + * @property {string} path_telemetry - Path in TelemetryFrame. + * @property {string} path - Path in CtrlFrame. + * @property {string} name - Name visible in table. + * @property {"header" | "number" | "enum"} type - Type of variable (header, number or enum). + * @property {string} modal - HTML ID in run approval dialog + * @property {object} enum - Reference to enumeration object + */ + + /** @type ConfigEntry[] */ + static config_values = [ + { + path_telemetry: "Configuration.CONFIG_RUNTYPE", + path: "config_runType", + name: "Run Type", + type: "enum", + modal: "run_modal_runtype", + enum: this.run_types + }, { + path_telemetry: "Configuration.CONFIG_PWMMETHOD", + path: "config_pwmMethod", + name: "PWM Generation Method", + type: "enum", + modal: "run_modal_pwmMethod", + enum: this.pwm_Methods + }, { + path_telemetry: "Configuration.CONFIG_RUNFORWARD", + path: "config_runForward", + name: "Run Forward", + type: "enum", + modal: "run_modal_runforward", + enum: this.true_false + }, { + path_telemetry: "Configuration.CONFIG_RUNDURATION", + path: "config_runDuration", + name: "Run Duration", + type: "decimal", + modal: "run_modal_runduration" + }, { + name: "Inverter - Current", + type: "header" + }, { + path_telemetry: "Configuration.CONFIG_BANDWIDTHLOW", + path: "config_bandwidthLow", + name: "Current Bandwidth Low", + type: "number", + modal: "run_modal_currentbandwidthlow", + }, { + path_telemetry: "Configuration.CONFIG_BANDWIDTHHIGH", + path: "config_bandwidthHigh", + name: "Current Bandwidth High", + type: "number", + modal: "run_modal_currentbandwidthhigh", + }, { + path_telemetry: "Configuration.CONFIG_TARGETCURRENT", + path: "config_targetCurrent", + name: "Target Current", + type: "number", + modal: "run_modal_targetcurrent", + }, { + path_telemetry: "Configuration.CONFIG_ZEROCURRENT", + path: "config_zeroCurrent", + name: "Zero Current", + type: "number", + modal: "run_modal_zerocurrent", + }, { + path_telemetry: "Configuration.CONFIG_OVERCURRENTLIMIT_LO", + path: "config_overCurrentLimit_lo", + name: "Over Current Limit Lo", + type: "number", + modal: "run_modal_overcurrentlimitlo", + }, { + path_telemetry: "Configuration.CONFIG_OVERCURRENTLIMIT_HI", + path: "config_overCurrentLimit_hi", + name: "Over Current Limit Hi", + type: "number", + modal: "run_modal_overcurrentlimithi", + }, { + path_telemetry: "Configuration.CONFIG_ADC_MAXOFFSET", + path: "config_ADC_MaxOffset", + name: "ADC Max Offset", + type: "number", + modal: "run_modal_adc_maxoffset", + }, { + path_telemetry: "Configuration.CONFIG_ADC_MAXNOISERANGE", + path: "config_ADC_MaxNoiseRange", + name: "ADC Max Noise Range", + type: "number", + modal: "run_modal_adc_maxnoiserange", + }, { + path_telemetry: "Configuration.CONFIG_NUMINVERTERSENABLED", + path: "config_NumInvertersEnabled", + name: "Number of Inverters enabled", + type: "number", + modal: "run_modal_numInverters", + }, { + name: "Track", + type: "header" + }, { + path_telemetry: "Configuration.CONFIG_SETPOSITION", + path: "config_setPosition", + name: "Set Position", + type: "number", + unit: "Teeth", + modal: "run_modal_setposition", + }, { + path_telemetry: "Configuration.CONFIG_RUNLENGTH", + path: "config_runLength", + name: "Run Length", + type: "number", + unit: "Teeth", + modal: "run_modal_runlength" + }, { + path_telemetry: "Configuration.CONFIG_RIDELENGTH", + path: "config_rideLength", + name: "Ride Length", + type: "number", + modal: "run_modal_ridelength" + }, { + path_telemetry: "Configuration.CONFIG_TRACKLENGTH", + path: "config_trackLength", + name: "Track Length", + type: "number", + modal: "run_modal_tracklength" + }, { + path_telemetry: "Configuration.CONFIG_PROPTRACKLENGTH", + path: "config_propTrackLength", + name: "Propulsion Track Length", + type: "number", + unit: "Teeth", + modal: "run_modal_proptracklength" + }, { + name: "Inverter - Temperature/Voltage", + type: "header" + }, { + path_telemetry: "Configuration.CONFIG_MAXVOLTAGE", + path: "config_maxVoltage", + name: "Max Voltage", + type: "number", + modal: "run_modal_maxvoltage", + }, { + path_telemetry: "Configuration.CONFIG_TEMP2STARTFAN", + path: "config_Temp2startFan", + name: "Temperature to start fan", + type: "number", + unit: "°C", + modal: "run_modal_Temp2startFan" + }, { + path_telemetry: "Configuration.CONFIG_MAXMOSFETTEMP", + path: "config_maxMosfetTemp", + name: "Maximum Mosfet Temperature", + type: "number", + unit: "°C", + modal: "run_modal_maxMosfetTemp" + }, { + name: "VCU - Braking", + type: "header" + }, { + path_telemetry: "Brake.BRAKE_STATE", + path: "config_brake_state", + name: "Brake State", + type: "enum", + modal: "run_modal_brakestate", + enum: this.brake_state + }, { + path_telemetry: "Brake.BRAKE_CONTROLLEDBRAKINGPRESSURE", + path: "config_controlled_Braking_pressure", + name: "Controlled Braking Pressure", + type: "number", + modal: "run_modal_controlledbrakingpressure" + }, { + name: "Battery", + type: "header" + }, { + path_telemetry: "HV_Batteries.NUMBER_OF_BATTERIES", + path: "config_number_of_batteries", + name: "Number of Batteries per side", + type: "number", + modal: "run_modal_num_of_batteries" + } + ]; + + /**************************************************************************/ + /**************************************************************************/ + /**************************************************************************/ + + + /**************************************************************************/ + /************************ UTILITY FUNCTIONS *******************************/ + /**************************************************************************/ + + /** + * Returns string descriptor of an enumerated value + * + * @param enumeration JSON with enumeration + * @param value Value or flag of enumeration + * @returns {object} Textual description + */ + static select_flag(enumeration, value) { + if (enumeration === undefined) return value; + let length = enumeration.length; + + for (let i = 0; i < length; i++) { + if (value === enumeration[i].flag) { + return enumeration[i].description; + } + } + + return "Unknown"; + } + + /** + * Returns JSON enumeration of a path + * + * @param list Array with mappings. + * @param path Value of enumeration + * @returns {object} JSON enumeration. + */ + static select_enum(list, path) { + if (list === undefined) return undefined; + let length = list.length; + + for (let i = 0; i < length; i++) { + if (path === list[i].path) { + return list[i].enum; + } + } + return undefined; + } + + static select_mapping(list, path) { + if (list === undefined) return undefined; + let length = list.length; + + for (let i = 0; i < length; i++) { + if (path === list[i].path) { + return list[i]; + } + } + return undefined; + } + + /**************************************************************************/ + /**************************************************************************/ + /**************************************************************************/ +} + +module.exports = Mappings; + +// Example: +// const mappings = require('./config/mappings.js'); +// const enum = mappings.select_enum(mappings.state_list, "State.STATE") +// const desc = mappings.select_flag(enum, 1) +// console.log(enum) +// console.log(desc) diff --git a/control_panel/css/app.css b/control_panel/css/app.css new file mode 100644 index 0000000..756ef75 --- /dev/null +++ b/control_panel/css/app.css @@ -0,0 +1,218 @@ +/** + * @file app.css + * @brief Cascading Style Sheets for main application + * + * @author Philip Wiese, philip.wiese@swissloop.ch + * + * @license + * Copyright (C) 2022 Swissloop + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + */ + +body { + margin: 0; + padding: 0; + font-family: "Century Gothic", CenturyGothic, Geneva, AppleGothic, sans-serif; +} + +/* +* === HEADER === +*/ + +#header { + height: 64px; + background-color: #DDDDDD; + box-shadow: 0 0.5px 1px 0 #9E9E9E; +} + +#header .version { + font-size: 8px; + color: black; + text-align: center; + margin-top: -9px; +} + +#header .btn.disabled, #header .btn:disabled { + opacity: .30; +} + +.btn-warning.disabled, .btn-warning:disabled { + color: white; +} + +/* +* === FOOTER === +*/ + +#footer { + height: 18px; + background-color: #DDDDDD; + box-shadow: 0 0.5px 1px 0 #9E9E9E; + font-size: 12px; + vertical-align:middle; + color: black; +} + + +/* +* === BUTTONS === +*/ + +button { + cursor:pointer; + text-decoration: none; + font-weight: bolder !important; +} + +.btn-warning { + color: white; +} + +.btn-warning:hover, .btn-warning:focus, .btn-warning:active, .btn-warning.active { + color: white !important; +} + +.btn-orange { + color: white; + background-color: #ff7500; + border-color: #ff7500; +} + +.btn-orange:hover, +.btn-orange:focus { + color: white; + background-color: #e86900; + background-position: 0 -15px; + border-color: #db6200; +} + +.btn-orange:active, +.btn-orange.active { + color: white; + background-color: #db6200; + border-color: #db6200; +} + +.btn-orange:focus, +.btn-orange.focus { + box-shadow: 0 0 0 .2rem rgba(219, 98, 0, 0.5); +} + +.btn-orange.disabled, +.btn-orange:disabled { + color: #fff; + background-color: #6c757d; + border-color: #6c757d; +} + +.btn-empty { + cursor: initial !important; + border: none !important; +} + +.btn-empty:hover { + background: transparent; +} + +/* +* === PROGRESS BAR === +*/ + +.progress { + font-size: 1.5rem; + text-align: center; + font-weight: bolder; +} +.progress-value { + position:absolute; + right:0; + left:0; + top:50%; +} + +.bg-run { + background-color: #ff7500; +} + +.bg-brake { + background-color: #28a745; +} + +/* +* === PROGRESS BAR === +*/ + +.instrument-row gauge, .instrument-row metric { + display: inline-block; +} + +/* +* === BOOTSTRAP CARDS === +*/ + +.card-columns { + display:flex; + margin-left: -5px; +} +.card-columns .card { + margin-left: 5px; +} + +.card-body { + padding: 0.5rem; + display: flex; + -ms-flex-wrap: wrap; + flex-wrap: wrap; +} + +.card-header { + font-weight: bolder; + padding: .5rem 1rem; +} + +.card-body button{ + margin: 5px 0; +} + +#no_connection, +#loading{ + width: 10rem; + display: block; + height: 10rem; + position: relative; + z-index: -1; + top: 50%; + margin: 0 auto; +} + +#no_connection.spinner-border, +#loading.spinner-border{ + border-width: 1em; +} + +#no_connection_overlay, +#loading_overlay{ + position: fixed; + z-index: 1000; + top: 0; + bottom: 0; + right: 0; + left: 0; + height: 100%; + width: 100%; + background: rgba( 0, 0, 0, 0.5 ); + overflow-y: hidden; +} \ No newline at end of file diff --git a/control_panel/css/config_table.css b/control_panel/css/config_table.css new file mode 100644 index 0000000..44de12b --- /dev/null +++ b/control_panel/css/config_table.css @@ -0,0 +1,47 @@ +/** + * @file config_table.css + * @brief Cascading Style Sheets for configuration table + * + * @author Philip Wiese, philip.wiese@swissloop.ch + * + * @license + * Copyright (C) 2022 Swissloop + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + */ + +/* +* === CSS for Config Table === +*/ + +#config-table tr td:nth-child(1) { + font-weight: bold; + width: 180px; +} + +#config-table td{ + vertical-align: middle; + padding-top: 0; + padding-bottom: 0; +} + +#config-table .form-control{ + font-size: 14px; +} + +#config-table-collapse { + overflow:scroll; + height: 500px; +} \ No newline at end of file diff --git a/control_panel/css/error_table.css b/control_panel/css/error_table.css new file mode 100644 index 0000000..c7f7189 --- /dev/null +++ b/control_panel/css/error_table.css @@ -0,0 +1,50 @@ +/** + * @file error_table.css + * @brief Cascading Style Sheets for error table + * + * @author Philip Wiese, philip.wiese@swissloop.ch + * + * @license + * Copyright (C) 2022 Swissloop + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + */ + +/* +* === CSS for Error Table === +*/ + +#error-table { + font-size: 12px; +} + +#error-table tr:nth-child(2n) { + background-color: #d0d0d0cf; +} + +#error-table tr td:nth-child(1) { + font-weight: bold; + width: 180px; +} + +#error-table a { + color: red; + text-decoration: none; +} + +#error-table a:hover { + color: red; + text-decoration: none; +} diff --git a/control_panel/css/hv_bat_table.css b/control_panel/css/hv_bat_table.css new file mode 100644 index 0000000..23379d4 --- /dev/null +++ b/control_panel/css/hv_bat_table.css @@ -0,0 +1,45 @@ +/** + * @file hv_bat_table.css + * @brief Cascading Style Sheets for high voltage battery table + * + * @author Philip Wiese, philip.wiese@swissloop.ch + * + * @license + * Copyright (C) 2022 Swissloop + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + */ + +/* +* === CSS for Error Table === +*/ + +.hv_bat_table { + font-size: 12px; +} + +.hv_bat_table tr { + text-align: center; +} + +.hv_bat_table tr td:nth-child(1) { + font-weight: bold; + width:50px; + text-align: center; +} + +#battery-voltages-table tr td:last-child { + font-weight: bold; +} diff --git a/control_panel/css/icu.css b/control_panel/css/icu.css new file mode 100644 index 0000000..63be4e9 --- /dev/null +++ b/control_panel/css/icu.css @@ -0,0 +1,37 @@ +/** + * @file icu.css + * @brief Cascading Style Sheets for icu status table + * + * @author Philip Wiese, philip.wiese@swissloop.ch + * + * @license + * Copyright (C) 2022 Swissloop + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + */ + +/* +* === CSS for ICU Status Table === +*/ + +#icu-vertical-table { + font-size: 13px; + text-align: center; +} + +#fpga-horizontal-table { + font-size: 13px; + text-align: right; +} \ No newline at end of file diff --git a/control_panel/css/logView.css b/control_panel/css/logView.css new file mode 100644 index 0000000..77662c8 --- /dev/null +++ b/control_panel/css/logView.css @@ -0,0 +1,35 @@ +/** + * @file logView.css + * @brief Cascading Style Sheets for log viewer + * + * @author Philip Wiese, philip.wiese@swissloop.ch + * + * @license + * Copyright (C) 2022 Swissloop + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + */ + +/* +* === CSS for LogView === +*/ + +#logViewChart_1 { + +} + +#main { + height: calc(100% - 64px) +} \ No newline at end of file diff --git a/control_panel/css/pod.css b/control_panel/css/pod.css new file mode 100644 index 0000000..579b648 --- /dev/null +++ b/control_panel/css/pod.css @@ -0,0 +1,34 @@ +/** + * @file pod.css + * @brief Cascading Style Sheets for pod status + * + * @author Philip Wiese, philip.wiese@swissloop.ch + * + * @license + * Copyright (C) 2022 Swissloop + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + */ + +/* +* === CSS for SVG === +*/ + +#pod { + padding: 10px 0; + font-family: "CenturyGothic"; +} + +/* Titles */ diff --git a/control_panel/css/protocol_table.css b/control_panel/css/protocol_table.css new file mode 100644 index 0000000..ac229d8 --- /dev/null +++ b/control_panel/css/protocol_table.css @@ -0,0 +1,45 @@ +/** + * @file protocol_table.css + * @brief Cascading Style Sheets for protocol table + * + * @author Philip Wiese, philip.wiese@swissloop.ch + * + * @license + * Copyright (C) 2022 Swissloop + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + */ + +/* +* === CSS for Protocol Table === +*/ + +#protocol-table { + text-align: center; + font-size: 14px; + width: 100%; +} + +#protocol-table tr:nth-child(2n) { + background-color: #d0d0d0cf; +} + +#protocol-table tr th:first-child, table tr td:first-child { + text-align: left; +} + +#protocol-table tr td:nth-child(3) { + font-weight: bold; +} diff --git a/control_panel/css/testing_area.css b/control_panel/css/testing_area.css new file mode 100644 index 0000000..200b858 --- /dev/null +++ b/control_panel/css/testing_area.css @@ -0,0 +1,47 @@ +/** + * @file testing_area.css + * @brief Cascading Style Sheets for testing area + * + * @author Philip Wiese, philip.wiese@swissloop.ch + * + * @license + * Copyright (C) 2022 Swissloop + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + */ + +/* +/* * === CSS for Testing Area === */ +*/ + +#testing_area_body_telemetry .table-inactive td { + background-color: #ececec; + color: #b9b9b9; +} + +#collapse_testing tr td:nth-child(1) { + font-weight: bold; + width: 180px; +} + +#collapse_testing td{ + vertical-align: middle; + padding-top: 0; + padding-bottom: 0; +} + +#collapse_testing .form-control{ + font-size: 14px; +} diff --git a/control_panel/html/comWorker.html b/control_panel/html/comWorker.html new file mode 100644 index 0000000..f862fef --- /dev/null +++ b/control_panel/html/comWorker.html @@ -0,0 +1,39 @@ + + + + + + + Communication Worker + + + + + + + diff --git a/control_panel/html/logViewer.html b/control_panel/html/logViewer.html new file mode 100644 index 0000000..a983836 --- /dev/null +++ b/control_panel/html/logViewer.html @@ -0,0 +1,129 @@ + + + + + + + Swissloop Log Viewer + + + + + + + + + + + + + + + + + + + +
+ + +
+
+
+ + + +
+ +
+ +
+
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+
+ + +
+ +
+ + + + + + + + + + diff --git a/control_panel/html/podControl.html b/control_panel/html/podControl.html new file mode 100644 index 0000000..9ca4f48 --- /dev/null +++ b/control_panel/html/podControl.html @@ -0,0 +1,523 @@ + + + + + + + Swissloop Control Panel + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ Loading... +
+
+ + + + + +
+
+
+
+ Position: 0 +
+
+
+ + +
+
+
+
Battery Management
+
+ + + + +
+
+
+
Vehicle Control
+
+ + + + +
+
+
+
Inverter Control
+
+ + + +
+
+
+
+
+ + + + + + + + + + + +
MinActualMaxUnit
+
+
+
+
+
Errors
+
+ + + + + + + + +
TypeError
+
+ +
States
+
+ + + + + + + + + +
SystemStateReady
+
+
+ + +
+
+ Configuration +
+
+
+ + + + + + + + + + + +
NameCurrentManualTXUnit
+
+
+
+
+ + +
+
+
Inverter Board
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Board:0123
Mosfet:012012012012
+ + +
+
FPGA ADC Currents
+
+ + + + + + + + + + + + + + +
Polepair01234567
+
+
+ +
+ +
+
+
HV Battery Left Temperatures in °C
+
+ + + + + + + + + + + + + + + + +
PackTemp 0Temp 1Temp 2Temp 3Temp 4Temp 5Temp 6Temp 7
+ +
+
+
+
HV Battery Left Voltages in V
+
+ + + + + + + + + + + + + + + + + + + + + + + + +
PackCell 0Cell 1Cell 2Cell 3Cell 4Cell 5Cell 6Cell 7Cell 8Cell 9Cell 10Cell 11Cell 12Cell 13Cell 14Cell 15Sum
+
+
+ +
+
+
+
HV Battery Right Temperatures in °C
+
+ + + + + + + + + + + + + + + + +
PackTemp 0Temp 1Temp 2Temp 3Temp 4Temp 5Temp 6Temp 7
+ +
+
+
+
HV Battery Right Voltages in V
+
+ + + + + + + + + + + + + + + + + + + + + + + + +
PackCell 0Cell 1Cell 2Cell 3Cell 4Cell 5Cell 6Cell 7Cell 8Cell 9Cell 10Cell 11Cell 12Cell 13Cell 14Cell 15Sum
+
+
+
+ +
+
+ Testing +
+
+
+ + + + + + + + + + + + +
NameCurrentManualTXDescription
+ + + + + + + + + + +
NameCurrentDescription
+
+
+
+ +
+ + + + + + + + + + + + + + diff --git a/control_panel/img/Icon.png b/control_panel/img/Icon.png new file mode 100644 index 0000000..cb60c59 Binary files /dev/null and b/control_panel/img/Icon.png differ diff --git a/control_panel/img/Motor.jpg b/control_panel/img/Motor.jpg new file mode 100644 index 0000000..be87589 Binary files /dev/null and b/control_panel/img/Motor.jpg differ diff --git a/control_panel/img/Pod.svg b/control_panel/img/Pod.svg new file mode 100644 index 0000000..3a05b02 --- /dev/null +++ b/control_panel/img/Pod.svg @@ -0,0 +1,1065 @@ + + + + + + + + + + + + + + + + + + 00 ºC + 00 ºC + 00 ºC + 00 ºC + 00 ºC + 00 ºC + 00 ºC + + + + + + + + + + + + + + + + + + + + + 999 A + 080 A + 000 A + 080 A + 000 A + 080 A + 000 A + 080 A + + + 1 + 2 + 3 + 0 + A- + B- + A+ + B+ + A- + B- + A+ + B+ + A- + B- + A+ + B+ + A- + B- + A+ + B+ + + + + + + + + + + + + + + --.- -- + ---.- -- + --.- -- + READY + + + + --.- -- + ---.- -- + --.- -- + READY + + + + + + --.- -- + --.-- -- + --.- - + + + + --.- -- + --.-- -- + --.- -- + + + + --.- -- + --.-- -- + --.- -- + + + + --.- -- + --.-- -- + --.- -- + + + + + + -.- - + --.- - + + + + -.- - + --.- - + + + + + ICU + + + + + --- bar + + + + -- bar + + + + -- bar + + + + -- bar + + + + -- bar + + + + R + L + + + + + + + + + + 00.0 m/s + 00.0 m/s + 00.0 km/h + 00.0 kW + 00.0 kvar + 00.0 m/s + 00.0 km/h + Max. Run + Max. Session + 000.0 m + + + + + + + + + + + + + + + + + + + + + + EMG + + + + + + + + + + LV + + + + + + + + BRAKE + + + + + + + + + + + + + TEMP + + + + + + + + + + + + + HV + + + + + + + + + + + + + ISO + + + + + + + + TRIP + + + + diff --git a/control_panel/img/Pod_top.png b/control_panel/img/Pod_top.png new file mode 100644 index 0000000..aaf0540 Binary files /dev/null and b/control_panel/img/Pod_top.png differ diff --git a/control_panel/img/Swissloop_Logo.svg b/control_panel/img/Swissloop_Logo.svg new file mode 100644 index 0000000..68b118d --- /dev/null +++ b/control_panel/img/Swissloop_Logo.svg @@ -0,0 +1,36 @@ + + + + + Swissloop_Logo + + + + + + + + + + + + + + + + diff --git a/control_panel/js/communication/com.js b/control_panel/js/communication/com.js new file mode 100644 index 0000000..8cc7de9 --- /dev/null +++ b/control_panel/js/communication/com.js @@ -0,0 +1,315 @@ +/** + * @file com.js + * @brief Communication with the pod + * + * @author Philip Wiese, philip.wiese@swissloop.ch + * + * @license + * Copyright (C) 2022 Swissloop + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * @module Communication.Communication + * @version 1.2.0 + * + * @listens dgram:socket~error + * @listens dgram:socket~message + * @listens dgram:socket~listening + * @listens electron:ipcRenderer~startLogging + * @listens electron:ipcRenderer~stopLogging + * @listens electron:ipcRenderer~packet + * + * @emits electron:ipcRenderer~connected + * @emits electron:ipcRenderer~error + * @emits electron:ipcRenderer~packet + * @emits electron:ipcRenderer~console_log + */ + +// IPC renderer to communicate with main GUI renderer +const { remote, ipcRenderer } = require('electron'); + +// To create folders and streams and files +const fs = require('fs'); + +// To create network sockets +const dgram = require('dgram'); + +// To create csv data and pipe into file streams +const csv = require('fast-csv'); + +// Library to work with date objects +const moment = require('moment'); + +// To parse and create telemetry data based on typesets +const jBinary = require('jbinary'); + +// Useful utilities +const util = require('../util.js'); + +// Parser for telemetry frames +const Parser = require('./parser.js').Parser; + +const typeset_ctrl_frame = require('../typesets/typeset_ctrl_frame.js').typeset; +const typeset_telemetry_frame = require('../typesets/typeset_telemetry_frame.js').typeset; + +// Load configuration settings +const config = require('../../config/config'); + +class Communication { + /** + * @constructor + * + * Creates new communication module including a network socket; three csv streams for telemetry and + * control frames. It further sends heartbeat messages to the pod on a regular basis. + * A heartbeat is simply a empty (all zero) control frame. + */ + constructor() { + /** + * CSV stream for telemetry frames + * @type {CsvTransformStream} + */ + this.csv_stream_telemetry = csv.format({headers: true, delimiter: ';', rowDelimiter: '\r\n'}); + + /** + * CSV stream for control frames + * @type {CsvTransformStream} + */ + this.csv_stream_ctrl = csv.format({headers: true, delimiter: ';', rowDelimiter: '\r\n'}); + + /** + * Socket to communicate (send and receive) messages from the pod. + * @type {dgram.Socket} + */ + this.socketReceive = dgram.createSocket( {reuseAddr: true, type : 'udp4'}); + this.socketSend = dgram.createSocket( {type : 'udp4'}); + + /** + * Used to disable heartbeats + * @type {boolean} + */ + this.send_heartbeat = true; + + /** + * Parser for telemetry frames + * @type {Parser} + */ + this.parser = new Parser( + this, + this.csv_stream_telemetry, + ); + + /** + * Wherever the logging of control frames is enabled + * @type {boolean} + */ + this.loggingControlEnabled = false; + + /** + * Wherever there is a connection. + * This is not automatically reset after a loss of communication. + * @type {boolean} + */ + this.connected = false; + + // Setup event listeners + this.setupLoggingListeners(); + this.setupNetworkListeners(); + + this.socketReceive.bind(config.communication.udp_port_listen); + + // Send heartbeats on a frequent basis + setInterval(this.sendHeartbeat.bind(this), config.communication.heartbeat_freq); + + if (config.testing.enabled) { + // Send random testing data to the socket. + setInterval(this.sendTestData.bind(this), config.testing.interval); + } + } + + /** + * Setup listener for logging. + * @package + */ + setupLoggingListeners() { + // Create log file and csv stream and start logging on IPC command fom main GUI renderer + ipcRenderer.on('startLogging', () => { + // Create logging path & folder + let appPath = remote.app.getAppPath(); + let logPath = `${appPath}/logbooks`; + let time = moment().format("YYYY-MM-DD_HH-mm-ss"); + let filename_telemetry = logPath + "/log_" + time + "_telemetry.csv"; + let filename_ctrl = logPath + "/log_" + time + "_ctrl.csv"; + + if (config.verbosity > 0) { + ipcRenderer.send('console_log',"[INFO] Log Path: " + logPath); + } + + // Create 'logbooks' directory if it doesn't exist: + if (!fs.existsSync(logPath)) { + fs.mkdirSync(logPath); + } + + this.csv_stream_telemetry = csv.format({headers: true, delimiter: ';', rowDelimiter: '\r\n'}); + this.csv_stream_ctrl = csv.format({headers: true, delimiter: ';', rowDelimiter: '\r\n'}); + + this.parser.set_csv_telemetry_stream(this.csv_stream_telemetry); + + // Create and configure write stream to log file + this.telemetry_fileStream = fs.createWriteStream(filename_telemetry); + this.ctrl_fileStream = fs.createWriteStream(filename_ctrl); + + // Write csv data to file: + this.csv_stream_telemetry.pipe(this.telemetry_fileStream); + this.csv_stream_ctrl.pipe(this.ctrl_fileStream); + + // Report errors to ipc which are then displayed in the main GUI window + this.telemetry_fileStream.on('error', (error) => { + ipcRenderer.send("error", error); + console.log(error) + }); + this.ctrl_fileStream.on('error', (error) => { + ipcRenderer.send("error", error); + console.log(error); + }); + + this.loggingControlEnabled = true; + }); + + // Stop logging and close file on IPC command fom main GUI renderer + ipcRenderer.on('stopLogging', () => { + this.loggingControlEnabled = false; + setTimeout(() => { + this.telemetry_fileStream.end(); + this.ctrl_fileStream.end(); + }, 200); + }); + } + + /** + * Setup listener for UDP networking + * @package + */ + setupNetworkListeners() { + this.socketReceive.on('error',(err) => { + ipcRenderer.send("error", err); + }); + + // The message event is fired, when a UDP packet arrives destined for this server.. + this.socketReceive.on('message', (data) => { + // Pipe data from server into parser: + this.parser.append_buffer(data); + if (this.connected === false) { + ipcRenderer.send("connected"); + this.connected = true; + } + ipcRenderer.send("packet"); + }); + + // The listening event is fired, when the server has initialized and all ready to receive UDP packets + this.socketReceive.on('listening', () => { + const address = this.socketReceive.address(); + ipcRenderer.send('console_log',`[INFO] Server Address: ${address.address}:${address.port}`); + ipcRenderer.send('console_log',`[INFO] Pod Address: ${config.communication.udp_host_send}:${config.communication.udp_port_send}`); + if (config.verbosity > 5) ipcRenderer.send('console_log',"[DEBUG] Socket Receive Buffer Size:", this.socketReceive.getRecvBufferSize()); + }); + + ipcRenderer.on("sendCtrlFrame", (_event, payload) =>{ + this.sendCtrlFrame(payload); + }); + + this.socketReceive.on('close',() => { + ipcRenderer.send('console_log',`[WARN] Socket close!`); + }); + } + + /** + * Send heartbeat to pod + * @package + */ + sendHeartbeat() { + if (this.send_heartbeat) this.sendCtrlFrame(); + } + + /** + * Send a control frame to the pod + * @example + * comm.sendCtrlFrame({ + * "set_state": 0, + * "bms_active_l": 1, + * "battery": 1 + * }); + * + * comm.sendCtrlFrame(); + * + * @param {object} ctrl_frame JSON with control frame + * @param {number} verbosity Required verbosity level to show message + * @public + */ + sendCtrlFrame(ctrl_frame= undefined, verbosity = 0) { + // Crate jBinary object based on control frame typeset + let raw_frame = new jBinary(typeset_ctrl_frame.Length, typeset_ctrl_frame); + + // If frame is not just a simple heartbeat + if (ctrl_frame !== undefined) { + // Parse object to jBinary + let temp_ctrl_frame = raw_frame.readAll() + Object.assign(temp_ctrl_frame, ctrl_frame); + raw_frame.writeAll(temp_ctrl_frame) + if (config.verbosity > verbosity && config.verbosity > 2) { + ipcRenderer.send('console_log',"Control Frame", JSON.parse(JSON.stringify( raw_frame.readAll()))); + } + if (config.verbosity > 6) ipcRenderer.send('console_log',"Control Frame", raw_frame.view.buffer.toString('hex')) + } else { + // Parse object to jBinary + raw_frame.writeAll(raw_frame.readAll()); + if (config.verbosity > 10) ipcRenderer.send('console_log',"[DEBUG] Heartbeat"); + } + + // Send data buffer to pod + this.socketSend.send(raw_frame.view.buffer, config.communication.udp_port_send, config.communication.udp_host_send, function(err, bytes) { + if (err) { + ipcRenderer.send('console_log',err); + throw err; + } + }); + + // Don't log heartbeats + if (this.loggingControlEnabled && !(ctrl_frame === undefined)) { + let frame = raw_frame.readAll(); + // Fat time is not useful instead save timestamp in the right format + delete frame.fat_time; + frame.Timestamp = moment().format("YYYY-MM-DDTHH:mm:ss.SSS"); + + this.csv_stream_ctrl.write(util.flatten(frame)); + } + } + + /** + * Send test data to pod + * @package + */ + sendTestData() { + // Create array with random data: + let testData = util.createTestingData(typeset_telemetry_frame.Length, config.testing.random); + let port = config.testing.enabled ? config.communication.udp_port_listen : config.communication.udp_port_send; + let address = config.testing.enabled ? "127.0.0.1" : config.communication.udp_host_send; + this.socketReceive.send(testData, port, address, function(err) { + if (err) throw err; + }); + } +} + +module.exports = { + Communication: Communication +}; \ No newline at end of file diff --git a/control_panel/js/communication/parser.js b/control_panel/js/communication/parser.js new file mode 100644 index 0000000..4e657cf --- /dev/null +++ b/control_panel/js/communication/parser.js @@ -0,0 +1,192 @@ +/** + * @file parser.js + * @brief Parser for different communication frames based on a jBinary + * typesets + * + * @author Philip Wiese, philip.wiese@swissloop.ch + * + * @license + * Copyright (C) 2022 Swissloop + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * @module Communication.Parser + * @version 1.2.0 + * + * @listens electron:ipcRenderer~startLogging + * @listens electron:ipcRenderer~stopLogging + * + * @emits electron:ipcRenderer~missingHeartbeat + * @emits electron:ipcRenderer~telemetryFrame + */ + +// IPC renderer to communicate with main GUI renderer +const { ipcRenderer } = require('electron'); + +// Library to work with date objects +const moment = require('moment'); + +// To parse and create telemetry data based on typesets +const jBinary = require('jbinary'); + +// Useful utilities +const util = require('../util.js'); + +const typeset_telemetry_frame = require('../typesets/typeset_telemetry_frame.js').typeset; + +// Load configuration settings +const config = require('../../config/config'); +const mappings = require('../../config/mappings.js'); + +class Parser { + + /** + * Parser for binary data + * + * @constructor + * @param {Communication.Communication} communication Communication module. + * @param {CsvTransformStream} csvStream_telemetry fast-csv object. + */ + constructor(communication,csvStream_telemetry) { + /** + * Reference to communication module + * @type {Communication.Communication} + */ + this.communication = communication; + + /** + * CSV stream for telemetry frames + * @type {CsvTransformStream} + */ + this.csvStream_telemetry = csvStream_telemetry; + + /** + * Telemetry frame typ3set + * @type {jBinary.typeSet} + */ + this.typeset_telemetry = typeset_telemetry_frame; + + /** + * Wherever the logging of telemetry frames is enabled + * @type {boolean} + */ + this.loggingTelemetryEnabled = false; + + this.buffer = Buffer.alloc(2048); + + this.telemetryTimeout = setTimeout(this.missingHeartbeat.bind(this), config.communication.heartbeat_timeout); + + // Listener for logging events + ipcRenderer.on('startLogging', (function() { + setTimeout(() => { + this.loggingTelemetryEnabled = true; + }, 200); + + }).bind(this)); + + ipcRenderer.on('stopLogging', (function() { + this.loggingTelemetryEnabled = false; + }).bind(this)); + } + + set_csv_telemetry_stream(csvStream_telemetry) { + this.csvStream_telemetry = csvStream_telemetry; + } + + /** + * Append raw binary data from UDP stream to local buffer. + * This function also checks for complete frames and parses them. + * + * @param {Buffer} raw Binary buffer with raw data from udp stream. + */ + append_buffer(raw) { + // Not Tested code for concatenation the telemetry frames that come in and search for the sync word + // this.buffer = Buffer.concat([this.buffer, raw]); + this.buffer = raw; + + // Do error detection if the udp frame is not the expected length + if (raw.length != this.typeset_telemetry.Length) { + if (config.verbosity > 10) ipcRenderer.send('console_log',"[INFO] Length of RAW: " + raw.length + " is not correct"); + return; + } + + // Search for telemetry frame sync word + let sync_index_tel = this.buffer.indexOf(this.typeset_telemetry.SyncWord, 0, 'hex'); + + if (config.verbosity > 10) ipcRenderer.send('console_log',"Telemetry", this.buffer.toString('hex')) + + // Parse all telemetry frame with a fixed known size + while (sync_index_tel !== -1 && this.typeset_telemetry.Length <= this.buffer.length) { + // Check if frame is complete + if (sync_index_tel + this.typeset_telemetry['Length'] <= this.buffer.length) { + // Parse complete frame + let frame_buffer = this.buffer.slice(sync_index_tel, sync_index_tel + this.typeset_telemetry.Length); + if (config.verbosity > 10) ipcRenderer.send('console_log',"Telemetry", this.buffer.toString('hex')) + this.parse_telemetry_frame(frame_buffer); + // Remove complete frame + this.buffer = this.buffer.slice(sync_index_tel + this.typeset_telemetry.Length); + } + sync_index_tel = this.buffer.indexOf(this.typeset_telemetry.SyncWord, 0,'hex'); + } + + // Clear the Heartbeat Timeout + if (this.telemetryTimeout) { + clearTimeout(this.telemetryTimeout); + this.telemetryTimeout = setTimeout(this.missingHeartbeat.bind(this), config.communication.heartbeat_timeout); + ipcRenderer.send("heartbeat"); + } + } + + /** + * This function parses a complete telemetry frame. + * + * @param {Buffer} raw_frame Binary buffer containing one telemetry frame. + */ + parse_telemetry_frame(raw_frame) { + // Crate jBinary object based on telemetry frame typeset + let frame_parser = new jBinary(raw_frame, this.typeset_telemetry); + // Parse frame with jBinary + let frame = frame_parser.readAll(); + // Add a timestamp to the frame + frame.Timestamp = moment().format("YYYY-MM-DDTHH:mm:ss.SSS"); + + if (this.loggingTelemetryEnabled) { + // Flatten to "State"."STATE" to "State.STATE" + let csv_frame = util.flatten(frame); + // Remove Sync byte + delete csv_frame['Sync.SYNC']; + if (this.csvStream_telemetry) this.csvStream_telemetry.write(csv_frame); + } + + if (config.verbosity > 7) { + ipcRenderer.send('console_log',"Telemetry_Frame", JSON.parse(JSON.stringify(frame))); + } + // Update the GUI + ipcRenderer.send("telemetryFrame", frame); + } + + /** + * Emitter for missing heartbeat. Used as the callback function of a timer on a timeout. + * + * @emits electron:ipcRenderer~missingHeartbeat + */ + missingHeartbeat (){ + ipcRenderer.send("missingHeartbeat"); + } + +} + +module.exports = { + "Parser": Parser +}; \ No newline at end of file diff --git a/control_panel/js/generator.js b/control_panel/js/generator.js new file mode 100644 index 0000000..0e18cbf --- /dev/null +++ b/control_panel/js/generator.js @@ -0,0 +1,182 @@ +/** + * @file generator.js + * @brief Generator for jBinary telemetry frame Typeset + * + * @author Philip Wiese, philip.wiese@swissloop.ch + * + * @license + * Copyright (C) 2022 Swissloop + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * @module TelemetryTypesetGenerator + * @version 3.0.0 + * + */ + +const fs = require("fs"); +const util = require('util'); +const path = require('path'); +const fetch = (...args) => import('node-fetch').then(({default: fetch}) => fetch(...args)); +const config = require("../config/config"); +const { abort } = require("process"); + +// (Tip) Use https://regex101.com/ to analyze regex +// Regex to split category comments, types, variable names and units +const regex = /\s*\/\/\s+(\w+)\s*\n|\s*(\w+)\s+(\w+)\[?(\w+\+*\d*)?\]?;\s*\/\/ ([\w°%\/]+)\s*(\(.*\))?\s*\n/g; +// Regex to split unit name and factor +const regex_unit = /([\w°%/]+)\/([0-9]+)/; + +/** + * Generator for jBinary telemetry frame Typeset + * + * @param {string} filepath Path to the network_telemetry_frame.h file. + * + */ +function generate_typeset_network(file_telemetry) { + // Skeleton for typeset + let frame_telemetry_typeset = { + 'jBinary.all': 'Data', + 'jBinary.littleEndian': true, + 'Length': 0, + 'SyncWord': 'FECA', + Data: { Misc: {} }, + Unit: { Misc : {}} , + Factor: { Misc: {} } + }; + + // List with available datatypes and sizes + const datatypes = { + "int8": 1, + "uint8": 1, + "int16": 2, + "uint16": 2, + "int32": 4, + "uint32": 4, + "int64": 8, + "uint64": 8, + "float32": 4, + "float64": 8 + }; + + let category = "Misc"; + let match; + + while ((match = regex.exec(file_telemetry)) != null) { + + // Match category specifier + if (match[1] !== undefined) { + category = match[1]; + frame_telemetry_typeset.Data[category] = {}; + frame_telemetry_typeset.Unit[category] = {}; + frame_telemetry_typeset.Factor[category] = {}; + }else { + let name = match[3].toUpperCase(); + let type = match[2].substr(0, match[2].length); + if (type === "float") { + type = "float32"; + } else if (type === "double") { + type = "float64"; + } else { + type = type.substr(0,type.length-2); + } + + let array_length = match[4]; + let unit_match = regex_unit.exec(match[5]); + let unit = (unit_match != null) ? unit_match[1] : match[5]; + let factor = (unit_match != null) ? unit_match[2] : 1; + + frame_telemetry_typeset.Factor[category][name] = Number(factor); + frame_telemetry_typeset.Unit[category][name] = unit; + + // Check for arrays + if (array_length !== undefined) { + frame_telemetry_typeset.Length += array_length * datatypes[type]; + frame_telemetry_typeset.Data[category][name] = ['array', type , Number(array_length) ]; + } else { + frame_telemetry_typeset.Length += datatypes[type]; + frame_telemetry_typeset.Data[category][name] = type; + } + } + } + + // Remove unused Category + if (Object.keys(frame_telemetry_typeset.Data['Misc']).length === 0) delete frame_telemetry_typeset.Data['Misc']; + if (Object.keys(frame_telemetry_typeset.Unit['Misc']).length === 0) delete frame_telemetry_typeset.Unit['Misc']; + if (Object.keys(frame_telemetry_typeset.Factor['Misc']).length === 0) delete frame_telemetry_typeset.Factor['Misc']; + + return util.inspect(frame_telemetry_typeset, depth=Infinity , maxArrayLength =Infinity); +} + +// Download latest frame +console.log("[GEN] Download latest VCU telemetry frame definition.") +fetch(config.github.telemetry_frame_src, { + /** @todo Remove API Access and replace with repository secret */ + headers: { + 'Authorization': 'Bearer ' + config.github.github_access_token, + 'Accept': 'application/vnd.github+json' + }, + cache: 'no-cache', + }).then(res => { + if (res.status != 200) { + throw(new Error(`Invalid HTML Response: ${res.statusText} (${res.status})` )); + } + return res.text() + }).then(body => { + console.log("[GEN] Received header."); + let file = new Buffer.from(JSON.parse(body).content, 'base64').toString(); + console.log(file); + + // Generate typeset + let frame_telemetry_typeset = generate_typeset_network(file); + + // Assemble rest of file + let content = `/** + * @file typeset_telemetry_frame.js + * @brief jBinary telemetry frame Typeset + * + * @author Philip Wiese, philip.wiese@swissloop.ch + * + * @license + * Copyright (C) 2022 Swissloop + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * @module TelemetryTypeset + * @version 3.0.0 + * + */ + +const frame_telemetry_typeset = +` + frame_telemetry_typeset + `; +module.exports = { + "typeset": frame_telemetry_typeset +}; + `; + + // Save new file + fs.writeFileSync(path.join(__dirname, 'typesets/typeset_telemetry_frame.js'), content); + console.log("[GEN] Generated new parser.") + }); diff --git a/control_panel/js/log_viewer/logView.js b/control_panel/js/log_viewer/logView.js new file mode 100644 index 0000000..6d1c39f --- /dev/null +++ b/control_panel/js/log_viewer/logView.js @@ -0,0 +1,610 @@ +/** + * @file logView.js + * @brief Viewer for control panel log files + * + * @author Philip Wiese, philip.wiese@swissloop.ch + * + * @license + * Copyright (C) 2022 Swissloop + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * @module LogView:ProtocolTable + * @version 3.0.0 + * + */ + +const $ = require('jquery'); + +// To create csv data and pipe into file streams +const fs = require('fs'); +const csv = require('@fast-csv/parse'); +const DataFrame = require('dataframe-js').DataFrame; + +const config = require('../../config/config'); + +const { + lightningChart, + SolidFill, + SolidLine, + emptyLine, + DataPatterns, + Themes, + emptyFill, + UIOrigins, + UIDirections, + ColorPalettes, + AxisTickStrategies, + AutoCursorModes +} = require('@arction/lcjs') + +const lc = lightningChart(); + +// Library to work with date objects +const moment = require('moment'); + +const control_panel_log_config = require('./logView_config.js').config + +const mappings = require('../../config/mappings.js'); + +const loadingOverlay = $('#loading_overlay'); +const groupControlPanel = $('#groupControlPanel'); +const groupVCU = $('#groupVCU'); +const groupInverter = $('#groupInverter'); + +class LogView{ + /** + * @typedef {object} LogViewConfig + * @property {String} id Div container to insert chart. + */ + + /** + * Constructor + * @param {EventEmitter} eventEmitter App-wide command emitter. + * @param {LogViewConfig} config Configuration for log view. + */ + constructor(eventEmitter, config) { + this.eventEmitter = eventEmitter; + this.config = config; + + /** + * @typedef {"controlpanel" | "vcu" | "inverter" } LogTypes + */ + + + /** @typedef {object} LogViewSeries + * @property {"config" | "state" | "series" | "event" } category Category of the series. + * @property {lcjs/LineSeries|lcjs/RectangleSeries} handle LCJS Handle. + * @property {String} label Label to show in legend. + * @property {String} color Color of the graph. + */ + + /** + * + * @type {{vcu: Object., inverter: Object., controlpanel: Object., dataset: DataFrame}} + */ + this.series = { + vcu: {}, + controlpanel: {}, + inverter: {}, + dataset: {} + } + + this.rectanglesSeries = []; + + // Add event listeners + window.addEventListener("load", () => { + this._generate_chart(); + this._add_event_listeners(); + }); + } + + /** + * Setup listener. + * @private + */ + _add_event_listeners() { + return 0; + } + + /** + * Setup dashboard. + * @private + */ + _generate_chart() { + + this.log_db = lc.Dashboard({ + numberOfRows: 3, + numberOfColumns: 1, + container: 'logViewChart_1', + theme: Themes.light, + }) + + this.chartValues = this.log_db.createChartXY({ + columnIndex: 0, + rowIndex: 0, + columnSpan: 1, + rowSpan: 1 + }) + .setTitle('') + .setTitleMarginBottom(0) + .setTitleMarginTop(0) + .setAutoCursor(cursor => { + cursor.disposeTickMarkerY() + cursor.setGridStrokeYStyle(emptyLine) + }) + .setPadding({top: -20, bottom: -5, left: config.logviewer["padding-left"], right: 20}) + + this.chartEvents = this.log_db.createChartXY({ + columnIndex: 0, + rowIndex: 1, + columnSpan: 1, + rowSpan: 1 + }) + .setTitle('') + .setTitleMarginBottom(0) + .setTitleMarginTop(0) + .setAutoCursorMode(AutoCursorModes.onHover) + .setAutoCursor(cursor => cursor + .setResultTableAutoTextStyle(false) + .disposePointMarker() + .disposeTickMarkerX() + .disposeTickMarkerY() + .setGridStrokeXStyle(emptyLine) + .setGridStrokeYStyle(emptyLine) + .setResultTable((table) => { + table + .setOrigin(UIOrigins.Center) + }) + ) + .setPadding({top: -20, bottom: -5, left: 0, right: 20}) + + + this.zoomBandChart = this.log_db.createZoomBandChart( { + columnIndex: 0, + rowIndex: 2, + columnSpan: 1, + rowSpan: 1, + axis: this.chartValues.getDefaultAxisX() + } ) + .setTitle('') + .setTitleMarginBottom(0) + .setTitleMarginTop(0) + .setPadding({top: -20, bottom: -5, left: 42+config.logviewer["padding-left"], right: 20}) + + // Manually attach to event chart + this.zoomBandChart.band.onValueChange( (handler, start, end) => { + this.chartEvents.getDefaultAxisX().setInterval(start, end, false, true) + }) + + this.chartValues.getDefaultAxisX().setAnimationScroll( undefined ) + this.chartValues.getDefaultAxisX().setAnimationZoom( undefined ) + this.chartValues.getDefaultAxisY().setAnimationScroll( undefined ) + this.chartValues.getDefaultAxisY().setAnimationZoom( undefined ) + this.chartEvents.getDefaultAxisX().setAnimationScroll( undefined ) + this.chartEvents.getDefaultAxisX().setAnimationZoom( undefined ) + this.chartEvents.getDefaultAxisY().setAnimationScroll( undefined ) + this.chartEvents.getDefaultAxisY().setAnimationZoom( undefined ) + this.zoomBandChart.getDefaultAxisX().setAnimationScroll( undefined ) + this.zoomBandChart.getDefaultAxisX().setAnimationZoom( undefined ) + this.zoomBandChart.getDefaultAxisY().setAnimationScroll( undefined ) + this.zoomBandChart.getDefaultAxisY().setAnimationZoom( undefined ) + + this.zoomBandChart.getDefaultAxisX().setTitle('seconds') + this.chartValues.getDefaultAxisX().setChartInteractionZoomByDrag(true) + this.chartValues.getDefaultAxisY().setChartInteractionZoomByDrag(true) + this.chartValues.getDefaultAxisX().setChartInteractionPanByDrag(true) + this.chartValues.getDefaultAxisY().setChartInteractionPanByDrag(true) + + this.chartEvents.getDefaultAxisY().setChartInteractionZoomByDrag(false) + this.chartEvents.getDefaultAxisY().setChartInteractionPanByDrag(false) + this.chartEvents.getDefaultAxisY().setChartInteractionZoomByWheel(false) + this.chartEvents.getDefaultAxisY().setAxisInteractionZoomByWheeling(false) + this.chartEvents.getDefaultAxisY().setAxisInteractionZoomByDragging(false) + this.chartEvents.getDefaultAxisY().setAxisInteractionPanByDragging(false) + + this.chartEvents.getDefaultAxisX().setChartInteractionZoomByDrag(false) + this.chartEvents.getDefaultAxisX().setChartInteractionPanByDrag(false) + this.chartEvents.getDefaultAxisX().setChartInteractionZoomByWheel(false) + this.chartEvents.getDefaultAxisX().setAxisInteractionZoomByWheeling(false) + this.chartEvents.getDefaultAxisX().setAxisInteractionZoomByDragging(false) + this.chartEvents.getDefaultAxisX().setAxisInteractionPanByDragging(false) + + console.log(this.chartEvents.getDefaultAxisY().setTickStrategy()) + this.chartEvents.getDefaultAxisY() + .setTickStrategy(AxisTickStrategies.Empty) + + this.log_db.setRowHeight(0, 5) + this.log_db.setRowHeight(1, 3) + this.log_db.setRowHeight(2, 2) + + } + /** + * @typedef {"controlpanel" | "vcu" | "inverter" } LogTypes + */ + + /** + * Open a log file and display data. + * @param htmlLegend {HTMLElement} Where to insert the legend. + * @param type {LogTypes} The type of the log file. + * @param filename {String} The path to the log file. + * @param callback {function} Callback function. + * @private + */ + _openLog(htmlLegend, type, filename, callback) { + return; + } + + /** + * Generate legend with checkboxes to disable series + * + * @param html_element {HTMLElement} Collapsable div where to insert legend + * @param series {Object.} + * @private + */ + _generate_legend(html_element, series) { + html_element.empty(); + + let html_group = $("
").addClass("card card-body"); + let categories = []; + + for (let index in series) { + if (index === 'dataset') continue + if (!series.hasOwnProperty(index)) continue; + let id = series[index].label; + const category = id.split(".")[0]; + + series[index].handle.dispose(); + + const label = id.substring(id.indexOf(".")+1) + id = id.replace(".","-"); + + if (!categories.includes(category)) { + categories.push(category); + if (html_group.children().length > 0) { + html_element.append(html_group) + } + html_group = $("
").addClass("card card-body"); + } + + let html_series = `
+
`; + html_group.append(html_series) + } + html_element.append(html_group) + + // Add event handlers + for (let index in series) { + if (index === 'dataset') continue + if (!series.hasOwnProperty(index)) continue; + const id = series[index].label.replace(".", "-"); + $(`#${id.replace(".","-")}`).change( (element) => { + if ($(`#${id.replace(".","-")}`).is(':checked')) { + series[index].handle.restore() + } else { + series[index].handle.dispose() + } + }) + } + } + + /** + * @param axis FitAxis + */ + fitAxis(axis) { + if (axis.x) this.chartValues.getDefaultAxisX().fit() + if (axis.y) { + this.chartValues.getDefaultAxisY().fit() + this.chartEvents.getDefaultAxisY().fit() + } + } + + /** + * Open and display a control panel log + * @param filename {String} The path to the log file. + */ + openLog_ControlPanel(filename) { + // Overlay + if (loadingOverlay.css('display') === 'none') loadingOverlay.show(); + // Clear rectangles + this.rectanglesSeries = []; + + // Open file a stream + const stream = fs.createReadStream(filename) + + // If there are existing data, hide it + for (let item in this.series.controlpanel) { + if (this.series.controlpanel.hasOwnProperty(item) && this.series.controlpanel[item].handle) this.series.controlpanel[item].handle.dispose() + } + + // Indicator to determine first row + let first = true; + // Timestamp used as origin + let time_origin = 0; + // Used to determine unchanged values and replace them with NaN + let lastRow = []; + + // Store the current label position for event chart + let eventY = 0; + + csv.parseStream(stream, {delimiter: ';', headers: true, objectMode:true}) + .on('headers', header => { + this.series.controlpanel.dataset = new DataFrame([], ['Timestamp']); + for (let item in header) { + if (!header.hasOwnProperty(item)) continue; + if (header[item] === 'Timestamp') continue; + let path = header[item].split('.'); + + // If not configured, hide series + if (control_panel_log_config[path[0]] === undefined || control_panel_log_config[path[0]][path[1]] === undefined) { + console.warn("[LogView]", header[item], " not properly not configured!") + continue; + } + + // If not active skip + if (control_panel_log_config[path[0]][path[1]].active === false) continue; + + // Use custom name if configured + let label = header[item]; + if (control_panel_log_config[path[0]][path[1]].name) { + let temp = Array.from(path); + temp[1] = control_panel_log_config[path[0]][path[1]].name; + label = temp.join('.'); + } + + // Select colors from palette + const lcColor = ColorPalettes.flatUI(header.length)(item) + const toHex = (value) => { + return Math.floor(value).toString(16); + } + const color = `#${toHex(lcColor.getR())}${toHex(lcColor.getG())}${toHex(lcColor.getB())}` + + // Set category and LightningChartJS handle + let handle = undefined; + let category = "" + + switch (control_panel_log_config[path[0]][path[1]].category) { + case "config": + category = "config" + break; + case "state": + category = "state" + handle = this.chartEvents.addRectangleSeries() + .setName(header[item]) + // Show state on hover + .setCursorResultTableFormatter((builder, series, figure) => { + const rect = this.rectanglesSeries.find((bar) => bar.rect === figure); + const mapping = mappings.select_enum(mappings.enum_list, rect.label) + const desc = mappings.select_flag(mapping, rect.data) + return builder + .addRow( "" +desc) + }) + this.chartEvents.getDefaultAxisY().addCustomTick() + .setValue(eventY + 0.4) + .setGridStrokeLength(0) + .setTextFormatter(_ => label.substring(label.indexOf(".")+1)) + .setMarker(marker => marker + .setPadding(0) + .setDirection(UIDirections.Left) + .setBackground(background => background + .setFillStyle(emptyFill) + .setStrokeStyle(emptyLine) + ) + ) + eventY += 1; + break; + case "series": + category = "series" + handle = this.chartValues.addLineSeries({dataPattern: DataPatterns.horizontalProgressive}) + .setName(header[item]) + .setStrokeStyle(new SolidLine({ + thickness: 2, + fillStyle: new SolidFill({ color: lcColor}) + })); + break; + case "event": + category = "event" + handle = this.chartValues.addPointLineSeries({dataPattern: DataPatterns.horizontalProgressive}) + .setName(header[item]) + .setStrokeStyle((strokeStyle) => strokeStyle.setFillStyle(emptyFill)) + .setPointFillStyle(new SolidFill({color: lcColor})) + .setPointSize(50) + break; + default: + continue; + } + + if (handle === undefined) continue; + + // Add column to dataset + this.series.controlpanel.dataset = this.series.controlpanel.dataset.withColumn(header[item]); + + this.series.controlpanel[header[item]] = { + category: category, + label: label, + color: color, + handle: handle + } + } + }) + .on('error', error => console.error(error)) + .on('data', row => { + if (first) { + time_origin = moment(row.Timestamp).valueOf(); + first = false; + } + + // Row with reduced values + let tempRowReduced = [(moment(row.Timestamp).valueOf()-time_origin)/1000]; + // Row with all values + let tempRowValues = [(moment(row.Timestamp).valueOf()-time_origin)/1000]; + let index = 1; + for (let item in row) { + if (!row.hasOwnProperty(item)) continue; + + if (item === 'Timestamp') continue; + if (!this.series.controlpanel[item]) continue; + + let path = item.split('.'); + let value = parseInt(row[item]); + // if (value > 36863) value -= 2 ** 16; + + value /= parseInt(mappings.typeset_telemetry_frame.Factor[path[0]][path[1]]); + + if (control_panel_log_config[path[0]][path[1]].factor) { + value *= control_panel_log_config[path[0]][path[1]].factor + } + + // Remember original values + tempRowValues.push(value); + + if (this.series.controlpanel[item].category !== 'series' || config.logviewer.reduce_series) { + if (value === lastRow[index]) value = NaN; + } + + tempRowReduced.push(value) + index += 1; + } + // Add row to dataset + this.series.controlpanel.dataset = this.series.controlpanel.dataset.push(tempRowReduced); + // Remember original values + lastRow = Array.from(tempRowValues); + }) + .on('end', rowCount => { + this.series.controlpanel.dataset = this.series.controlpanel.dataset.push(lastRow); + eventY = 0; + // Process and reduce data + for (let item in this.series.controlpanel) { + if (item === 'dataset') continue; + if (! this.series.controlpanel.hasOwnProperty(item)) continue; + + // Remove all missing values from previous step + let reduced = this.series.controlpanel.dataset.dropMissingValues([item]); + + switch (this.series.controlpanel[item].category) { + case "config": + break; + case "state": + let data = reduced.toArray(item); + let time = reduced.toArray('Timestamp') + + for (let i = 0; i < data.length-1; i++ ) { + if (data[i] !== 0) { + const rectDimensions = { + x: time[i], + y: eventY, + width: time[i+1] - time[i], + height: 0.8 + } + const lcColor = ColorPalettes.flatUI(Math.max(config.logviewer.max_states))(data[i]) + let rect = this.series.controlpanel[item].handle.add(rectDimensions) + .setFillStyle(new SolidFill({ color: lcColor})) + this.rectanglesSeries.push({ + rect: rect, + data: data[i], + label: item, + }) + + } + } + eventY +=1; + break; + case "series": + this.series.controlpanel[item].handle.addArraysXY(reduced.toArray('Timestamp'), reduced.toArray(item)); + break; + case "event": + this.series.controlpanel[item].handle.addArraysXY(reduced.toArray('Timestamp'), reduced.toArray(item)); + break + } + } + + // Generate custom labels + this._generate_legend(groupControlPanel, this.series.controlpanel); + + // Show all data + this.chartValues.getDefaultAxisX().fit() + this.chartValues.getDefaultAxisY().fit() + + // Show legend + groupControlPanel.collapse("show"); + groupVCU.collapse("hide"); + groupInverter.collapse("hide"); + + if (loadingOverlay.css('display') !== 'none') loadingOverlay.hide(); + }); + } + + /** + * Open and display a VCU log. + * @param filename {String} The path to the log file. + */ + openLog_VCU(filename) { + // Overlay + if (loadingOverlay.css('display') === 'none') loadingOverlay.show(); + // Clear rectangles + this.rectanglesSeries = []; + + // Open file a stream + const stream = fs.createReadStream(filename) + + // If there are existing data, hide it + for (let item in this.series.vcu) { + if (this.series.vcu.hasOwnProperty(item) && this.series.vcu[item].handle) this.series.vcu[item].handle.dispose() + } + + // Indicator to determine first row + let first = true; + // Timestamp used as origin + let time_origin = 0; + + csv.parseStream(stream, {delimiter: ';', headers: true, objectMode:true}) + .on('error', error => console.error(error)) + .on('data', row => { + if (first) { + time_origin = row.timestamp; + first = false; + } + + }) + .on('end', rowCount => { + // Generate custom labels + this._generate_legend(groupVCU, this.series.vcu); + + // Show all data + this.chartValues.getDefaultAxisX().fit() + this.chartValues.getDefaultAxisY().fit() + + // Show legend + groupControlPanel.collapse("hide"); + groupVCU.collapse("show"); + groupInverter.collapse("hide"); + + if (loadingOverlay.css('display') !== 'none') loadingOverlay.hide(); + }); + } + + /** + * Open and display a inverter log, + * @param filename {String} The path to the log file. + */ + openLog_Inverter(filename) { + + } +} + +function init(eventEmitter, config) { + return new LogView(eventEmitter, config); +} + +module.exports = init; \ No newline at end of file diff --git a/control_panel/js/log_viewer/logView_config.js b/control_panel/js/log_viewer/logView_config.js new file mode 100644 index 0000000..ee51bcb --- /dev/null +++ b/control_panel/js/log_viewer/logView_config.js @@ -0,0 +1,52 @@ +/** + * @file logView_config.js + * @brief Configuration file for log viewer + * + * @author Philip Wiese, philip.wiese@swissloop.ch + * + * @license + * Copyright (C) 2022 Swissloop + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + */ + +/** + * @typedef {object} SeriesConfig + * @property {"number" | "enum" | "boolean" } type Type of the series. + * @property {"config" | "state" | "series" | "event" } category Category of the series. + * @property {boolean} active If the series is loaded into the chart. + * @property {string} color RGBA Color (e.g. #FF011702). + */ + +/** + * @type {SeriesConfig} + */ +const logView_config_controlPanel = { + Sync: { + SYNC: {type: 'number', category: 'config', active: false, color: undefined} + }, + State: { + STATE: {type: 'enum', category: 'state', active: true, color: undefined}, + VCU_ERRORS: {type: 'enum', category: 'event', active: true, color: undefined}, + + }, + Velocity: { + CORRAIL_VELOCITY: {type: 'number', category: 'series', active: true, color: undefined}, + }, +}; + +module.exports = { + "config": logView_config_controlPanel +}; diff --git a/control_panel/js/log_viewer/renderer.js b/control_panel/js/log_viewer/renderer.js new file mode 100644 index 0000000..7c60856 --- /dev/null +++ b/control_panel/js/log_viewer/renderer.js @@ -0,0 +1,82 @@ +/** + * @file renderer.js + * @brief Main renderer for log viewer + * + * @author Philip Wiese, philip.wiese@swissloop.ch + * + * @license + * Copyright (C) 2022 Swissloop + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * @module LogView:Main + * @version 3.0.0 + * + */ + +const { dialog } = require('@electron/remote') +const mainWindow = require('@electron/remote').getCurrentWindow() + +const EventEmitter = require('events').EventEmitter; +let eventEmitter = new EventEmitter(); +eventEmitter.setMaxListeners(Infinity); + +// Initialize & load modules +let logView_1_config = { + id: 'logViewChart_1' +} + +const btn_open_vcu_log = $("#btn_open_vcu_log"); +const btn_open_controlpanel_log = $("#btn_open_controlpanel_log"); +const btn_open_inverter_log = $("#btn_open_inverter_log"); +const btn_close = $("#btn_close"); +const btn_home = $('#btn_home') +const btn_fit_x = $('#btn_fit_x') +const btn_fit_y = $('#btn_fit_y') + +// Initialize & load modules +const logView_1 = require('./logView.js')(eventEmitter, logView_1_config); + +btn_open_vcu_log.on("click", () => { + return; +}); + +btn_open_controlpanel_log.on("click", () => { + let file = dialog.showOpenDialog({ + properties: ['openFile'], + filters: [{ name: 'Log', extensions: ['csv'] }] + }).then (result => { + logView_1.openLog_ControlPanel(result.filePaths[0]); + }); +}); + +btn_open_inverter_log.on("click", () => { + return; +}); + +btn_home.on("click", () => { + logView_1.fitAxis({x: true, y:true}) +}); + +btn_fit_x.on("click", () => { + logView_1.fitAxis({x: true}) +}); + +btn_fit_y.on("click", () => { + logView_1.fitAxis({y:true}) +}); + +btn_close.on("click", () => { + mainWindow.hide(); +}) \ No newline at end of file diff --git a/control_panel/js/pod_gui/config_table.js b/control_panel/js/pod_gui/config_table.js new file mode 100644 index 0000000..b1106d0 --- /dev/null +++ b/control_panel/js/pod_gui/config_table.js @@ -0,0 +1,243 @@ +/** + * @file config_table.js + * @brief Protocol table in GUI + * + * @author Philip Wiese, philip.wiese@swissloop.ch + * + * @license + * Copyright (C) 2022 Swissloop + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * @module GUI:ProtocolTable + * @version 3.0.0 + * + * @listens electron:ipcRenderer~telemetryFrame + * @listens window~load + * + */ + +const $ = require('jquery'); +const { ipcRenderer } = require('electron') + +const mappings = require('../../config/mappings.js'); + +// Time until input boxes are updated automatically +const selected_time = 2000; + +class ConfigTable { + /** + * Constructor + * @param eventEmitter App-wide command emitter. + */ + constructor(eventEmitter) { + this.eventEmitter = eventEmitter; + this.last_time_selected = Array(mappings.config_values.length).fill(Date.now()- selected_time); + // Add event listeners + window.addEventListener("load", () => { + this.generate_table(); + this.add_event_listeners(); + }); + } + + /** + * Setup listener for new telemetry frame + * @package + */ + add_event_listeners() { + ipcRenderer.on('telemetryFrame', (_event, frame) =>{ + this.update_table(frame); + }); + } + + /** + * Initially generate table skeleton + * @package + */ + generate_table() { + let table = document.getElementById("config-table-body"); + + for (let j = 0; j < mappings.config_values.length; j ++) { + // Create an empty element and add it to the i'th position of the table: + let row = table.insertRow(j); + + let config_param = mappings.config_values[j]; + if (config_param.type === "header") { + let cell = row.insertCell(0) + row.style.backgroundColor="darkgrey"; + row.style.fontSize="1.3em" + cell.innerHTML = `${config_param.name}`; + cell.colSpan = 5; + continue; + } + + // Insert new cells ( elements) at the 1st and 2nd position of the "new" element: + let name = row.insertCell(0); + let current = row.insertCell(1); + let manual = row.insertCell(2); + let tx = row.insertCell(3); + let unit = row.insertCell(4); + + let frame_category = config_param.path_telemetry.split(".")[0]; + let frame_name = config_param.path_telemetry.split(".")[1]; + + let frame_unit = (config_param.hasOwnProperty('unit')) ? config_param.unit : mappings.typeset_telemetry_frame.Unit[frame_category][frame_name]; + let frame_factor= mappings.typeset_telemetry_frame.Factor[frame_category][frame_name]; + + let config_factor = (config_param.hasOwnProperty('factor')) ? config_param.factor : 1; + + // Add some text to the new cells: + name.innerHTML = config_param.name; + current.innerHTML = `0` + tx.innerHTML = `` + unit.innerHTML = frame_unit; + + switch (config_param.type) { + case "number": + manual.innerHTML = `` + break; + case "decimal": + manual.innerHTML = `` + break; + case "enum": + if (config_param.hasOwnProperty('enum')) { + let selectList = document.createElement("select"); + selectList.id = `Config-${frame_name}-input`; + selectList.classList.add(...["custom-select", "mr-sm-2", "form-control"]); + + // Skip first state (UNDEFINED) + for (let i=0; i` + } + + break; + case "readonly": + tx.innerHTML="" + break; + default: + break; + } + + $(`#Config-${frame_name}-send`).on("click", () => { + let ctrl_frame = {} + ctrl_frame[config_param.path] = $(`#Config-${frame_name}-input`).val()*frame_factor*config_factor + if ( + config_param.path == "config_current_setpoint_run" || + config_param.path == "config_current_setpoint_crawl" || + config_param.path == "config_run_velocity" + ) { + ctrl_frame[config_param.path] = (ctrl_frame[config_param.path] == 0) ? -1: ctrl_frame[config_param.path]; + } + + // Nasty hack to remove unwanted factor + if (config_param.path == "config_run_velocity" && ctrl_frame[config_param.path] > 0) { + ctrl_frame[config_param.path] /= 1000; + } + ipcRenderer.send('sendCtrlFrame',ctrl_frame); + // Immediately update value in "current" field + this.last_time_selected[j] = Date.now() - selected_time; + }); + + $(`#Config-${frame_name}-input`).keypress( () => { + var keycode = (event.keyCode ? event.keyCode : event.which); + if(keycode == '13'){ + let ctrl_frame = {} + ctrl_frame[config_param.path] = $(`#Config-${frame_name}-input`).val()*frame_factor*config_factor + if ( + config_param.path == "config_current_setpoint_run" || + config_param.path == "config_current_setpoint_crawl" || + config_param.path == "config_run_velocity" + ) { + ctrl_frame[config_param.path] = (ctrl_frame[config_param.path] == 0) ? -1: ctrl_frame[config_param.path]; + } + + // Nasty hack to remove unwanted factor + if (config_param.path == "config_run_velocity" && ctrl_frame[config_param.path] > 0) { + ctrl_frame[config_param.path] /= 1000; + } + + ipcRenderer.send('sendCtrlFrame',ctrl_frame); + // Immediately update value in "current" field + this.last_time_selected[j] = Date.now() - selected_time; + } + }); + } + } + + /** + * Update table with new data from frame + * @param {object} frame Parsed jBinary TelemetryFrame + */ + update_table(frame) { + for (let j = 0; j < mappings.config_values.length; j ++) { + let config_param = mappings.config_values[j]; + if (config_param.type === "header") { + continue; + } + + let category = config_param.path_telemetry.split(".")[0]; + let name = config_param.path_telemetry.split(".")[1]; + let value = frame[category][name]; + + let config_factor = (config_param.hasOwnProperty('factor')) ? config_param.factor : 1; + let frame_unit = (config_param.hasOwnProperty('unit')) ? config_param.unit : mappings.typeset_telemetry_frame.Unit[category][name]; + + let unit = mappings.typeset_telemetry_frame.Unit[category][name]; + let factor= mappings.typeset_telemetry_frame.Factor[category][name]; + let data = mappings.typeset_telemetry_frame.Data[category][name]; + + if (data === "float32") { + $(`#Config-${name}-label`).text((value / factor/config_factor).toFixed(2)); + } else { + $(`#Config-${name}-label`).text(value / factor/config_factor); + } + + if (config_param.hasOwnProperty('modal')) { + if (config_param.hasOwnProperty('enum')) { + let desc = mappings.select_flag(config_param.enum, frame[category][name]) + $(`#${config_param.modal}`).text(desc); + } else { + $(`#${config_param.modal}`).text(value/factor* 1/config_factor + " " + frame_unit); + } + } + + let input = $(`#Config-${name}-input`); + if (!input.is(":focus")) { + if (this.last_time_selected[j] < Date.now() - selected_time) { + if (data === "float32"){ + input.val((value/factor* 1/config_factor).toFixed(2)); + } else { + input.val(value/factor* 1/config_factor); + } + } + } + else { + this.last_time_selected[j] = Date.now(); + } + } + } +} + +function init(eventEmitter) { + return new ConfigTable(eventEmitter); +} + +module.exports = init; \ No newline at end of file diff --git a/control_panel/js/pod_gui/error_table.js b/control_panel/js/pod_gui/error_table.js new file mode 100644 index 0000000..25f68c3 --- /dev/null +++ b/control_panel/js/pod_gui/error_table.js @@ -0,0 +1,160 @@ +/** + * @file error_table.js + * @brief Error table in GUI + * + * @author Philip Wiese, philip.wiese@swissloop.ch + * + * @license + * Copyright (C) 2022 Swissloop + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * @module GUI:ProtocolTable + * @version 3.0.0 + * + * @listens electron:ipcRenderer~telemetryFrame + * @listens window~load + * + */ + +const $ = require('jquery'); +const {ipcRenderer } = require('electron') + +const mappings = require('../../config/mappings.js'); + +class ErrorTable { + /** + * Constructor + * @param eventEmitter App-wide command emitter. + */ + constructor(eventEmitter) { + this.eventEmitter = eventEmitter; + // Add event listeners + window.addEventListener("load", () => { + this.generate_table(); + this.add_event_listeners(); + }); + $(document).ready(function(){ + $("[rel='tooltip'], .tooltip").tooltip(); + }); + } + + /** + * Setup listener for new telemetry frame + * @package + */ + add_event_listeners() { + ipcRenderer.on('telemetryFrame', (_event, frame) =>{ + this.update_table(frame); + }); + } + + /** + * Initially generate table skeleton + * @package + */ + generate_table() { + this.table = document.getElementById("error-table-body"); + + for (let j = 0; j < mappings.errors_list.length; j ++) { + // Create an empty element and add it to the i'th position of the table: + let row = this.table.insertRow(j); + + // Insert new cells ( elements) at the 1st and 2nd position of the "new" element: + let name = row.insertCell(0); + let errors = row.insertCell(1); + let error_param = mappings.errors_list[j]; + name.innerHTML = error_param.name; + errors.innerHTML = "None"; + } + } + + /** + * Update table with new data from frame + * @param {object} frame Parsed jBinary TelemetryFrame + */ + update_table(frame) { + let length = mappings.errors_list.length; + let rows = this.table.rows; + + let errors = []; + + mappings.errors_list.forEach( (element, i) => { + let category = element.path.split(".")[0]; + let name = element.path.split(".")[1]; + let value = frame[category][name]; + let source = undefined; + + if (element.hasOwnProperty('index')) value = frame[category][name][element.index]; + + if (element.hasOwnProperty('source')) { + let source_path = element.source.split(".")[1]; + source = frame[category][source_path]; + value = frame[category][name]; + } + + let table_cell = this.table.rows[i].cells[1] + + table_cell.innerHTML = ""; + + let error_number = 0 + for (let j = 0; j < element.enum.length; j++) { // json errors length + if (value & parseInt(element.enum[j].flag)) { + error_number += 1; + if (source !== undefined) { + // Select only values with known source + if ( !( (source[error_number-1] === element.index) || (element.index === -1 && error_number > 8) )) { + continue; + } + } + if (table_cell.innerHTML.length > 0) { + table_cell.innerHTML += ", "; + } + if (element.enum[j].description !== undefined) { + table_cell.innerHTML += `${element.enum[j].error_type}`; + } else { + table_cell.innerHTML += element.enum[j].error_type; + } + } + } + if (table_cell.innerHTML === "") { + table_cell.innerHTML = "None"; + } + + }); + + if (frame["State"]["VCU_ERRORS"] !== 0 || + frame["State"]["VCU_EMERGENCY_REASON"] !== 0 ) { + errors.push("VCU Error"); + } + + if (frame["Inverter"]["ICUFPGASTATE"] !== 0 || + frame["Inverter"]["ICUMCUSTATE"] !== 0 || + frame["Inverter"]["GD_STATUS"] !== 0 ) { + errors.push("Inverter Error"); + } + + if (frame["HV_Right"]["HV_R_ERROR"] !== 0 || + frame["HV_Left"]["HV_L_ERROR"] !== 0) { + errors.push("Battery Error"); + } + this.eventEmitter.emit('errorStatus', errors); + } +} + +function init(eventEmitter) { + return new ErrorTable(eventEmitter); +} + +module.exports = init; \ No newline at end of file diff --git a/control_panel/js/pod_gui/hv_bat_tables.js b/control_panel/js/pod_gui/hv_bat_tables.js new file mode 100644 index 0000000..83a4b2d --- /dev/null +++ b/control_panel/js/pod_gui/hv_bat_tables.js @@ -0,0 +1,224 @@ +/** + * @file hv_bat_tables.js + * @brief HV battery temperature and voltage table in GUI + * + * @author Philip Wiese, philip.wiese@swissloop.ch + * + * @license + * Copyright (C) 2022 Swissloop + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * @module GUI:HV_bat_Table + * @version 3.0.0 + * + * @listens electron:ipcRenderer~telemetryFrame + * @listens window~load + * + */ + +const $ = require('jquery'); +const {ipcRenderer } = require('electron') + +const mappings = require('../../config/mappings.js'); + +class HVBat_Table { + /** + * Constructor + * @param eventEmitter App-wide command emitter. + */ + constructor(eventEmitter) { + this.eventEmitter = eventEmitter; + // Taking the values of the left hv box since both should always be the same anyways + this.num_voltages = mappings.typeset_telemetry_frame.Data.HV_Left.HV_L_VOLTAGES[2] + this.num_temperatures = mappings.typeset_telemetry_frame.Data.HV_Left.HV_L_TEMPERATURES[2] + + // Add event listeners + window.addEventListener("load", () => { + this.generate_table(); + this.add_event_listeners(); + }); + } + + /** + * Setup listener for new telemetry frame + * @package + */ + add_event_listeners() { + ipcRenderer.on('telemetryFrame', (_event, frame) =>{ + this.update_table(frame); + }); + } + + /** + * Initially generate table skeleton + * @package + */ + generate_table() { + let table_temp_left = document.getElementById("battery-left-temperatures-table-body"); + let table_volt_left = document.getElementById("battery-left-voltages-table-body"); + + // Temperature Table Left + for (let i = 0; i < this.num_temperatures/8; i++) { + // Create an empty element and add it to the i'th position of the table: + let row = table_temp_left.insertRow(i); + row.insertCell(0).innerHTML = i+1; + for (let j=1; j<9; j++) { + row.insertCell(j) + } + } + + // Voltages Table Left + for (let i = 0; i < this.num_voltages/16; i++) { + // Create an empty element and add it to the i'th position of the table: + let row = table_volt_left.insertRow(i); + row.insertCell(0).innerHTML = i+1; + for (let j=1; j<18; j++) { + row.insertCell(j) + } + } + + let table_temp_right = document.getElementById("battery-right-temperatures-table-body"); + let table_volt_right = document.getElementById("battery-right-voltages-table-body"); + + // Temperature Table Right + for (let i = 0; i < this.num_temperatures/8; i++) { + // Create an empty element and add it to the i'th position of the table: + let row = table_temp_right.insertRow(i); + row.insertCell(0).innerHTML = i+1; + for (let j=1; j<9; j++) { + row.insertCell(j) + } + } + + // Voltages Table Right + for (let i = 0; i < this.num_voltages/16; i++) { + // Create an empty element and add it to the i'th position of the table: + let row = table_volt_right.insertRow(i); + row.insertCell(0).innerHTML = i+1; + for (let j=1; j<18; j++) { + row.insertCell(j) + } + } + } + + /** + * Update table with new data from frame + * @param {object} frame Parsed jBinary TelemetryFrame + */ + update_table(frame) { + // Temperature Table Right + let factor_temp = mappings.typeset_telemetry_frame.Factor.HV_Right.HV_R_TEMPERATURES + let max_temperature_right = Math.max.apply(Math, frame.HV_Right.HV_R_TEMPERATURES)/factor_temp; + for (let i = 0; i < this.num_temperatures/8; i++) { + let row = $(`#battery-right-temperatures-table-body tr:nth-child(${i+1})`) + for (let j=0; j<8; j++) { + let cell = row.children(`td:nth-child(${j+2})`) + let value = frame.HV_Right.HV_R_TEMPERATURES[i*8+j]/factor_temp + cell[0].style.border = "1px solid #dee2e6" + if (value == max_temperature_right) { + cell[0].style.border = "2px dashed black" + } + cell.text(value.toFixed(1)) + if (1.0 > value || value > 55) { + cell[0].style.backgroundColor = "#FF5722"; + } else { + cell[0].style.backgroundColor = "#4CAF50" + } + } + } + + // Voltage Table Right + let factor_volt = mappings.typeset_telemetry_frame.Factor.HV_Right.HV_R_VOLTAGES; + let max_voltages_right = Math.max.apply(Math, frame.HV_Right.HV_R_VOLTAGES)/factor_volt; + let min_voltages_right = Math.min.apply(Math, frame.HV_Right.HV_R_VOLTAGES.filter(Boolean))/factor_volt; + for (let i = 0; i < this.num_voltages/16; i++) { + let row = $(`#battery-right-voltages-table-body tr:nth-child(${i+1})`) + let sum = 0; + for (let j=0; j<16; j++) { + let cell = row.children(`td:nth-child(${j+2})`) + let value = frame.HV_Right.HV_R_VOLTAGES[i*16+j]/factor_volt + cell[0].style.border = "1px solid #dee2e6" + if (value == max_voltages_right) { + cell[0].style.border = "2px dashed black" + } + if (value == min_voltages_right) { + cell[0].style.border = "2px solid black" + } + cell.text(value.toFixed(1)) + sum += value + + if (3.0 > value || value > 4.3) { + cell[0].style.backgroundColor = "#FF5722"; + } else { + cell[0].style.backgroundColor = "#4CAF50" + } + } + row.children(`td:last-child`).text(sum.toFixed(1)) + } + // Temperature Table Left + let max_temperature_left = Math.max.apply(Math, frame.HV_Left.HV_L_TEMPERATURES)/factor_temp; + for (let i = 0; i < this.num_temperatures/8; i++) { + let row = $(`#battery-left-temperatures-table-body tr:nth-child(${i+1})`) + for (let j=0; j<8; j++) { + let cell = row.children(`td:nth-child(${j+2})`) + let value = frame.HV_Left.HV_L_TEMPERATURES[i*8+j]/factor_temp + cell[0].style.border = "1px solid #dee2e6" + if (value == max_temperature_left) { + cell[0].style.border = "2px dashed black" + } + cell.text(value.toFixed(1)) + if (1.0 > value || value > 55) { + cell[0].style.backgroundColor = "#FF5722"; + } else { + cell[0].style.backgroundColor = "#4CAF50" + } + } + } + + // Voltage Table Left + let max_voltages_left = Math.max.apply(Math, frame.HV_Left.HV_L_VOLTAGES)/factor_volt; + let min_voltages_left = Math.min.apply(Math, frame.HV_Left.HV_L_VOLTAGES.filter(Boolean))/factor_volt; + for (let i = 0; i < this.num_voltages/16; i++) { + let row = $(`#battery-left-voltages-table-body tr:nth-child(${i+1})`) + let sum = 0; + for (let j=0; j<16; j++) { + let cell = row.children(`td:nth-child(${j+2})`) + let value = frame.HV_Left.HV_L_VOLTAGES[i*16+j]/factor_volt + cell[0].style.border = "1px solid #dee2e6" + if (value == max_voltages_left) { + cell[0].style.border = "2px dashed black" + } + if (value == min_voltages_left) { + cell[0].style.border = "2px solid black" + } + cell.text(value.toFixed(1)) + sum += value + + if (3.0 > value || value > 4.3) { + cell[0].style.backgroundColor = "#FF5722"; + } else { + cell[0].style.backgroundColor = "#4CAF50" + } + } + row.children(`td:last-child`).text(sum.toFixed(1)) + } + } +} + +function init(eventEmitter) { + return new HVBat_Table(eventEmitter); +} + +module.exports = init; \ No newline at end of file diff --git a/control_panel/js/pod_gui/icu.js b/control_panel/js/pod_gui/icu.js new file mode 100644 index 0000000..0364719 --- /dev/null +++ b/control_panel/js/pod_gui/icu.js @@ -0,0 +1,272 @@ +/** + * @file icu.js + * @brief GUI for ICU values + * + * @author Hanno Hiss, hanno.hiss@swissloop.ch + * @author Philip Wiese, philip.wiese@swissloop.ch + * + * @license + * Copyright (C) 2022 Swissloop + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * @module GUI:ICU_Table + * @version 3.0.0 + * + * @listens electron:ipcRenderer~telemetryFrame + * @listens window~load + * + */ + +const mappings = require("../../config/mappings"); + +const $ = require('jquery'); +const {ipcRenderer } = require('electron') +const util = require('../util'); + +let protocol = [ + {"name": "FPGA.FPGA_CURRENT_ADC_VALUES", "title": "ADC", "min":0, "max":65535, "unit": "BIN.14"}, + {"name": "FPGA.FPGA_CURRENT_ADC_VALUES", "title": "Currents in A", "min":0, "max":300, "unit": "A"}, + {"name": "FPGA.FPGA_CURRENT_STATUS", "title": "Status", "min":0, "max":65535, "unit": "TEXT"} +] + +class ICU_Table { + /** + * Constructor + * @param eventEmitter App-wide command emitter. + */ + constructor(eventEmitter) { + this.eventEmitter = eventEmitter; + // Taking the values of the left hv box since both should always be the same anyways + this.num_temperatures = mappings.typeset_telemetry_frame.Data.Inverter.TEMPERATURE_MOSFETS[2]; + // Add event listeners + window.addEventListener("load", () => { + this.generate_table(); + this.add_event_listeners(); + }); + } + + /** + * Setup listener for new telemetry frame + * @package + */ + add_event_listeners() { + ipcRenderer.on('telemetryFrame', (_event, frame) =>{ + this.update_table(frame); + }); + } + + /** + * Initially generate table skeleton + * @package + */ + generate_table() { + let table_icu = document.getElementById("icu-vertical-table-body"); + let cell = undefined; + + // Add Row for MOSFET Temperature Values + let row = table_icu.insertRow(0); + for (let j=0; j element and add it to the i'th position of the table: + let row = table_fpga_hor.insertRow(i); + row.insertCell(0).innerHTML = protocol[i].title; + for (let j=1; j value || value > 80) { + cell[0].style.backgroundColor = "#FF5722"; + } else { + cell[0].style.backgroundColor = "#4CAF50" + } + } + // voltages + let factor_voltage = mappings.typeset_telemetry_frame.Factor.Inverter.BOARD_VOLTAGES + let max_voltage = Math.max.apply(Math, frame.Inverter.BOARD_VOLTAGES)/factor_voltage; + row = $(`#icu-vertical-table-body tr:nth-child(${2})`) + cell = row.children(`td:nth-child(${1})`) + cell.text("Voltages:") + cell[0].style = "text-align: center; vertical-align: middle;" + for (let j=0; j= 40 && value < 50) { + cell[0].style.backgroundColor = "#ff8522"; + } else { + cell[0].style.backgroundColor = "#FF5722"; + } + } else { + if (value < 40) { + cell[0].style.backgroundColor = "#4CAF50" + } else { + cell[0].style.backgroundColor = "#FF5722"; + } + } + } + // faults + row = $(`#icu-vertical-table-body tr:nth-child(${3})`) + cell = row.children(`td:nth-child(${1})`) + cell.text("Faults:") + cell[0].style = "text-align: center; vertical-align: middle;" + let gd_status = frame.Inverter.GD_STATUS + for (let j=0; j. + * + * @module GUI:ProtocolTable + * @version 3.0.0 + * + * @listens electron:ipcRenderer~telemetryFrame + * @listens window~load + * + */ + +const $ = require('jquery'); +const util = require('../util.js') +const { ipcRenderer } = require('electron') + +const mappings = require('../../config/mappings.js'); + +const buttons = { + bms_balance: $("#bms_balance"), + bms_balance_start:$("#bms_balance_start"), + bms_balance: $("#bms_balance"), + bms_restart_left: $("#bms_restart_left"), + bms_restart_right: $("#bms_restart_right"), + pod_reset: $("#pod_reset"), + vcu_reset: $("#vcu_reset"), + inverter_adc_calibrate: $("#inverter_adc_calibrate"), + inverter_restart: $("#inverter_restart"), + inverter_do_foo: $("#inverter_do_foo"), + main_brake: $("#main_brake"), + main_clear: $("#main_clear"), + run_reset: $("#run_reset"), + eject: $("#eject_button") +}; + +class PodControl { + /** + * Constructor + * @param eventEmitter App-wide command emitter. + */ + constructor(eventEmitter) { + this.eventEmitter = eventEmitter; + + // Add event listeners + window.addEventListener("load", () => { + this.add_event_listeners(); + }); + } + + /** + * Setup listener for new telemetry frame and button clicks + * @package + */ + add_event_listeners() { + buttons["eject"].on("click", () => { + ipcRenderer.send('sendCtrlFrame',{"sd_card_flush": 1}); + }); + buttons["bms_balance"].on("click", () => { + $('#balanceModal').modal('hide'); + ipcRenderer.send('sendCtrlFrame',{"bms_balance" : 1}); + }); + buttons["bms_balance"].on("click", () => { + if (buttons["bms_balance"].html() === "Balance Disable") { + // Disable Balancing + ipcRenderer.send('sendCtrlFrame',{"bms_balancing": 1}); + } else { + // Enable Balancing + ipcRenderer.send('sendCtrlFrame',{"bms_balancing": 2}); + } + }); + buttons["bms_restart_left"].on("click", () => { + ipcRenderer.send('sendCtrlFrame',{"bms_software_reset_left": 1}); + }); + buttons["bms_restart_right"].on("click", () => { + ipcRenderer.send('sendCtrlFrame',{"bms_software_reset_right": 1}); + }); + buttons["inverter_adc_calibrate"].on("click", () => { + ipcRenderer.send('sendCtrlFrame',{"inverter_adc_calibrate": 1}); + }); + buttons["inverter_restart"].on("click", () => { + ipcRenderer.send('sendCtrlFrame',{"inverter_software_reset": 1}); + }); + buttons["inverter_do_foo"].on("click", () => { + ipcRenderer.send('sendCtrlFrame',{"inverter_do_foo": 1}); + }); + buttons["pod_reset"].on("click", () => { + ipcRenderer.send('sendCtrlFrame',{"pod_reset_error": 1}); + }); + buttons["vcu_reset"].on("click", () => { + ipcRenderer.send('sendCtrlFrame',{"vcu_software_reset": 1}); + }); + buttons["main_brake"].on("click", () => { + if (buttons["main_brake"].html() === "Brake Do Disengage") { + // Engage Brake + ipcRenderer.send('sendCtrlFrame',{"brake_engage": 1}); + } else { + // Disengage Brake + ipcRenderer.send('sendCtrlFrame',{"brake_engage": 2}); + } + }); + buttons["run_reset"].on("click", () => { + ipcRenderer.send('sendCtrlFrame',{"reset_run": 1}); + }); + + ipcRenderer.on('telemetryFrame', (_event, frame) =>{ + this.update_control_buttons(frame); + }); + } + + update_control_buttons(frame) { + let text = ""; + + // Charge + // let state = util.json_array_select_value(mappings.main_state, frame.State.STATE); + // if ( + // state.description === "Idle" + // && ! frame.HV_Left.HV_L_ERROR + // && ! frame.HV_Right.HV_R_ERROR + // ) { + // buttons["bms_balance_start"].attr("disabled", false) + // } else { + // buttons["bms_balance_start"].attr("disabled", true); + // } + + // // Balance + // // if ( + // // !frame.High_Voltage_Batteries.HV_BAT_ERROR + // // ){ + // // buttons["bms_balance"].attr("disabled", false) + // // } else { + // // buttons["bms_balance"].attr("disabled", true) + // // } + // if (frame.HV_Left.HV_L_BALANCING || frame.HV_Right.HV_R_BALANCING) { + // text = "Balance Disable"; + // buttons["bms_balance"].addClass("btn-success") + // buttons["bms_balance"].removeClass("btn-orange") + // } else { + // text = "Balance Enable"; + // buttons["bms_balance"].removeClass("btn-success") + // buttons["bms_balance"].addClass("btn-orange") + // } + // buttons["bms_balance"].html(text); + + // Brake + let brakes_engaged = frame.Brake.BRAKE_ENGAGE + if (brakes_engaged === 1) { + text ="Brake Do Disengage"; + buttons["main_brake"].css("background","grey"); + } else { + text ="Brake Do Engage"; + buttons["main_brake"].css("background","green"); + } + buttons["main_brake"].html(text); + } +} + +function init(eventEmitter) { + return new PodControl(eventEmitter); +} + +module.exports = init; \ No newline at end of file diff --git a/control_panel/js/pod_gui/pod_overview.js b/control_panel/js/pod_gui/pod_overview.js new file mode 100644 index 0000000..87fe3e1 --- /dev/null +++ b/control_panel/js/pod_gui/pod_overview.js @@ -0,0 +1,548 @@ +/** + * @file pod_overview.js + * @brief Virtual representation of the pod featuring an overview of all + * important values and states + * + * @author Philip Wiese, philip.wiese@swissloop.ch + * + * @license + * Copyright (C) 2022 Swissloop + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * @module GUI:Pod_overview + * @version 3.0.0 + * + * @listens electron:ipcRenderer~telemetryFrame + * @listens window~load + * + * @emits electron:ipcRenderer~mainStatus + * + */ +const { ipcRenderer } = require('electron') +const util = require('../util'); +const mappings = require('../../config/mappings.js'); + +let pod_svg; + +class Pod_overview { + /** + * Constructor + * @param eventEmitter App-wide command emitter. + */ + constructor(eventEmitter) { + this.eventEmitter = eventEmitter; + + window.addEventListener("load", () => { + // Insert CSS into SVG object, save objects & add event listeners + pod_svg = document.getElementById('pod-svg').contentDocument; + let style_pod = pod_svg.createElementNS("http://www.w3.org/2000/svg", "style"); + let svgElem_pod = pod_svg.querySelector('svg'); + + style_pod.textContent = '@import url("../css/pod.css");'; + svgElem_pod.insertBefore(style_pod, svgElem_pod.firstChild); + + this.add_event_listeners(); + }); + } + + /** + * Setup listener for new telemetry frame + * @package + */ + add_event_listeners() { + ipcRenderer.on('telemetryFrame', (_event, frame) => { + this.update_raw_values(frame); + this.update_calculated_values(frame); + this.update_indicators(frame); + }); + } + + /** + * Directly update values in pod overview with new data from frame + * @param {object} frame Parsed jBinary TelemetryFrame + */ + update_raw_values(frame) { + + // Binding SVG fields directly to telemetry frame values + // Format {svg_id}: {config} + // Possible properties in {config}: + // precision: number (e.g. 2) + // array_range: array (e.g. [0, 1, 3]) + // array_func: function (e.g. Math.max) + const value_overview = { + "inverter.1.temp": + { path: "Inverter.TEMPERATURE_MOSFETS", precision: 1, array_range: [0, 1, 2], array_func: Math.max }, + "inverter.2.temp": + { path: "Inverter.TEMPERATURE_MOSFETS", precision: 1, array_range: [3, 4, 5], array_func: Math.max }, + "inverter.3.temp": + { path: "Inverter.TEMPERATURE_MOSFETS", precision: 1, array_range: [6, 7, 8], array_func: Math.max }, + "inverter.4.temp": + { path: "Inverter.TEMPERATURE_MOSFETS", precision: 1, array_range: [9, 10, 11], array_func: Math.max }, + "tank.pressure": + { path: "Brake.BRAKE_TANK", precision: 0 }, + "brake.l.action.pressure": + { path: "Brake.BRAKE_LEFT_ACTION", precision: 0 }, + "brake.r.action.pressure": + { path: "Brake.BRAKE_RIGHT_ACTION", precision: 0 }, + "brake.l.release.pressure": + { path: "Brake.BRAKE_LEFT_RELEASE", precision: 0 }, + "brake.r.release.pressure": + { path: "Brake.BRAKE_RIGHT_RELEASE", precision: 0 }, + "motor.temp.1": + { path: "Motor_Temperatures.TEMP_PT100_2.0", precision: 0 }, + "motor.temp.2": + { path: "Motor_Temperatures.TEMP_PT100_2.1", precision: 0 }, + "motor.temp.3": + { path: "Motor_Temperatures.TEMP_PT100_2.2", precision: 0 }, + "motor.temp.4": + { path: "Motor_Temperatures.TEMP_PT100_2.3", precision: 0 }, + "motor.temp.5": + { path: "Motor_Temperatures.TEMP_PT100_2.4", precision: 0 }, + "motor.temp.6": + { path: "Motor_Temperatures.TEMP_PT100_2.5", precision: 0 }, + "motor.temp.7": + { path: "Motor_Temperatures.TEMP_PT100_2.6", precision: 0 }, + "lv.1.current": + { path: "LV_Batteries.LV_BAT1_CURRENT", precision: 1 }, + "lv.2.current": + { path: "LV_Batteries.LV_BAT2_CURRENT", precision: 1 }, + "lv.1.voltage": + { path: "LV_Batteries.LV_BAT1_VOLTAGE", precision: 1 }, + "lv.2.voltage": + { path: "LV_Batteries.LV_BAT2_VOLTAGE", precision: 1 }, + "speed.distance": + { path: "Velocity.CORRAIL_DISTANCE", precision: 1 }, + "speed.velocity.max.run": + { path: "Velocity.CORRAIL_MAX_VELOCITY", precision: 1 }, + "speed.velocity": + { path: "Velocity.CORRAIL_VELOCITY", precision: 1 }, + "hv.bat_r.current": + { path: "HV_Right.HV_R_CURRENT", precision: 1 }, + "hv.bat_r.voltage": + { path: "HV_Right.HV_R_VOLTAGE", precision: 1 }, + "hv.bat_r.temp": + { path: "HV_Right.HV_R_MAX_CELL_TEMP", precision: 1 }, + "hv.bat_l.current": + { path: "HV_Left.HV_L_CURRENT", precision: 1 }, + "hv.bat_l.voltage": + { path: "HV_Left.HV_L_VOLTAGE", precision: 1 }, + "hv.bat_l.temp": + { path: "HV_Left.HV_L_MAX_CELL_TEMP", precision: 1 }, + "motor.current.0b": + { path: "FPGA.FPGA_CURRENT_ADC_VALUES.7", precision: 0 }, + "motor.current.0a": + { path: "FPGA.FPGA_CURRENT_ADC_VALUES.6", precision: 0 }, + "motor.current.1b": + { path: "FPGA.FPGA_CURRENT_ADC_VALUES.1", precision: 0 }, + "motor.current.1a": + { path: "FPGA.FPGA_CURRENT_ADC_VALUES.0", precision: 0 }, + "motor.current.2b": + { path: "FPGA.FPGA_CURRENT_ADC_VALUES.5", precision: 0 }, + "motor.current.2a": + { path: "FPGA.FPGA_CURRENT_ADC_VALUES.4", precision: 0 }, + "motor.current.3b": + { path: "FPGA.FPGA_CURRENT_ADC_VALUES.3", precision: 0 }, + "motor.current.3a": + { path: "FPGA.FPGA_CURRENT_ADC_VALUES.2", precision: 0 }, + "inverter.1.voltage": + { path: "Inverter.BOARD_VOLTAGES.0", precision: 1 }, + "inverter.2.voltage": + { path: "Inverter.BOARD_VOLTAGES.1", precision: 1 }, + "inverter.3.voltage": + { path: "Inverter.BOARD_VOLTAGES.2", precision: 1 }, + "inverter.4.voltage": + { path: "Inverter.BOARD_VOLTAGES.3", precision: 1 }, + } + + for (let key in value_overview) { + let category = value_overview[key].path.split(".")[0]; + let name = value_overview[key].path.split(".")[1]; + let index = value_overview[key].path.split(".")[2]; + let precision = value_overview[key].precision + let unit = mappings.typeset_telemetry_frame.Unit[category][name]; + let factor = mappings.typeset_telemetry_frame.Factor[category][name]; + + if (unit === "number") { + unit = ""; + } + + let value = NaN; + // Check if values have to be reduced + if (value_overview[key].hasOwnProperty('array_range')) { + let values = [] + for (let index in value_overview[key].array_range) { + values.push(frame[category][name][index]) + } + value = value_overview[key].array_func(...values) + } else { + if (index === undefined) { + value = frame[category][name]; + } else { + value = frame[category][name][index]; + } + } + + // Do special transformation for certain fields + switch (key) { + case "motor.current.0b": + case "motor.current.0a": + case "motor.current.1b": + case "motor.current.1a": + case "motor.current.2b": + case "motor.current.2a": + case "motor.current.3b": + case "motor.current.3a": + value = util.adc2ampere(value) + unit = "A" + pod_svg.getElementById(key).style.fill = + value > 100 || value < -100 ? this.indicator_colors['r'] : + value > 5 ? this.indicator_colors['black'] : + value < -5 ? this.indicator_colors['b'] : + this.indicator_colors['gray']; + break; + default: + break; + } + pod_svg.getElementById(key).innerHTML = `${(value / factor).toFixed(precision)} ${unit}`; + } + } + + // Calculated values + #max_velocity = 0; + #max_power_reactive= 0; + #max_power_real = 0; + + /** + * Update calculated values based on frame values + * @param {object} frame Parsed jBinary TelemetryFrame + */ + update_calculated_values(frame) { + let value = 0 + + // Maximal session speed + value = frame.Velocity.CORRAIL_MAX_VELOCITY / mappings.typeset_telemetry_frame.Factor.Velocity.CORRAIL_MAX_VELOCITY + if (value > this.#max_velocity) this.#max_velocity = value + + pod_svg.getElementById('speed.velocity.max.session').innerHTML = `${this.#max_velocity.toFixed(1)} m/s`; + pod_svg.getElementById('speed.velocity.max.session.kmh').innerHTML = `${(this.#max_velocity*3.6).toFixed(1)} km/h`; + + // Max run velocity in km/h + pod_svg.getElementById('speed.velocity.max.run.kmh').innerHTML = `${(value*3.6).toFixed(1)} km/h`; + + // Maximal session power + value = frame.Inverter.REAL_POWER / mappings.typeset_telemetry_frame.Factor.Inverter.REAL_POWER + if (value > this.#max_power_real) this.#max_power_real = value + pod_svg.getElementById('power.real.max.session').innerHTML = `${(this.#max_power_real/1000).toFixed(1)} kW`; + + value = frame.Inverter.REACTIVE_POWER / mappings.typeset_telemetry_frame.Factor.Inverter.REACTIVE_POWER + if (value > this.#max_power_reactive) this.#max_power_reactive = value + pod_svg.getElementById('power.reactive.max.session').innerHTML = `${(this.#max_power_reactive).toFixed(1)} kvar`; + + + // Inverter power + [0, 1, 2, 3].forEach(index => { + let current1 = util.adc2ampere(frame.FPGA.FPGA_CURRENT_ADC_VALUES[2 * index]) + let current2 = util.adc2ampere(frame.FPGA.FPGA_CURRENT_ADC_VALUES[2 * index + 1]) + let voltage = frame.Inverter.BOARD_VOLTAGES[index] / mappings.typeset_telemetry_frame.Factor.Inverter.BOARD_VOLTAGES + + let value = (Math.abs(current1 * voltage) + Math.abs(current2 * voltage)) / 1000 + pod_svg.getElementById(`inverter.${index + 1}.power`).innerHTML = `${value.toFixed(2)} kW`; + + }) + + // Battery State + value = mappings.select_flag(mappings.bms_state, frame.HV_Right.HV_R_STATE) + pod_svg.getElementById(`hv.bat_r.state`).innerHTML = `${value}`; + + value = mappings.select_flag(mappings.bms_state, frame.HV_Left.HV_L_STATE) + pod_svg.getElementById(`hv.bat_l.state`).innerHTML = `${value}`; + + // Pod State + value = mappings.select_flag(mappings.main_state, frame.State.STATE) + if (value === "Emergency") { + this.#EMGWarn = true + } else { + this.#EMGWarn = false; + } + + this.#LVWarn = false; + ["LV_Batteries.LV_BAT1_VOLTAGE", + "LV_Batteries.LV_BAT2_VOLTAGE", + "LV_Batteries.LV_BAT1_CURRENT", + "LV_Batteries.LV_BAT2_CURRENT" + ].forEach(path => { + let prot = mappings.select_mapping(mappings.protocol, path) + let value = eval("frame. " + path) / eval("mappings.typeset_telemetry_frame.Factor." + path); + if ((value > prot["max"]) || (value < prot["min"])) { + this.#LVWarn = true; + } + }); + + // Brake State + this.#BrakeWarn = false; + if (frame.Brake.BRAKE_SENSORLEFTENGAGED || frame.Brake.BRAKE_SENSORRIGHTENGAGED) { + this.#BrakeWarn = true; + } + + // Temperature + this.#TempWarn = false; + ["Motor_Temperatures.TEMP_PT100_MAX", + "Inverter.MAX_TEMP", + ].forEach(path => { + let prot = mappings.select_mapping(mappings.protocol, path) + let value = eval("frame. " + path) / eval("mappings.typeset_telemetry_frame.Factor." + path); + if ((value > prot["max"]) || (value < prot["min"])) { + this.#TempWarn = true; + } + }); + + + // Inverter voltage + this.#HVWarn = false; + [0, 1, 2, 3].forEach(i => { + if ((frame.Inverter.BOARD_VOLTAGES[i] / mappings.typeset_telemetry_frame.Factor.Inverter.BOARD_VOLTAGES >= 42)) { + this.#HVWarn = true; + } + }); + + // Isolation + if (frame.HV_Batteries.HV_ISOLATION) { + this.#ISOWarn = false; + } else { + this.#ISOWarn = true; + } + + // Trip + this.#TripWarn = false; + let prot = mappings.select_mapping(mappings.protocol, "Motor_Temperatures.TEMP_PTC_MAX") + if (frame.Motor_Temperatures.TEMP_PTC_MAX / mappings.typeset_telemetry_frame.Factor.Motor_Temperatures.TEMP_PTC_MAX > prot["max"]) { + this.#TripWarn = true; + } + } + + // Pod State Indicator + #EMGWarn = false + #LVWarn = false; + #BrakeWarn = false; + #TempWarn = false + #HVWarn = false; + #ISOWarn = false; + #TripWarn = false; + + indicator_colors = { + r: '#FF0033', + y: '#FFCC33', + g: '#66FF33', + b: '#3399CC', + w: '#DDDDDD', + gray: '#999999', + black: '#000000', + } + + /** + * Update indicators in pod overview with new data from frame + * @param {object} frame Parsed jBinary TelemetryFrame + */ + update_indicators(frame) { + + /** + * Indicator Icons + */ + + // FPGA Phase (frame.FPGA.FPGA_PHASE) + // console.log('iref', util.dec2bin(frame.FPGA.FPGA_IREF, 8)); + for (let i = 0; i < 4; i++) { + for (let j = 0; j < 2; j++) { + let inverterId = i; + switch (inverterId) { // invert inverter mapping + case 0: inverterId = 3; break; + case 1: inverterId = 0; break; + case 2: inverterId = 2; break; + case 3: inverterId = 1; break; + } + let phaseId = inverterId*2 + j; + let alphaPhase = (j === 0 ? 'a' : 'b'); + if(frame.FPGA.FPGA_PHASE & (1 << phaseId)) { + pod_svg.getElementById('motor.phase.' + i + '.' + alphaPhase + '0').style.opacity = 0; + pod_svg.getElementById('motor.phase.' + i + '.' + alphaPhase + '1').style.opacity = 0; + } else { + pod_svg.getElementById('motor.phase.' + i + '.' + alphaPhase + '0').style.opacity = 0.7; + pod_svg.getElementById('motor.phase.' + i + '.' + alphaPhase + '1').style.opacity = 0.7; + } + } + } + + // EMG Warning Icon + if (this.#EMGWarn) { + pod_svg.getElementById('indicators.emg').style.fill = "red"; + pod_svg.getElementById('indicators.emg').style.filter = "url(#f_indicators)"; + pod_svg.getElementById('indicators.emg.anim').style.fill = "#ffea96"; + pod_svg.getElementById('indicators.emg.anim').style.filter = "url(#f_indicators)"; + pod_svg.getElementById('indicators.emg.animation').beginElement(); + } else { + pod_svg.getElementById('indicators.emg').style.fill = "#DEDEDE"; + pod_svg.getElementById('indicators.emg').style.filter = "none"; + pod_svg.getElementById('indicators.emg.anim').style.fill = "none"; + pod_svg.getElementById('indicators.emg.anim').style.filter = "none"; + pod_svg.getElementById('indicators.emg.animation').endElement(); + } + + // LV Warning Icon + if (this.#LVWarn) { + pod_svg.getElementById('indicators.lv').style.fill = "red"; + pod_svg.getElementById('indicators.lv').style.filter = "url(#f_indicators)"; + } else { + pod_svg.getElementById('indicators.lv').style.fill = "#DEDEDE"; + pod_svg.getElementById('indicators.lv').style.filter = "none"; + } + + // Brake Warning Icon + if (this.#BrakeWarn) { + pod_svg.getElementById('indicators.brake').style.fill = "red"; + pod_svg.getElementById('indicators.brake').style.filter = "url(#f_indicators)"; + } else { + pod_svg.getElementById('indicators.brake').style.fill = "#DEDEDE"; + pod_svg.getElementById('indicators.brake').style.filter = "none"; + } + + // Temperature Warning Icon + if (this.#TempWarn) { + pod_svg.getElementById('indicators.temperature').style.fill = "red"; + pod_svg.getElementById('indicators.temperature').style.filter = "url(#f_indicators)"; + } else { + pod_svg.getElementById('indicators.temperature').style.fill = "#DEDEDE"; + pod_svg.getElementById('indicators.temperature').style.filter = "none"; + } + + // HV Warning Icon + if (this.#HVWarn) { + pod_svg.getElementById('indicators.hv').style.fill = "red"; + pod_svg.getElementById('indicators.hv').style.filter = "url(#f_indicators)"; + } else { + pod_svg.getElementById('indicators.hv').style.fill = "#DEDEDE"; + pod_svg.getElementById('indicators.hv').style.filter = "none"; + } + + // Isolation Warning Icon + if (this.#ISOWarn) { + pod_svg.getElementById('indicators.iso').style.fill = "red"; + pod_svg.getElementById('indicators.iso').style.filter = "url(#f_indicators)"; + pod_svg.getElementById("isolation").style.stroke = this.indicator_colors['r']; + pod_svg.getElementById("isolation").style.strokeDasharray = 20; + } else { + pod_svg.getElementById('indicators.iso').style.fill = "#DEDEDE"; + pod_svg.getElementById('indicators.iso').style.filter = "none"; + pod_svg.getElementById("isolation").style.stroke = this.indicator_colors['g']; + pod_svg.getElementById("isolation").style.strokeDasharray = 0; + } + + // Trip Warning Icon + if (this.#TripWarn) { + pod_svg.getElementById('indicators.trip').style.fill = "red"; + pod_svg.getElementById('indicators.trip').style.filter = "url(#f_indicators)"; + } else { + pod_svg.getElementById('indicators.trip').style.fill = "#DEDEDE"; + pod_svg.getElementById('indicators.trip').style.filter = "none"; + } + + /** + * Colors Indicators + */ + + // Inverter Errors + let errors = mappings.gatedriver_status; + let boards = [false, false, false, false]; + errors.forEach(error => { + if ((1 << error.flag) & frame.Inverter.GD_STATUS) { + let board = parseInt(error.description.split("_")[1]) + boards[board] = true; + } + }); + + for (let i = 0; i < 4; ++i) { + if (boards[i] == true) { + pod_svg.getElementById('inverter.' + (i + 1) + '.background').style.fill = this.indicator_colors["r"]; + } else { + pod_svg.getElementById('inverter.' + (i + 1) + '.background').style.fill = this.indicator_colors["g"]; + } + } + + // HV Battery states + switch (mappings.select_flag(mappings.bms_state, frame.HV_Right.HV_R_STATE)) { + case "Charge": + case "Balancing": + pod_svg.getElementById('hv.bat_r.background').style.fill = this.indicator_colors["y"]; + break; + case "Idle": + case "Precharge": + case "Ready": + case "Run": + pod_svg.getElementById('hv.bat_r.background').style.fill = this.indicator_colors["g"]; + break; + case "Disconnected": + pod_svg.getElementById('hv.bat_r.background').style.fill = this.indicator_colors["gray"]; + break; + case "Emergency": + default: + pod_svg.getElementById('hv.bat_r.background').style.fill = this.indicator_colors["r"]; + break; + } + + switch (mappings.select_flag(mappings.bms_state, frame.HV_Left.HV_L_STATE)) { + case "Charge": + case "Balancing": + pod_svg.getElementById('hv.bat_l.background').style.fill = this.indicator_colors["y"]; + break; + case "Idle": + case "Precharge": + case "Ready": + case "Run": + pod_svg.getElementById('hv.bat_l.background').style.fill = this.indicator_colors["g"]; + break; + case "Disconnected": + pod_svg.getElementById('hv.bat_l.background').style.fill = this.indicator_colors["gray"]; + break; + case "Emergency": + default: + pod_svg.getElementById('hv.bat_l.background').style.fill = this.indicator_colors["r"]; + break; + } + + // ICU State + switch (mappings.select_flag(mappings.icu_state, frame.Inverter.ICUMCUSTATE)) { + case "Idle": + case "Setup": + case "Ready": + case "Run": + case "Shutdown": + case "Reset": + pod_svg.getElementById('icu.background').style.fill = this.indicator_colors["g"]; + break; + case "Disconnected": + pod_svg.getElementById('icu.background').style.fill = this.indicator_colors["gray"]; + break; + case "Emergency": + default: + pod_svg.getElementById('icu.background').style.fill = this.indicator_colors["r"]; + break; + } + } +} + +function init(eventEmitter) { + return new Pod_overview(eventEmitter); +} + +module.exports = init; diff --git a/control_panel/js/pod_gui/progress_bar.js b/control_panel/js/pod_gui/progress_bar.js new file mode 100644 index 0000000..bd17b4e --- /dev/null +++ b/control_panel/js/pod_gui/progress_bar.js @@ -0,0 +1,90 @@ +/** + * @file progress_bar.js + * @brief Progress bars in GUI + * + * @author Philip Wiese, philip.wiese@swissloop.ch + * + * @license + * Copyright (C) 2022 Swissloop + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * @module GUI:ProgressBars + * @version 3.0.0 + * + * @listens electron:ipcRenderer~telemetryFrame + * @listens window~load + * + */ + +const $ = require('jquery'); +const {ipcRenderer } = require('electron'); +const mappings = require('../../config/mappings'); +const util = require('../util'); + +class ProgressBars { + /** + * Constructor + * @param eventEmitter App-wide command emitter. + */ + constructor(eventEmitter) { + this.eventEmitter = eventEmitter; + // Add event listeners + window.addEventListener("load", () => { + this.add_event_listeners(); + }); + } + + /** + * Setup listener for new telemetry frame + * @package + */ + add_event_listeners() { + ipcRenderer.on('telemetryFrame', (_event, frame) =>{ + this.update_progress_bars(frame); + }); + } + + /** + * Update progress bars with new data from frame + * @param {object} frame Parsed jBinary TelemetryFrame + */ + update_progress_bars(frame) { + let phase = frame["FPGA"]["FPGA_PHASE"]; + let position = frame["FPGA"]["FPGA_POSITION"]; + let subphase = frame["FPGA"]["FPGA_SUBPHASE"]; + let run_distance = frame["Configuration"]["CONFIG_RUN_DISTANCE"]; + let setPosition = frame["Configuration"]["SETPOSITION"]; + let max_position = 72; + let run_distance_percentage = position / run_distance; + let start_distance_percentage = setPosition / run_distance; + let bar_percentage = start_distance_percentage - run_distance_percentage; + + $('#progress_corrail span').text("Pos: " + position + " / Phase: 0b" + util.dec2bin(phase, 3) + " / Sub: 0b" + util.dec2bin(subphase, 6)); + $("#progress_corrail div.bg-run").css("width: ", 100 + "%"); // + "left:" + start_distance_percentage + "%" + $("#progress_corrail div.bg-brake").css("width: ", 100 + "%"); // + "left:" + start_distance_percentage + "%" + $('#progress_corrail').css("background", `linear-gradient(90deg, #e9ecef ${100*0}%, #aeafb4 ${100*1}%)`); + + // $("#progress_corrail div.bg-run").css("width", 100*Math.min(progress_corrail / track_length, run_distance_percentage) + "%"); + // $("#progress_corrail div.bg-brake").css("width", 100*Math.max(0, (progress_corrail-run_distance) / track_length) + "%"); + // $('#progress_corrail').css("background", `linear-gradient(90deg, #e9ecef ${100*run_distance_percentage}%, #aeafb4 ${100*run_distance_percentage}%)`); + + } +} + +function init(eventEmitter) { + return new ProgressBars(eventEmitter); +} + +module.exports = init; \ No newline at end of file diff --git a/control_panel/js/pod_gui/protocol_table.js b/control_panel/js/pod_gui/protocol_table.js new file mode 100644 index 0000000..1a7e8ac --- /dev/null +++ b/control_panel/js/pod_gui/protocol_table.js @@ -0,0 +1,221 @@ +/** + * @file protocol_table.js + * @brief Protocol table in GUI + * + * @author Philip Wiese, philip.wiese@swissloop.ch + * + * @license + * Copyright (C) 2022 Swissloop + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * @module GUI:ProtocolTable + * @version 3.0.0 + * + * @listens electron:ipcRenderer~telemetryFrame + * @listens window~load + * + */ +const {ipcRenderer } = require('electron'); +const util = require('../util.js'); + +const mappings = require('../../config/mappings.js'); + +const protocol = mappings.protocol; + +class ProtocolTable { + /** + * Constructor + * @param eventEmitter App-wide command emitter. + */ + constructor(eventEmitter) { + this.eventEmitter = eventEmitter; + this.table = document.getElementById("protocol-table-body"); + // Add event listeners + window.addEventListener("load", () => { + this.generate_table(); + this.add_event_listeners(); + }); + } + + /** + * Setup listener for new telemetry frame + * @package + */ + add_event_listeners() { + ipcRenderer.on('telemetryFrame', (_event, frame) =>{ + this.update_table(frame); + }); + } + + /** + * Initially generate table skeleton + * @package + */ + generate_table() { + let offset = protocol.length; + + for (let i = 0; i < offset; i++) { + + // Create an empty element and add it to the i'th position of the table: + let row = this.table.insertRow(i); + + // Insert new cells ( elements) at the 1st and 2nd position of the "new" element: + let name = row.insertCell(0); + let min = row.insertCell(1); + let actual = row.insertCell(2); + let max = row.insertCell(3); + let unit = row.insertCell(4); + + // Add some text to the new cells: + name.innerHTML = protocol[i].alias; + min.innerHTML = protocol[i].min; + max.innerHTML = protocol[i].max; + unit.innerHTML = protocol[i].unit; + } + } + + /** + * Update table with new data from frame + * @param {object} frame Parsed jBinary TelemetryFrame + */ + update_table(frame) { + // Dynamically calculate hv bat max and min voltage + // let num_hv_bats = frame.HV_Batteries.NUMBER_OF_BATTERIES / 2; + // NASTY EHW Hack + let num_hv_bats = 10*8; + let length = protocol.length; + + // Not used 22 + // for (let i = 0; i < protocol.length; i++) { + // if ('Inverter.INVERTER_V_DC_LEFT' === protocol[i].path || 'Inverter.INVERTER_V_DC_RIGHT' === protocol[i].name) { + // protocol[i].max = Math.max((32 * num_active_bats+20), 25).toFixed(0); + // protocol[i].min = Math.max((30 * num_active_bats-20), 0).toFixed(0); + // } + // } + + + for (let i = 0; i < length; i++) { + if ('HV_Left.HV_L_VOLTAGE' === protocol[i].path) { + protocol[i].max = (4.15 * num_hv_bats).toFixed(0); + protocol[i].min = (3.7 * num_hv_bats).toFixed(0); + } + if ('HV_Right.HV_R_VOLTAGE' === protocol[i].path) { + protocol[i].max = (4.15 * num_hv_bats).toFixed(0); + protocol[i].min = (3.7 * num_hv_bats).toFixed(0); + } + } + + protocol.forEach( (element, i) => { + let category = element.path.split(".")[0]; + let name = element.path.split(".")[1]; + let array_idx = element.path.split(".")[2]; + let precision = element.precision; + + let factor= mappings.typeset_telemetry_frame.Factor[category][name]; + let value = frame[category][name]/factor; + + // Handle the Currents Array correctly + if(element.path === "FPGA.FPGA_CURRENT_ADC_VALUES") { + value = util.adc2ampere(frame[category][name][array_idx]); + } + if(element.path === "Inverter.BOARD_VOLTAGES") { + value = frame[category][name][array_idx]; + } + if(element.path === "HV_Right.HV_R_VOLTAGE") { + this.table.rows[i].cells[1].innerHTML = protocol[i].min; + this.table.rows[i].cells[3].innerHTML = protocol[i].max; + } + if(element.path === "HV_Left.HV_L_VOLTAGE") { + this.table.rows[i].cells[1].innerHTML = protocol[i].min; + this.table.rows[i].cells[3].innerHTML = protocol[i].max; + } + let brake_status = mappings.select_flag(mappings.brake_status, frame.Brake.BRAKE_ENGAGE); + if (element.path === "Brake.BRAKE_LEFT_ACTION") { + if (brake_status === "Engaged") { + this.table.rows[i].cells[1].innerHTML = protocol[i].min; + this.table.rows[i].cells[3].innerHTML = protocol[i].max; + } else { + this.table.rows[i].cells[1].innerHTML = 0; + this.table.rows[i].cells[3].innerHTML = 0.2; + } + } + if (element.path === "Brake.BRAKE_RIGHT_ACTION") { + if (brake_status === "Engaged") { + this.table.rows[i].cells[1].innerHTML = protocol[i].min; + this.table.rows[i].cells[3].innerHTML = protocol[i].max; + } else { + this.table.rows[i].cells[1].innerHTML = 0; + this.table.rows[i].cells[3].innerHTML = 0.2; + } + } + if (element.path === "Brake.BRAKE_LEFT_RELEASE") { + if (brake_status === "Engaged") { + let brake_status = mappings.select_flag(mappings.brake_status, frame.Brake.BRAKE_ENGAGE); + this.table.rows[i].cells[1].innerHTML = 0; + this.table.rows[i].cells[3].innerHTML = 0.2; + } else { + this.table.rows[i].cells[1].innerHTML = protocol[i].min; + this.table.rows[i].cells[3].innerHTML = protocol[i].max; + } + } + if (element.path === "Brake.BRAKE_RIGHT_RELEASE") { + if (brake_status === "Engaged") { + let brake_status = mappings.select_flag(mappings.brake_status, frame.Brake.BRAKE_ENGAGE); + this.table.rows[i].cells[1].innerHTML = 0; + this.table.rows[i].cells[3].innerHTML = 0.2; + } else { + this.table.rows[i].cells[1].innerHTML = protocol[i].min; + this.table.rows[i].cells[3].innerHTML = protocol[i].max; + } + } + + if (element.path.split('.')[1] === "BOARD_VOLTAGES") { + let main_state = mappings.select_flag(mappings.main_state, frame.State.STATE); + if (main_state === "Idle"){ // || main_state === "Run" || main_state === "Breaking" || main_state === "Stop" || main_state === "Crawl" + this.table.rows[i].cells[1].innerHTML = 0; + this.table.rows[i].cells[3].innerHTML = 10; + } else { + this.table.rows[i].cells[1].innerHTML = protocol[i].min + this.table.rows[i].cells[3].innerHTML = protocol[i].max; + } + } + + let element_value = this.table.rows[i].cells[2]; + let min = parseFloat(this.table.rows[i].cells[1].innerHTML); + let max = parseFloat(this.table.rows[i].cells[3].innerHTML); + + if (value < min || max < value) { + element_value.style.backgroundColor = "#FF0000"; + }else { + element_value.style.backgroundColor = "#4CAF50"; + + // Special color for brake values + if (element.path === "Brake.BRAKE_RIGHT_RELEASE" || element.path === "Brake.BRAKE_RIGHT_ACTION" || + element.path === "Brake.BRAKE_LEFT_RELEASE" || element.path === "Brake.BRAKE_LEFT_ACTION") { + element_value.style.backgroundColor = "#ff7500"; + } + } + + + element_value.innerHTML = value.toFixed(precision); + }) + } +} + +function init(eventEmitter) { + return new ProtocolTable(eventEmitter); +} + +module.exports = init; \ No newline at end of file diff --git a/control_panel/js/pod_gui/renderer.js b/control_panel/js/pod_gui/renderer.js new file mode 100644 index 0000000..1597311 --- /dev/null +++ b/control_panel/js/pod_gui/renderer.js @@ -0,0 +1,338 @@ +/** + * @file renderer.js + * @brief Main renderer for GUI + * + * @author Philip Wiese, philip.wiese@swissloop.ch + * + * @license + * Copyright (C) 2022 Swissloop + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * @module GUI:Pod + * @version 3.0.0 + * + * @listens window~keydown + * + * @emits electron:ipcRenderer~startLogging + * @emits electron:ipcRenderer~stopLogging + * + */ + +const electronLocalShortcut = require('electron-localshortcut'); +const { ipcRenderer } = require('electron') +const mainWindow = require('@electron/remote').getCurrentWindow(); +const EventEmitter = require('events').EventEmitter; + +let eventEmitter = new EventEmitter(); +eventEmitter.setMaxListeners(Infinity); + +// Initialize & load modules +require('./progress_bar.js')(eventEmitter); +require('./protocol_table.js')(eventEmitter); +require('./error_table.js')(eventEmitter); +require('./hv_bat_tables')(eventEmitter); +require('./pod_control')(eventEmitter); +require('./config_table.js')(eventEmitter); +require('./state_table.js')(eventEmitter); +require('./icu.js')(eventEmitter); +require('./pod_overview.js')(eventEmitter); +require('./testing_area.js')(eventEmitter); + +const util = require('../util.js') +const mappings = require('../../config/mappings.js') + +const config = require('../../config/config'); + +ipcRenderer.setMaxListeners(Infinity); + +// Log errors to console: +ipcRenderer.on('error', (_event, args) => { + console.log(...args) +}); + +ipcRenderer.on('console_log', (_event, args) => { + console.log(...args) +}); + +// MANUAL KEYBOARD CONTROLS +let last_space_timestamp = 0; +electronLocalShortcut.register(mainWindow, 'B', () => { + // emergency on double press + if (Date.now() - last_space_timestamp < 1000) { + ipcRenderer.send('sendCtrlFrame',{"vcu_set_state": 7}); + } else { + last_space_timestamp = Date.now(); + } +}); + +let command = 1; +let crawlEnabled = false; + +// Prevent from scrolling and emergency on space +mainWindow.webContents.on("before-input-event", function (_event, e) { + if (e.type === "keyDown") { + // Forward + if (e.code === "KeyW" || e.code === "KeyD") { + command = 2; + } + // Backwards + if (e.code === "KeyS" || e.code === "KeyA") { + command = 3; + } + if ( e.code === "Space") { + if (Date.now() - last_space_timestamp < 1000) { + ipcRenderer.send('sendCtrlFrame',{"vcu_set_state": 7}); + } else { + last_space_timestamp = Date.now(); + } + } + } + if (e.type === "keyUp") { + if (e.code === "KeyW" || e.code === "KeyS" ) { + command = 1; + } + } +}, true); + +document.documentElement.addEventListener('keydown', function (e) { + if (e.code === "Space") { + e.preventDefault(); + } +}); + +// Send direction commands with heartbeat frequency +setInterval( () => { + if (crawlEnabled) ipcRenderer.send('sendCtrlFrame',{"crawl_cmd": command}); + if (crawlEnabled) console.log(command); +}, config.communication.heartbeat_freq); + + +const logViewer_button = $("#logViewer_button"); +const logging_button = $("#logging_button"); +const setup_button = $("#setup_button"); +const idle_button = $("#idle_button"); +const run_button = $("#run_button"); +const run_button_modal = $("#run_button_modal"); +const emergency_button = $("#emergency_stop_button"); + +const no_conn_overlay = $('#no_connection_overlay'); +const no_conn_music = document.getElementById("no_connection_music"); +const conn_music = document.getElementById("connection_music"); +const success_music = document.getElementById("success_music"); +const emergency_music = document.getElementById("emergency_music"); + +// Add event listeners for buttons +logViewer_button.on("click", () => { + ipcRenderer.send("openLogViewer"); +}) +logging_button.on("click", () => { + if (!this.isLogging) { + ipcRenderer.send("startLogging"); + eventEmitter.emit("startLogging"); + logging_button.html("Stop Local Logging"); + this.isLogging = true; + } else { + ipcRenderer.send("stopLogging"); + eventEmitter.emit("stopLogging"); + logging_button.html("Start Local Logging"); + this.isLogging = false; + } +}); +idle_button.on("click", () => { + ipcRenderer.send('sendCtrlFrame',{"vcu_set_state": 1}); +}); +setup_button.on("click", () => { + if (setup_button.html() === "SETUP") { + ipcRenderer.send('sendCtrlFrame',{"vcu_set_state": 2}); + } else { + // Go to shutdown if we cancel the setup or go to shutdown after emergency + ipcRenderer.send('sendCtrlFrame',{"vcu_set_state": 6}); + } + +}); +run_button.on("click", () => { + $('#runModal').modal('hide'); + ipcRenderer.send('sendCtrlFrame',{"vcu_set_state": 4}); +}); +emergency_button.on("click", () => { + ipcRenderer.send('sendCtrlFrame',{"vcu_set_state": 7}); +}); +ipcRenderer.on('telemetryFrame', (_event, frame) =>{ + setup_button.text("SETUP"); + switch(frame.State.STATE) { + // IDLE + case 1: + idle_button.attr("disabled", false); + setup_button.attr("disabled", false); + run_button.attr("disabled", true); + run_button_modal.attr("disabled", true); + emergency_button.attr("disabled", false); + break; + // SETUP + case 2: + setup_button.text("CANCEL SETUP"); + idle_button.attr("disabled", true); + setup_button.attr("disabled", false); + run_button.attr("disabled", true); + run_button_modal.attr("disabled", true); + emergency_button.attr("disabled", false); + break; + // READY + case 3: + idle_button.attr("disabled", true); + setup_button.attr("disabled", false); + run_button.attr("disabled", false); + run_button_modal.attr("disabled", false); + emergency_button.attr("disabled", false); + break; + // RUN + case 4: + idle_button.attr("disabled", true); + setup_button.attr("disabled", true); + run_button.attr("disabled", true); + run_button_modal.attr("disabled", true); + emergency_button.attr("disabled", false); + break; + // BRAKING + case 5: + idle_button.attr("disabled", true); + setup_button.attr("disabled", true); + run_button.attr("disabled", true); + run_button_modal.attr("disabled", true); + emergency_button.attr("disabled", false); + break; + // SHUTDOWN + case 6: + idle_button.attr("disabled", false); + setup_button.attr("disabled", true); + run_button.attr("disabled", true); + run_button_modal.attr("disabled", true); + emergency_button.attr("disabled", false); + break; + // EMERGENCY + case 7: + setup_button.text("SHUTDOWN"); + idle_button.attr("disabled", false); + setup_button.attr("disabled", false); + run_button.attr("disabled", true); + run_button_modal.attr("disabled", true); + emergency_button.attr("disabled", false); + break; + default: + idle_button.attr("disabled", true); + setup_button.attr("disabled", true); + run_button.attr("disabled", true); + run_button_modal.attr("disabled", true); + emergency_button.attr("disabled", false); + break; + } + + // Handle automatic logging + if (config.logging.start_automatic && !this.isLogging) { + let main_name = mappings.select_flag(mappings.main_state, frame.State.STATE) + if (main_name === config.logging.start_state) { + setTimeout(function(){ + ipcRenderer.send("startLogging"); + eventEmitter.emit("startLogging"); + logging_button.html("Stop Local Logging"); + this.isLogging = true; + }.bind(this), config.logging.start_delay); + } + } + + if (config.logging.stop_automatic && this.isLogging) { + let main_name = mappings.select_flag(mappings.main_state, frame.State.STATE) + if (main_name === config.logging.stop_state) { + setTimeout(function(){ + ipcRenderer.send("stopLogging"); + eventEmitter.emit("stopLogging"); + logging_button.html("Start Local Logging"); + this.isLogging = false; + }.bind(this), config.logging.stop_delay); + } + } +}); + +// Event emitter for riot-tags +ipcRenderer.on('telemetryFrame', (_event, frame) =>{ + // eventEmitter.emit("CORRAIL_VELOCITY", {value: frame["Corrail"]["CORRAIL_VELOCITY"]/typeset_telemetry_frame.Factor["Corrail"]["CORRAIL_VELOCITY"]*3.6}); + // eventEmitter.emit("CORRAIL_MAX_VELOCITY", {value: frame["Corrail"]["CORRAIL_MAX_VELOCITY"]/typeset_telemetry_frame.Factor["Corrail"]["CORRAIL_MAX_VELOCITY"]*3.6}); + let state = util.json_array_select_value(mappings.main_state, frame["State"]["STATE"]); + eventEmitter.emit("mainStatus", state.description); +}); + +let shutdown_state_transition = false; +let emergency_state_transition = false; +// Overlay +$(document).ready(function() { + if (config.music.waiting) { + no_conn_music.play(); + no_conn_music.loop = true; + } + ipcRenderer.on('missingHeartbeat', (function () { + if (no_conn_overlay.css('display') === 'none') { + if (config.music.waiting) { + no_conn_music.pause(); + no_conn_music.currentTime = 0 + no_conn_music.play(); + } + no_conn_overlay.show(); + } + })); + + // Connection status: + ipcRenderer.on('heartbeat', (function () { + if (no_conn_overlay.css('display') !== 'none') { + if (config.music.waiting) no_conn_music.pause(); + if (config.music.onConnection) { + conn_music.currentTime = 0 + conn_music.play(); + } + no_conn_overlay.hide(); + } + })); + + eventEmitter.on('mainStatus', (function(state) { + if (state === "Shutdown") { + if (shutdown_state_transition == false) { + shutdown_state_transition = true; + if (config.music.onSuccess) { + success_music.currentTime = 0; + success_music.play(); + } + } + }else if (state === "Emergency") { + if (emergency_state_transition == false) { + emergency_state_transition = true; + if (config.music.onEmergency) { + emergency_music.currentTime = 0; + emergency_music.play(); + } + } + } else { + shutdown_state_transition = false; + emergency_state_transition = false; + if (config.music.onSuccess) success_music.pause(); + if (config.music.onEmergency) emergency_music.pause(); + } + })); +}); + + +// Mount status display: +riot.mount('status-display', { + "commandEmitter": eventEmitter, + "ipcRenderer": ipcRenderer +}); \ No newline at end of file diff --git a/control_panel/js/pod_gui/state_table.js b/control_panel/js/pod_gui/state_table.js new file mode 100644 index 0000000..e0fe28f --- /dev/null +++ b/control_panel/js/pod_gui/state_table.js @@ -0,0 +1,137 @@ +/** + * @file state_table.js + * @brief State table in GUI + * + * @author Hanno Hiss, hanno.hiss@swissloop.ch + * @author Philip Wiese, philip.wiese@swissloop.ch + * + * @license + * Copyright (C) 2022 Swissloop + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * @module GUI:State_Table + * @version 3.0.0 + * + * @listens electron:ipcRenderer~telemetryFrame + * @listens window~load + * + */ + +const $ = require('jquery'); +const {ipcRenderer } = require('electron') + +const mappings = require('../../config/mappings.js'); +const util = require('../util.js') + +class StateTable { + /** + * Constructor + * @param eventEmitter App-wide command emitter. + */ + constructor(eventEmitter) { + this.eventEmitter = eventEmitter; + // Add event listeners + window.addEventListener("load", () => { + this.generate_table(); + this.add_event_listeners(); + }); + $(document).ready(function(){ + $("[rel='tooltip'], .tooltip").tooltip(); + }); + + this.state_list = mappings.enum_list.filter(value => value.type === "FSM") + } + + /** + * Setup listener for new telemetry frame + * @package + */ + add_event_listeners() { + ipcRenderer.on('telemetryFrame', (_event, frame) =>{ + this.update_table(frame); + }); + } + + /** + * Initially generate table skeleton + * @package + */ + generate_table() { + this.table = document.getElementById("state-table-body"); + + for (let j = 0; j < this.state_list.length; j ++) { + // Create an empty element and add it to the i'th position of the table: + let row = this.table.insertRow(j); + + // Insert new cells ( elements) at the 1st and 2nd and 3rd position of the "new" element: + let name = row.insertCell(0); + let state = row.insertCell(1); + let ready = row.insertCell(2); + let state_param = this.state_list[j]; + + // Add some text to the new cells: + name.innerHTML = state_param.name; + state.innerHTML = "not init"; + } + } + + /** + * Update table with new data from frame + * @param {object} frame Parsed jBinary TelemetryFrame + */ + update_table(frame) { + // let length = this.state_list.length; + + this.state_list.forEach( (element, i) => { + let category = element.path.split(".")[0]; + let name = element.path.split(".")[1]; + let value = frame[category][name]; + let ready_val = undefined; + + let table_cell = this.table.rows[i].cells[1] + let ready_cell = this.table.rows[i].cells[2] + let state = util.json_array_select_value(element.enum, value) + + // get ready value for system + if (element.hasOwnProperty('ready')) { + // if ready path is defined + let ready_cat = element.ready.split(".")[0]; + let ready_name = element.ready.split(".")[1]; + ready_val = frame[ready_cat][ready_name]; + } else if (element.path === "State.STATE") { + // VCU has Ready low only in Emergency state + if (state.description == "Emergency") { + ready_val = 0; + } else { + ready_val = 1; + } + } else { + // set ready value so displays NaN + ready_val = 2; + } + + // set HTML text + table_cell.innerHTML = state.description; + ready_cell.innerHTML = ready_val; + + }); + } +} + +function init(eventEmitter) { + return new StateTable(eventEmitter); +} + +module.exports = init; \ No newline at end of file diff --git a/control_panel/js/pod_gui/testing_area.js b/control_panel/js/pod_gui/testing_area.js new file mode 100644 index 0000000..19e7f57 --- /dev/null +++ b/control_panel/js/pod_gui/testing_area.js @@ -0,0 +1,285 @@ +/** + * @file testing_area.js + * @brief Testing are in GUI + * + * @author Philip Wiese, philip.wiese@swissloop.ch + * + * @license + * Copyright (C) 2022 Swissloop + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * @module GUI:TestingArea + * @version 3.0.0 + * + * @listens electron:ipcRenderer~telemetryFrame + * + */ + +const $ = require('jquery'); +const {ipcRenderer } = require('electron') + +// To parse and create telemetry data based on typesets +const jBinary = require('jbinary'); + +const mappings = require('../../config/mappings.js'); + +const ignore =[ + "Sync.SYNC", +]; + +const table_telemetry = document.getElementById(`testing_area_body_telemetry`); +const table_ctrl = document.getElementById(`testing_area_body_ctrl`); + +// Time until input boxes are updated automatically +const selected_time = 2000; + +class TestingArea { + /** + * Constructor + * @param eventEmitter App-wide command emitter. + */ + constructor(eventEmitter) { + this.eventEmitter = eventEmitter; + // Add event listeners + window.addEventListener("load", () => { + this.generate_area(); + this.add_event_listeners(); + }); + } + + /** + * Setup listener for new telemetry frame + * @package + */ + add_event_listeners() { + ipcRenderer.on('telemetryFrame', (_event, frame) =>{ + this.update_gui(frame); + }); + } + + /** + * Initially generate area skeleton + * @package + */ + generate_area() { + // Generate Table for Control Frame + for (let key in mappings.typeset_ctrl_frame.Data) { + if (!mappings.typeset_ctrl_frame.Data.hasOwnProperty(key)) { + continue; + } + + let row = table_ctrl.insertRow(); + let unit = mappings.typeset_ctrl_frame.Unit[key]; + + let cell_name = row.insertCell(0); + let cell_value = row.insertCell(1); + let cell_input = row.insertCell(2); + let cell_tx = row.insertCell(3); + let cell_note1 = row.insertCell(4); + + cell_name.innerHTML = key; + cell_tx.innerHTML = `` + cell_note1.innerHTML = unit; + + let mapping = mappings.select_mapping(mappings.config_values, `${key}`) + + // If value is an enumeration + if (mapping !== undefined) { + cell_value.innerHTML =`0`; + + if (mapping.hasOwnProperty('enum')) { + let selectList = document.createElement("select"); + selectList.id = `testing_area_config_${key}_input`; + selectList.classList.add(...["custom-select", "mr-sm-2", "form-control"]); + + // Skip first state (UNDEFINED) + for (let i = 0; i < mapping.enum.length; i++) { + let option = document.createElement("option"); + option.value = mapping.enum[i].flag; + option.text = `${mapping.enum[i].description} (${option.value})`; + selectList.appendChild(option); + } + cell_input.appendChild(selectList); + } else{ + cell_input.innerHTML = `` + } + } else { + // Check for arrays + if (typeof mappings.typeset_ctrl_frame.Data[key] == "object") { + for (let j = 0; j < mappings.typeset_ctrl_frame.Data[key][2]; ++j) { + cell_input.innerHTML += `` + } + // Add keypress event listeners + for (let j = 0; j < mappings.typeset_ctrl_frame.Data[key][2]; ++j) { + $(`#testing_area_config_${key}_${j}_input`).keypress( () => { + var keycode = (event.keyCode ? event.keyCode : event.which); + if(keycode == '13'){ + let raw_frame = new jBinary(mappings.typeset_ctrl_frame.Length, mappings.typeset_ctrl_frame); + let ctrl_frame = raw_frame.readAll() + for (let i = 0; i < mappings.typeset_ctrl_frame.Data[key][2]; ++i) { + ctrl_frame[key][i] = $(`#testing_area_config_${key}_${i}_input`).val()*1 + // Send -1 for active 0 values + if (key == "inverter_gain_i" || key == "inverter_gain_p") { + ctrl_frame[key][i] = (ctrl_frame[key][i] == 0) ? -1: ctrl_frame[key][i]; + } + } + ipcRenderer.send('sendCtrlFrame', JSON.parse(JSON.stringify(ctrl_frame))); + } + }); + } + // Add Send button click events + $(`#testing_area_config-${key}_send`).on("click", () => { + let raw_frame = new jBinary(mappings.typeset_ctrl_frame.Length, mappings.typeset_ctrl_frame); + let ctrl_frame = raw_frame.readAll() + for (let i = 0; i < mappings.typeset_ctrl_frame.Data[key][2]; ++i) { + ctrl_frame[key][i] = $(`#testing_area_config_${key}_${i}_input`).val()*1 + // Send -1 for active 0 values + if (key == "inverter_gain_i" || key == "inverter_gain_p") { + ctrl_frame[key][i] = (ctrl_frame[key][i] == 0) ? -1: ctrl_frame[key][i]; + } + } + ipcRenderer.send('sendCtrlFrame', JSON.parse(JSON.stringify(ctrl_frame))); + }); + + continue; + } else { + cell_input.innerHTML = `` + } + } + // Add keypress event listeners + $(`#testing_area_config-${key}_send`).on("click", () => { + let ctrl_frame = {} + ctrl_frame[key] = $(`#testing_area_config_${key}_input`).val()*1 + // Send -1 for active 0 values + if ( + key == "config_current_setpoint_run" || + key == "config_current_setpoint_crawl" || + key == "config_run_velocity" + ) { + ctrl_frame[key] = (ctrl_frame[key] == 0) ? -1: ctrl_frame[key]; + } + ipcRenderer.send('sendCtrlFrame',ctrl_frame); + }); + // Add Send button click events + $(`#testing_area_config_${key}_input`).keypress( () => { + var keycode = (event.keyCode ? event.keyCode : event.which); + if(keycode == '13'){ + let ctrl_frame = {} + ctrl_frame[key] = $(`#testing_area_config_${key}_input`).val()*1 + if ( + key == "config_current_setpoint_run" || + key == "config_current_setpoint_crawl" || + key == "config_run_velocity" + ) { + ctrl_frame[key] = (ctrl_frame[key] == 0) ? -1: ctrl_frame[key]; + } + ipcRenderer.send('sendCtrlFrame',ctrl_frame); + } + }); + } + // Generate Table for Telemetry Frame + for (let category in mappings.typeset_telemetry_frame.Data) { + // Check for property + if (!mappings.typeset_telemetry_frame.Data.hasOwnProperty(category)) { + continue; + } + + for (let key in mappings.typeset_telemetry_frame.Data[category]) { + // Check for property + if (!mappings.typeset_telemetry_frame.Data[category].hasOwnProperty(key)) { + continue; + } + + let row = table_telemetry.insertRow(); + + let unit = mappings.typeset_telemetry_frame.Unit[category][key]; + if (ignore.includes(category + "." + key) > 0) { + row.classList.add("table-inactive") + } + let cell_name = row.insertCell(0); + let cell_value = row.insertCell(1); + cell_value.id = `testing_area_${category}-${key}`; + let cell_unit = row.insertCell(2); + + cell_name.innerHTML = key; + cell_unit.innerHTML = unit; + } + } + } + + /** + * Update area with new data from frame + * @param {object} frame Parsed jBinary TelemetryFrame + */ + update_gui(frame) { + // Update config table + for (let key in mappings.typeset_ctrl_frame.Data) { + let mapping = mappings.select_mapping(mappings.config_values, `${key}`) + + if (mapping === undefined) { + continue; + } + let category = mapping.path_telemetry.split(".")[0]; + let name = mapping.path_telemetry.split(".")[1]; + let value = frame[category][name]; + + $(`#testing_area_config_${key}_label`).text(value); + } + + // Updated telemetry table + for (let category in frame) { + // Check for property + if (!mappings.typeset_telemetry_frame.Data.hasOwnProperty(category)) { + continue; + } + + for (let key in frame[category]) { + // Check for property + if (!mappings.typeset_telemetry_frame.Data[category].hasOwnProperty(key)) { + continue; + } + if (ignore.includes(category + "." + key) > 0) continue; + + let unit = mappings.typeset_telemetry_frame.Unit[category][key]; + let factor = mappings.typeset_telemetry_frame.Factor[category][key]; + let cell_value = $(`#testing_area_${category}-${key}`); + + // Check for array + if (typeof(mappings.typeset_telemetry_frame.Data[category][key]) === "object") { + let value = "" + for (let item in frame[category][key]) { + let temp = (unit === "enum") ? "0x" + (frame[category][key][item]/factor).toString(16).toUpperCase() : frame[category][key][item]/factor; + value += temp + ", "; + } + cell_value.html(value.substring(0, value.length-2)) + } else { + let value = mappings.select_mapping(mappings.enum_list, `${category}.${key}`) + if (value !== undefined) { + value = mappings.select_flag(value.enum, frame[category][key]) + } else { + value = (unit === "enum") ? "0x" + (frame[category][key]/factor).toString(16).toUpperCase() : frame[category][key]/factor; + } + cell_value.html(value) + } + } + } + } +} + +function init(eventEmitter) { + return new TestingArea(eventEmitter); +} + +module.exports = init; \ No newline at end of file diff --git a/control_panel/js/typesets/typeset_ctrl_frame.js b/control_panel/js/typesets/typeset_ctrl_frame.js new file mode 100644 index 0000000..705675c --- /dev/null +++ b/control_panel/js/typesets/typeset_ctrl_frame.js @@ -0,0 +1,141 @@ +/** + * @file typeset_ctrl_frame.js + * @brief jBinary control frame Typeset + * + * @author Philip Wiese, philip.wiese@swissloop.ch + * + * @license + * Copyright (C) 2022 Swissloop + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * @module TelemetryTypeset + * @version 3.0.0 + * + */ + +const moment = require('moment'); +const jBinary = require('jbinary'); + +const frame_ctrl_typeset = +{ + 'jBinary.all': 'Data', + 'jBinary.littleEndian': true, + DateTimeFatFS: jBinary.Type({ + read: function () { + let rawValue = this.binary.read('uint32'); + let year = ((rawValue >> 25) & 0x3F) + 1980; + let month = ((rawValue >> 21) & 0x0F) - 1; + let date = ((rawValue >> 16) & 0x1F); + let hour = ((rawValue >> 11) & 0x1F); + let min = ((rawValue >> 5 ) & 0x3F); + let sec = ((rawValue >> 0 ) & 0x1F); + return moment([year, month, date, hour, min, sec]); + }, + write: function () { + let rawValue = 0; + let time= moment(); + rawValue = rawValue | (((time.year() - 1980) & 0x3F) << 25); + rawValue = rawValue | (((time.month() + 1) & 0x0F) << 21); + rawValue = rawValue | ((time.date() & 0x1F) << 16); + rawValue = rawValue | ((time.hour() & 0x1F) << 11); + rawValue = rawValue | ((time.minute() & 0x3F) << 5 ); + rawValue = rawValue | ((time.second() & 0x1F) << 0 ); + this.binary.write('uint32', rawValue); + } + }), + Length: 83, + Data: { + config_runType: 'uint8', + config_pwmMethod: 'uint8', + config_bandwidthLow: 'float', + config_bandwidthHigh: 'float', + config_zeroCurrent: 'float', + config_targetCurrent: 'float', + config_maxVoltage: 'float', + config_overCurrentLimit_lo: 'float', + config_overCurrentLimit_hi: 'float', + config_ADC_MaxOffset: 'float', + config_ADC_MaxNoiseRange: 'float', + config_runForward: 'uint8', + config_setPosition: 'uint8', + config_runLength: 'uint8', + config_runDuration: 'float', + config_trackLength: 'uint8', + config_propTrackLength: 'uint8', + config_Temp2startFan: 'float', + config_maxMosfetTemp: 'float', + config_brake_state: 'uint8', + config_controlled_Braking_pressure: 'float', + config_number_of_batteries: 'uint8', // 44 + config_rideLength: 'float', + config_NumInvertersEnabled: 'uint8', + reset_run: 'uint8', + fat_time: 'DateTimeFatFS', // 13 + 4 + sd_card_flush: 'uint8', + brake_engage: 'uint8', + vcu_set_state: 'uint16', + pod_reset_error: 'uint8', + vcu_software_reset: 'uint8', + bms_software_reset_left: 'uint8', + bms_software_reset_right: 'uint8', + bms_balancing: 'uint8', + inverter_software_reset: 'uint8', + inverter_adc_calibrate: 'uint8', + inverter_do_foo: 'uint8', + }, + Unit: { + config_runType: 'enum', + config_pwmMethod: 'enum', + config_bandwidthLow: 'A', + config_bandwidthHigh: 'A', + config_zeroCurrent: 'A', + config_targetCurrent: 'A', + config_maxVoltage: 'V', + config_overCurrentLimit_lo: 'A', + config_overCurrentLimit_hi: 'A', + config_ADC_MaxOffset: 'A', + config_ADC_MaxNoiseRange: 'A', + config_runForward: 'boolean', + config_setPosition: 'number', + config_runLength: 'm', + config_runDuration: 'ms', + config_trackLength: 'm', + config_propTrackLength: 'number', + config_Temp2startFan: 'C', + config_maxMosfetTemp: 'C', + config_brake_state: 'enum', + config_controlled_Braking_pressure: 'bar', + config_number_of_batteries: 'number', + config_rideLength: 'm', + config_NumInvertersEnabled: 'number', + reset_run: 'boolean', + fat_time: 'DateTimeFatFS', + sd_card_flush: 'boolean', + brake_engage: 'boolean', + vcu_set_state: 'enum', + pod_reset_error: 'boolean', + vcu_software_reset: 'boolean', + bms_software_reset_left: 'boolean', + bms_software_reset_right: 'boolean', + bms_balancing: 'boolean', + inverter_software_reset: 'boolean', + inverter_adc_calibrate: 'boolean', + inverter_do_foo: 'boolean', + } +}; + +module.exports = { + "typeset": frame_ctrl_typeset +}; diff --git a/control_panel/js/typesets/typeset_telemetry_frame.js b/control_panel/js/typesets/typeset_telemetry_frame.js new file mode 100644 index 0000000..ed7a9f0 --- /dev/null +++ b/control_panel/js/typesets/typeset_telemetry_frame.js @@ -0,0 +1,482 @@ +/** + * @file typeset_telemetry_frame.js + * @brief jBinary telemetry frame Typeset + * + * @author Philip Wiese, philip.wiese@swissloop.ch + * + * @license + * Copyright (C) 2022 Swissloop + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * @module TelemetryTypeset + * @version 3.0.0 + * + */ + +const frame_telemetry_typeset = +{ + 'jBinary.all': 'Data', + 'jBinary.littleEndian': true, + Length: 632, + SyncWord: 'FECA', + Data: { + Sync: { SYNC: 'uint16' }, + State: { + STATE: 'uint8', + VCU_EMERGENCY_REASON: 'uint32', + VCU_ERRORS: 'uint32', + RUN_TIMER: 'uint32', + LOG_FILE_NUM: 'uint16', + LOG_DISCARDED_DATA: 'uint8' + }, + Brake: { + BRAKE_ENGAGE: 'uint8', + BRAKE_STATE: 'uint8', + BRAKE_VALVEEMERGENCYBRAKE: 'uint8', + BRAKE_VALVEENGAGEBRAKE: 'uint8', + BRAKE_CONTROLLEDBRAKINGPRESSURE: 'float32', + BRAKE_PRESSUREREGULATORIN: 'float32', + BRAKE_SENSORLEFTENGAGED: 'uint8', + BRAKE_SENSORRIGHTENGAGED: 'uint8', + BRAKE_BRAKEDISTANCE: 'uint16', + BRAKE_BRAKEENGAGETIMELEFT: 'uint16', + BRAKE_BRAKEENGAGETIMERIGHT: 'uint16', + BRAKE_TANK: 'uint32', + BRAKE_LEFT_ACTION: 'uint16', + BRAKE_LEFT_RELEASE: 'uint16', + BRAKE_RIGHT_ACTION: 'uint16', + BRAKE_RIGHT_RELEASE: 'uint16' + }, + Velocity: { + CORRAIL_VELOCITY: 'int32', + CORRAIL_DISTANCE: 'uint32', + CORRAIL_MAX_VELOCITY: 'uint32', + CORRAIL_OFFSET_AFTER_RESET: 'uint32', + GAM900_ACCELERATION_X: 'int32', + GAM900_ACCELERATION_Y: 'int32', + COMBINED_VELOCITY: 'int16' + }, + OM20: { + OM20_FRONT_FINDOWN: 'uint16', + OM20_FRONT_FINLEFT: 'uint32', + OM20_FRONT_FINRIGHT: 'uint32', + OM20_FRONT_CHASSISLEFT: 'uint16', + OM20_FRONT_CHASSISRIGHT: 'uint16', + OM20_BACK_FINDOWN: 'uint16', + OM20_BACK_FINLEFT: 'uint32', + OM20_BACK_FINRIGHT: 'uint32', + OM20_BACK_CHASSISLEFT: 'uint16', + OM20_BACK_CHASSISRIGHT: 'uint16' + }, + Motor_Temperatures: { + TEMP_PT100_2: [ 'array', 'uint8', 7 ], + TEMP_PTC_1: [ 'array', 'uint8', 8 ], + TEMP_PT100_MAX: 'uint8', + TEMP_PTC_MAX: 'uint8' + }, + LV_Batteries: { + LV_BAT1_CURRENT: 'float32', + LV_BAT1_VOLTAGE: 'float32', + LV_BAT1_POWER: 'float32', + LV_BAT2_CURRENT: 'float32', + LV_BAT2_VOLTAGE: 'float32', + LV_BAT2_POWER: 'float32' + }, + HV_Batteries: { + HV_ISOLATION: 'uint8', + HV_DO_PRECHARGE: 'uint8', + HV_PRECHARGE_DONE: 'uint8', + NUMBER_OF_BATTERIES: 'uint8' + }, + HV_Left: { + HV_L_READY: 'uint8', + HV_L_ERROR: 'uint32', + HV_L_STATE: 'uint8', + HV_L_BALANCING: 'uint8', + HV_L_VOLTAGE: 'uint16', + HV_L_CURRENT: 'int16', + HV_L_MIN_CELL_VOLTAGE: 'float32', + HV_L_MAX_CELL_VOLTAGE: 'float32', + HV_L_MAX_CELL_TEMP: 'float32', + HV_L_VOLTAGES: [ 'array', 'uint8', 96 ], + HV_L_TEMPERATURES: [ 'array', 'uint8', 48 ] + }, + HV_Right: { + HV_R_READY: 'uint8', + HV_R_ERROR: 'uint32', + HV_R_STATE: 'uint8', + HV_R_BALANCING: 'uint8', + HV_R_VOLTAGE: 'uint16', + HV_R_CURRENT: 'int16', + HV_R_MIN_CELL_VOLTAGE: 'float32', + HV_R_MAX_CELL_VOLTAGE: 'float32', + HV_R_MAX_CELL_TEMP: 'float32', + HV_R_VOLTAGES: [ 'array', 'uint8', 96 ], + HV_R_TEMPERATURES: [ 'array', 'uint8', 48 ] + }, + FPGA: { + FPGA_CURRENT_ADC_VALUES: [ 'array', 'uint16', 8 ], + FPGA_CURRENT_STATUS: [ 'array', 'uint8', 8 ], + FPGA_POSITION: 'uint8', + FPGA_PHASE: 'uint8', + FPGA_SUBPHASE: 'uint8', + FPGA_FULL_POSITION: 'uint16', + FPGA_IREF: 'uint8', + FPGA_PULLUP_CURRENT: 'uint8', + FPGA_ZERO_CURRENT: 'uint8' + }, + Inverter: { + READY: 'uint8', + SHUTDOWNDONE: 'uint8', + ICUFPGAREADY: 'uint8', + ICUFPGASTATE: 'uint8', + ICUFPGASTATUS: 'uint32', + ICUFPGAREADYBITS: 'uint16', + ICUMCUSTATE: 'uint8', + ICUMCUSTATUS: 'uint32', + GD_STATUS: 'uint32', + MAX_CURRENT: 'uint8', + BOARD_VOLTAGES: [ 'array', 'float32', 4 ], + TEMPERATURE_MOSFETS: [ 'array', 'uint8', 12 ], + MAX_TEMP: 'uint8', + REAL_POWER: 'int16', + REACTIVE_POWER: 'int16' + }, + Configuration: { + CONFIG_RUNTYPE: 'uint8', + CONFIG_PWMMETHOD: 'uint8', + CONFIG_BANDWIDTHLOW: 'float32', + CONFIG_BANDWIDTHHIGH: 'float32', + CONFIG_MAXVOLTAGE: 'float32', + CONFIG_TARGETCURRENT: 'float32', + CONFIG_ZEROCURRENT: 'float32', + CONFIG_OVERCURRENTLIMIT_LO: 'float32', + CONFIG_OVERCURRENTLIMIT_HI: 'float32', + CONFIG_ADC_MAXOFFSET: 'float32', + CONFIG_ADC_MAXNOISERANGE: 'float32', + CONFIG_RUNFORWARD: 'uint8', + CONFIG_RUNDURATION: 'float32', + CONFIG_TRACKLENGTH: 'uint8', + CONFIG_SETPOSITION: 'uint8', + CONFIG_RUNLENGTH: 'uint8', + CONFIG_PROPTRACKLENGTH: 'uint8', + CONFIG_TEMP2STARTFAN: 'float32', + CONFIG_MAXMOSFETTEMP: 'float32', + CONFIG_RIDELENGTH: 'float32', + CONFIG_NUMINVERTERSENABLED: 'uint8' + }, + END: { TELEMETRY_FRAME_END: 'uint32' } + }, + Unit: { + Sync: { SYNC: '0xCAFE' }, + State: { + STATE: 'enum', + VCU_EMERGENCY_REASON: 'enum', + VCU_ERRORS: 'enum', + RUN_TIMER: 'ms', + LOG_FILE_NUM: 'number', + LOG_DISCARDED_DATA: 'boolean' + }, + Brake: { + BRAKE_ENGAGE: 'boolean', + BRAKE_STATE: 'enum', + BRAKE_VALVEEMERGENCYBRAKE: 'boolean', + BRAKE_VALVEENGAGEBRAKE: 'boolean', + BRAKE_CONTROLLEDBRAKINGPRESSURE: 'bar', + BRAKE_PRESSUREREGULATORIN: 'bar', + BRAKE_SENSORLEFTENGAGED: 'boolean', + BRAKE_SENSORRIGHTENGAGED: 'boolean', + BRAKE_BRAKEDISTANCE: 'm', + BRAKE_BRAKEENGAGETIMELEFT: 'ms', + BRAKE_BRAKEENGAGETIMERIGHT: 'ms', + BRAKE_TANK: 'bar', + BRAKE_LEFT_ACTION: 'bar', + BRAKE_LEFT_RELEASE: 'bar', + BRAKE_RIGHT_ACTION: 'bar', + BRAKE_RIGHT_RELEASE: 'bar' + }, + Velocity: { + CORRAIL_VELOCITY: 'm/s', + CORRAIL_DISTANCE: 'm', + CORRAIL_MAX_VELOCITY: 'm/s', + CORRAIL_OFFSET_AFTER_RESET: 'm', + GAM900_ACCELERATION_X: 'm/ss', + GAM900_ACCELERATION_Y: 'm/ss', + COMBINED_VELOCITY: 'm/s' + }, + OM20: { + OM20_FRONT_FINDOWN: 'mm', + OM20_FRONT_FINLEFT: 'um', + OM20_FRONT_FINRIGHT: 'um', + OM20_FRONT_CHASSISLEFT: 'mm', + OM20_FRONT_CHASSISRIGHT: 'mm', + OM20_BACK_FINDOWN: 'mm', + OM20_BACK_FINLEFT: 'um', + OM20_BACK_FINRIGHT: 'um', + OM20_BACK_CHASSISLEFT: 'mm', + OM20_BACK_CHASSISRIGHT: 'mm' + }, + Motor_Temperatures: { + TEMP_PT100_2: '°C', + TEMP_PTC_1: 'kOhm', + TEMP_PT100_MAX: '°C', + TEMP_PTC_MAX: 'kOhm' + }, + LV_Batteries: { + LV_BAT1_CURRENT: 'A', + LV_BAT1_VOLTAGE: 'V', + LV_BAT1_POWER: 'W', + LV_BAT2_CURRENT: 'A', + LV_BAT2_VOLTAGE: 'V', + LV_BAT2_POWER: 'W' + }, + HV_Batteries: { + HV_ISOLATION: 'boolean', + HV_DO_PRECHARGE: 'boolean', + HV_PRECHARGE_DONE: 'boolean', + NUMBER_OF_BATTERIES: 'number' + }, + HV_Left: { + HV_L_READY: 'boolean', + HV_L_ERROR: 'enum', + HV_L_STATE: 'enum', + HV_L_BALANCING: 'boolean', + HV_L_VOLTAGE: 'V', + HV_L_CURRENT: 'A', + HV_L_MIN_CELL_VOLTAGE: 'V', + HV_L_MAX_CELL_VOLTAGE: 'V', + HV_L_MAX_CELL_TEMP: '°C', + HV_L_VOLTAGES: 'V', + HV_L_TEMPERATURES: '°C' + }, + HV_Right: { + HV_R_READY: 'boolean', + HV_R_ERROR: 'enum', + HV_R_STATE: 'enum', + HV_R_BALANCING: 'boolean', + HV_R_VOLTAGE: 'V', + HV_R_CURRENT: 'A', + HV_R_MIN_CELL_VOLTAGE: 'V', + HV_R_MAX_CELL_VOLTAGE: 'V', + HV_R_MAX_CELL_TEMP: '°C', + HV_R_VOLTAGES: 'V', + HV_R_TEMPERATURES: '°C' + }, + FPGA: { + FPGA_CURRENT_ADC_VALUES: 'number', + FPGA_CURRENT_STATUS: 'enum', + FPGA_POSITION: 'number', + FPGA_PHASE: 'number', + FPGA_SUBPHASE: 'number', + FPGA_FULL_POSITION: 'number', + FPGA_IREF: 'enum', + FPGA_PULLUP_CURRENT: 'enum', + FPGA_ZERO_CURRENT: 'enum' + }, + Inverter: { + READY: 'enum', + SHUTDOWNDONE: 'enum', + ICUFPGAREADY: 'enum', + ICUFPGASTATE: 'enum', + ICUFPGASTATUS: 'enum', + ICUFPGAREADYBITS: 'enum', + ICUMCUSTATE: 'enum', + ICUMCUSTATUS: 'enum', + GD_STATUS: 'enum', + MAX_CURRENT: 'V', + BOARD_VOLTAGES: 'V', + TEMPERATURE_MOSFETS: '°C', + MAX_TEMP: '°C', + REAL_POWER: 'W', + REACTIVE_POWER: 'kvar' + }, + Configuration: { + CONFIG_RUNTYPE: 'enum', + CONFIG_PWMMETHOD: 'enum', + CONFIG_BANDWIDTHLOW: 'A', + CONFIG_BANDWIDTHHIGH: 'A', + CONFIG_MAXVOLTAGE: 'V', + CONFIG_TARGETCURRENT: 'A', + CONFIG_ZEROCURRENT: 'A', + CONFIG_OVERCURRENTLIMIT_LO: 'A', + CONFIG_OVERCURRENTLIMIT_HI: 'A', + CONFIG_ADC_MAXOFFSET: 'A', + CONFIG_ADC_MAXNOISERANGE: 'A', + CONFIG_RUNFORWARD: 'boolean', + CONFIG_RUNDURATION: 's', + CONFIG_TRACKLENGTH: 'm', + CONFIG_SETPOSITION: 'number', + CONFIG_RUNLENGTH: 'number', + CONFIG_PROPTRACKLENGTH: 'number', + CONFIG_TEMP2STARTFAN: 'number', + CONFIG_MAXMOSFETTEMP: 'number', + CONFIG_RIDELENGTH: 'm', + CONFIG_NUMINVERTERSENABLED: 'number' + }, + END: { TELEMETRY_FRAME_END: '0xDECAFBAD' } + }, + Factor: { + Sync: { SYNC: 1 }, + State: { + STATE: 1, + VCU_EMERGENCY_REASON: 1, + VCU_ERRORS: 1, + RUN_TIMER: 1, + LOG_FILE_NUM: 1, + LOG_DISCARDED_DATA: 1 + }, + Brake: { + BRAKE_ENGAGE: 1, + BRAKE_STATE: 1, + BRAKE_VALVEEMERGENCYBRAKE: 1, + BRAKE_VALVEENGAGEBRAKE: 1, + BRAKE_CONTROLLEDBRAKINGPRESSURE: 1, + BRAKE_PRESSUREREGULATORIN: 1, + BRAKE_SENSORLEFTENGAGED: 1, + BRAKE_SENSORRIGHTENGAGED: 1, + BRAKE_BRAKEDISTANCE: 1000, + BRAKE_BRAKEENGAGETIMELEFT: 1, + BRAKE_BRAKEENGAGETIMERIGHT: 1, + BRAKE_TANK: 1000, + BRAKE_LEFT_ACTION: 1000, + BRAKE_LEFT_RELEASE: 1000, + BRAKE_RIGHT_ACTION: 1000, + BRAKE_RIGHT_RELEASE: 1000 + }, + Velocity: { + CORRAIL_VELOCITY: 1000, + CORRAIL_DISTANCE: 1000, + CORRAIL_MAX_VELOCITY: 1000, + CORRAIL_OFFSET_AFTER_RESET: 1000, + GAM900_ACCELERATION_X: 1000, + GAM900_ACCELERATION_Y: 1000, + COMBINED_VELOCITY: 1000 + }, + OM20: { + OM20_FRONT_FINDOWN: 100, + OM20_FRONT_FINLEFT: 10, + OM20_FRONT_FINRIGHT: 10, + OM20_FRONT_CHASSISLEFT: 100, + OM20_FRONT_CHASSISRIGHT: 100, + OM20_BACK_FINDOWN: 100, + OM20_BACK_FINLEFT: 10, + OM20_BACK_FINRIGHT: 10, + OM20_BACK_CHASSISLEFT: 100, + OM20_BACK_CHASSISRIGHT: 100 + }, + Motor_Temperatures: { + TEMP_PT100_2: 1, + TEMP_PTC_1: 125, + TEMP_PT100_MAX: 1, + TEMP_PTC_MAX: 125 + }, + LV_Batteries: { + LV_BAT1_CURRENT: 1, + LV_BAT1_VOLTAGE: 1, + LV_BAT1_POWER: 1, + LV_BAT2_CURRENT: 1, + LV_BAT2_VOLTAGE: 1, + LV_BAT2_POWER: 1 + }, + HV_Batteries: { + HV_ISOLATION: 1, + HV_DO_PRECHARGE: 1, + HV_PRECHARGE_DONE: 1, + NUMBER_OF_BATTERIES: 1 + }, + HV_Left: { + HV_L_READY: 1, + HV_L_ERROR: 1, + HV_L_STATE: 1, + HV_L_BALANCING: 1, + HV_L_VOLTAGE: 50, + HV_L_CURRENT: 50, + HV_L_MIN_CELL_VOLTAGE: 1, + HV_L_MAX_CELL_VOLTAGE: 1, + HV_L_MAX_CELL_TEMP: 1, + HV_L_VOLTAGES: 50, + HV_L_TEMPERATURES: 2 + }, + HV_Right: { + HV_R_READY: 1, + HV_R_ERROR: 1, + HV_R_STATE: 1, + HV_R_BALANCING: 1, + HV_R_VOLTAGE: 50, + HV_R_CURRENT: 50, + HV_R_MIN_CELL_VOLTAGE: 1, + HV_R_MAX_CELL_VOLTAGE: 1, + HV_R_MAX_CELL_TEMP: 1, + HV_R_VOLTAGES: 50, + HV_R_TEMPERATURES: 2 + }, + FPGA: { + FPGA_CURRENT_ADC_VALUES: 1, + FPGA_CURRENT_STATUS: 1, + FPGA_POSITION: 1, + FPGA_PHASE: 1, + FPGA_SUBPHASE: 1, + FPGA_FULL_POSITION: 1, + FPGA_IREF: 1, + FPGA_PULLUP_CURRENT: 1, + FPGA_ZERO_CURRENT: 1 + }, + Inverter: { + READY: 1, + SHUTDOWNDONE: 1, + ICUFPGAREADY: 1, + ICUFPGASTATE: 1, + ICUFPGASTATUS: 1, + ICUFPGAREADYBITS: 1, + ICUMCUSTATE: 1, + ICUMCUSTATUS: 1, + GD_STATUS: 1, + MAX_CURRENT: 1, + BOARD_VOLTAGES: 1, + TEMPERATURE_MOSFETS: 2, + MAX_TEMP: 2, + REAL_POWER: 1, + REACTIVE_POWER: 1000 + }, + Configuration: { + CONFIG_RUNTYPE: 1, + CONFIG_PWMMETHOD: 1, + CONFIG_BANDWIDTHLOW: 1, + CONFIG_BANDWIDTHHIGH: 1, + CONFIG_MAXVOLTAGE: 1, + CONFIG_TARGETCURRENT: 1, + CONFIG_ZEROCURRENT: 1, + CONFIG_OVERCURRENTLIMIT_LO: 1, + CONFIG_OVERCURRENTLIMIT_HI: 1, + CONFIG_ADC_MAXOFFSET: 1, + CONFIG_ADC_MAXNOISERANGE: 1, + CONFIG_RUNFORWARD: 1, + CONFIG_RUNDURATION: 1, + CONFIG_TRACKLENGTH: 1, + CONFIG_SETPOSITION: 1, + CONFIG_RUNLENGTH: 1, + CONFIG_PROPTRACKLENGTH: 1, + CONFIG_TEMP2STARTFAN: 1, + CONFIG_MAXMOSFETTEMP: 1, + CONFIG_RIDELENGTH: 1, + CONFIG_NUMINVERTERSENABLED: 1 + }, + END: { TELEMETRY_FRAME_END: 1 } + } +}; +module.exports = { + "typeset": frame_telemetry_typeset +}; + \ No newline at end of file diff --git a/control_panel/js/util.js b/control_panel/js/util.js new file mode 100644 index 0000000..a4e9c33 --- /dev/null +++ b/control_panel/js/util.js @@ -0,0 +1,209 @@ +/** + * @file util.js + * @brief Utility functions + * + * @author Philip Wiese, philip.wiese@swissloop.ch + * + * @license + * Copyright (C) 2022 Swissloop + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * @module Utils + * @version 3.0.0 + * + */ + +/** + * Flatten JSON + * Code from https://stackoverflow.com/questions/11332530/flattening-json-to-csv-format + * + * @param obj + * @param path + * @returns {{}} + */ +const flatten = (obj, path = []) => { + return Object.keys(obj).reduce((result, prop) => { + if (typeof obj[prop] !== "object") { + result[path.concat(prop).join(".")] = obj[prop]; + return result; + } + return Object.assign(result, flatten(obj[prop], path.concat(prop), result)); + }, {}); +}; + +/** + * Create test data with sync word + * + * @param frameLength Length of buffer + * @param random Whether to create random data + * @returns {Array} + */ +let offset = -17; +function createTestingData(frameLength = 512, random=true, max_offset = 9) { + let testData = new Uint8Array(frameLength); + testData[0] = 0xFE; + testData[1] = 0xCA; + if (random) { + for (let i = 2; i < testData.length; i+=1) { + testData[i] = randomInt(0, 255); + } + } else { + for (let i = 3; i < testData.length; i+=2) { + testData[i] = (i-1+offset)/2 % 255; + } + if (offset === max_offset) offset = -17; + //offset += 1 + } + return testData; +} + +/** + * Create random number within lower and upper bound. + * + * @param low Lower bound. + * @param high Upper bound. + * @returns {number} + */ +function randomInt (low, high) { + return Math.floor(Math.random() * (high - low) + low); +} + +/** @ToDo Make sure this is correct */ +/** + * Calculate lookup table for SOC of HV Batteries + * + * @returns {[number]} + */ +function soc() { + const a = 3.6; + const b = -0.111; + const c = -0.5; + const d = 1.113; + const m = 1.093; + const n = 1.9; + + let soc_ = []; + + for (let i = 0; i < 100; i++) { + let s = (i+1)*0.01; + soc_[i] = (a+b*(-1*Math.log(s))**m + c*s+d* Math.exp(n*(s-1))); + } + + return soc_; +} + +const soc_lookup = soc(); + +/** + * Returns the state of charge closest to the current Voltage + * + * @param volt Voltage of battery + * @returns {number} + */ +function get_soc(volt) { + let closest_i = 0; + let closest_dist = 1000000; + + // Calculate the closest voltage in the lookup table + for (let i = 0; i < soc_lookup.length; i++) { + let dist = Math.abs(volt-soc_lookup[i]); + + if (dist < closest_dist) closest_i = i; + closest_dist = dist; + } + + return (closest_i+1); +} + +/** + * Returns string descriptor of an enumerated value + * + * @param json_array JSON with enumeration + * @param value Value of enumeration + * @returns {object} Textual description + */ +function json_array_select_value(json_array, value) { + let length = json_array.length; + + for (let i = 0; i < length; i++) { + if (value === parseInt(json_array[i].flag)) { + return json_array[i]; + } + } + + return {description: "Not Found", value: -1}; +} + +/** + * Returns the absolute value of dq vector + * + * @param q_val Q value in DQ frame + * @param d_val D value in DQ frame + * @returns {number} Absolute value + */ +function dq_abs(q_val, d_val) { + return Math.sqrt(q_val*q_val + d_val*d_val) +} + +/** + * Check if the control frame is empty. + * A control frame is empty if all elements are zero. + * A empty control frame is a heartbeat. + * + * @param {object} frame JSON with control frame + * @returns {boolean} Whether the frame is a heartbeat (all zeros) or not. + */ +function ctr_frame_isEmpty(frame) { + delete frame['fat_time']; + let empty = true; + frame = flatten((frame)); + for (key in frame) { + if (frame[key]) empty = false; + } + return empty; +} +/** + * returns the binary form of an object as a string + * @param {object} dec + * @returns string + */ +function dec2bin(dec, length){ + var binaryStr = (dec >>> 0).toString(2); + while(binaryStr.length < length) { + binaryStr = "0" + binaryStr; + } + return binaryStr +} + +/** + * + * @param {object} adc + * @returns converted value to ampere + */ +function adc2ampere(adc) +{ + return (adc/16384 - 0.5) * 614.4; +} + +module.exports = { + "flatten": flatten, + "createTestingData": createTestingData, + "get_soc": get_soc, + "json_array_select_value": json_array_select_value, + "dq_abs": dq_abs, + "ctr_frame_isEmpty": ctr_frame_isEmpty, + "dec2bin": dec2bin, + "adc2ampere": adc2ampere +}; \ No newline at end of file diff --git a/control_panel/json/bms_com_fault.json b/control_panel/json/bms_com_fault.json new file mode 100644 index 0000000..8f51758 --- /dev/null +++ b/control_panel/json/bms_com_fault.json @@ -0,0 +1,44 @@ +[ + {"error_type": "No Error", "flag": "0x0000", + "description": "-" + }, + {"error_type": "Stack Fault", "flag": "0x0001", + "description": "Stack fault input (FAULTH±) is too noisy or is running at the wrong frequency.\n\nNote: The STK_FAULT_ERR flag may not be clearable under some conditions. If a\nSTK_FAULT_ERR is detected, and then no more edges appear on the high-side fault pins (as\nwould be the case if the chip above had a fault condition), it may be impossible to clear the\nSTK_FAULT_ERR flag. Once proper signaling resumes on the high-side fault pin, it will again be\npossible to clear this fault.\nMasked STK_FAULT_ERR is not cleared during initialization. As a result, there is a approximately\n5-μs window at startup where, if the high-side fault receiver detects more than four falling edges,\nthe STK_FAULT_ERR will be set even though it is masked." + }, + {"error_type": "Stop Err", "flag": "0x0008", + "description": "The UART receiver detected an invalid stop bit on the single-ended low-side interface.\n\nThis error only appears on chips using the UART interface. COMM_CLEAR and COMM_RESET\nwill also cause this fault.\nThis error is specific to the UART interface." + }, + {"error_type": "Frame Err", "flag": "0x0020", + "description": "A framing error has been detected.\n\nThis indicates that the chip received a start of frame on the differential communications interface\nbefore it had completed the prior frame." + }, + {"error_type": "CRC Fault L", "flag": "0x0040", + "description": "CRC fault has been detected on the low-side interface (either single-ended UART or differential VBUS).\n\nThe frame was discarded." + }, + {"error_type": "CRC Fault H", "flag": "0x0080", + "description": "A CRC fault has been detected on the high-side interface.\n\nThe frame was discarded. If it occurs on the high side, it may have caused this chip to fail to return\nits frame in a broadcast or group response." + }, + {"error_type": "Abort L", "flag": "0x0100", + "description": "A framing bit with value 1 was detected on the low-side differential interface.\n\nA data byte was stopped and ignored. ABORT_L also reads 1 on devices in a stack when a\nCOMM_RESET or COMM_CLEAR is sent to the UART interface on the base device of a stack." + }, + {"error_type": "Abort H", "flag": "0x0200", + "description": "A framing bit with value 1 was detected on the high-side differential interface.\n\nA data byte was stopped and ignored. If this occurs on the high-side interface, it is always due to a\ncommunication problem. Sending COMM_RESET or COMM_CLEAR to the UART interface on the\nbottom chip will cause this fault on the low-side interface.\nWhen it occurs on the high side, it may have caused this chip to fail to return its frame in a\nbroadcast or group response (although the microcontroller should already have detected this\nbefore this bit was read)." + }, + {"error_type": "Edge L", "flag": "0x0400", + "description": "A falling edge was not detected on the low-side interface by the 4th bit." + }, + {"error_type": "Edge H", "flag": "0x0800", + "description": "A falling edge was not detected on the high-side interface by the 4th bit." + }, + {"error_type": "Comp Fault L", "flag": "0x1000", + "description": "A frame on the low-side interface (COMML) was stopped due to two or more complement errors (COMP_ERR_L)." + }, + {"error_type": "Comp Fault H", "flag": "0x2000", + "description": "A frame on the high-side interface (COMMH) was stopped due to two or more complement errors (COMP_ERR_H)." + }, + {"error_type": "Comp Warn L", "flag": "0x4000", + "description": "A bit on the low-side interface failed to compare with its complement. This is notification only; the frame is processed by the communications interface logic." + }, + {"error_type": "Comp Warn H", "flag": "0x8000", + "description": "A bit on the high-side interface failed to compare with its complement. This is notification only; the frame is processed by the communications interface logic." + } +] \ No newline at end of file diff --git a/control_panel/json/bms_dev_fault.json b/control_panel/json/bms_dev_fault.json new file mode 100644 index 0000000..eb2a9d6 --- /dev/null +++ b/control_panel/json/bms_dev_fault.json @@ -0,0 +1,35 @@ +[ + {"error_type": "No Error", "flag": "0x0000", + "description": "-" + }, + {"error_type": "Factory ECC Err", "flag": "0x0001", + "description": "An uncorrectable ECC fault was detected while loading from factory registers.\nRegisters in the block (not all registers) have been loaded with their default values. The device\nis operating abnormally and has probably failed. Functionality, behavior, and results are suspect\nand should not be relied upon." + }, + {"error_type": "Factory ECC Corr", "flag": "0x0002", + "description": "A ECC fault was corrected while loading factory space from EEPROM." + }, + {"error_type": "User ECC Err", "flag": "0x0004", + "description": "An uncorrectable ECC fault was detected while loading from EEPROM. Registers in\nthe block (not all registers) have been loaded with their default values." + }, + {"error_type": "User ECC Corr", "flag": "0x0008", + "description": "A ECC fault was corrected while loading user space from EEPROM." + }, + {"error_type": "ADC Calib", "flag": "0x0010", + "description": "An ADC test (ADC_FCAL_TEST or ADC_PCAL_TEST) failed." + }, + {"error_type": "HREF Ground", "flag": "0x0800", + "description": "Analog-die reference ground measurement was out of range." + }, + {"error_type": "HREF", "flag": "0x1000", + "description": "Analog die 4.5-V reference measurement was out of range." + }, + {"error_type": "Analog Fault", "flag": "0x2000", + "description": "The analog die is reporting an error, but it cannot tell what the error is (no error\ncondition has been detected). This may be caused by a single event upset, or possibly the\ndevice has failed. If this occurs consistently, the device should be removed from service." + }, + {"error_type": "Fact Checksum", "flag": "0x4000", + "description": "A checksum error was detected in the factory registers." + }, + {"error_type": "User Checksum", "flag": "0x8000", + "description": "A checksum error was detected in the registers. This fault is self-clearing when the condition goes away." + } +] \ No newline at end of file diff --git a/control_panel/json/bms_errors.json b/control_panel/json/bms_errors.json new file mode 100644 index 0000000..023bc8d --- /dev/null +++ b/control_panel/json/bms_errors.json @@ -0,0 +1,101 @@ +[ + {"error_type": "No Error", "flag": "0x00000000", + "description": "-" + }, + {"error_type": "Error", "flag": "0x00000001", + "description": "Error" + }, + {"error_type": "Invalid Argument", "flag": "0x00000002", + "description": "Invalid Argument" + }, + {"error_type": "UART", "flag": "0x00000004", + "description": "UART" + }, + {"error_type": "TX Timeout", "flag": "0x00000008", + "description": "TX Timeout" + }, + {"error_type": "TX Failed", "flag": "0x00000010", + "description": "TX Failed" + }, + {"error_type": "RX Timeout", "flag": "0x00000020", + "description": "RX Timeout" + }, + {"error_type": "RX Length Mismatch", "flag": "0x00000040", + "description": "RX Length Mismatch" + }, + {"error_type": "RX CRC Mismatch", "flag": "0x00000080", + "description": "RX CRC Mismatch" + }, + {"error_type": "No Device", "flag": "0x00000100", + "description": "No Device" + }, + {"error_type": "Lost Module", "flag": "0x00000200", + "description": "Lost Module" + }, + {"error_type": "Balancing Stopped", "flag": "0x00000400", + "description": "Balancing Stopped" + }, + {"error_type": "No Heartbeat from VCU", "flag": "0x00000800", + "description": "No Heartbeat from VCU" + }, + {"error_type": "COM Reset", "flag": "0x00001000", + "description": "COM Reset" + }, + {"error_type": "Open Wire", "flag": "0x00002000", + "description": "Open Wire" + }, + {"error_type": "Not Awake", "flag": "0x00004000", + "description": "Not Awake" + }, + {"error_type": "Received Faulty Event from VCU", "flag": "0x00008000", + "description": "Received Faulty Event from VCU" + }, + {"error_type": "Overcurrent", "flag": "0x00010000", + "description": "Overcurrent" + }, + {"error_type": "Overvoltage", "flag": "0x00020000", + "description": "Overvoltage" + }, + {"error_type": "Undervoltage", "flag": "0x00040000", + "description": "Undervoltage" + }, + {"error_type": "Overtemperature", "flag": "0x00080000", + "description": "Overtemperature" + }, + {"error_type": "Undertemperature", "flag": "0x00100000", + "description": "Undertemperature" + }, + {"error_type": "Wrong relay state", "flag": "0x00200000", + "description": "Wrong relay state" + }, + {"error_type": "Isolation", "flag": "0x00400000", + "description": "Isolation" + }, + {"error_type": "Fault Pin Daisy 1", "flag": "0x00800000", + "description": "Fault Pin Daisy 1" + }, + {"error_type": "Fault Pin Daisy 2", "flag": "0x01000000", + "description": "Fault Pin Daisy 2" + }, + {"error_type": "Logging Error", "flag": "0x02000000", + "description": "Logging Error" + }, + {"error_type": "Charging Error", "flag": "0x04000000", + "description": "Charging Error" + }, + {"error_type": "Timeout Error", "flag": "0x08000000", + "description": "Timeout Error" + }, + {"error_type": "Overflow Error", "flag": "0x10000000", + "description": "Overflow Error" + }, + {"error_type": "External Error", "flag": "0x20000000", + "description": "The error is not caused by the BMS" + }, + {"error_type": "LV Error", "flag": "0x40000000", + "description": "LV Error" + }, + {"error_type": "Tripactive Error", "flag": "0x80000000", + "description": "Tripactive Error" + } +] \ No newline at end of file diff --git a/control_panel/json/bms_pl455_fault_summary.json b/control_panel/json/bms_pl455_fault_summary.json new file mode 100644 index 0000000..3fe1d48 --- /dev/null +++ b/control_panel/json/bms_pl455_fault_summary.json @@ -0,0 +1,35 @@ +[ + {"error_type": "No Error", "flag": "0x0000", + "description": "-" + }, + {"error_type": "GPIO Fault", "flag": "0x0040", + "description": "One or more of the individual fault bits of this type are currently set.\nThese bits always reflect the state of the underlying bits in the other fault registers, which may be\nlatched or not, depending on the setting of the DEVCONFIG[UNLATCHED_FAULT] bit." + }, + {"error_type": "Chip Fault", "flag": "0x0080", + "description": "One or more of the individual fault bits of this type are currently set.\nThese bits always reflect the state of the underlying bits in the other fault registers, which may be\nlatched or not, depending on the setting of the DEVCONFIG[UNLATCHED_FAULT] bit." + }, + {"error_type": "SYS Fault", "flag": "0x0100", + "description": "One or more of the individual fault bits of this type are currently set.\nThese bits always reflect the state of the underlying bits in the other fault registers, which may be\nlatched or not, depending on the setting of the DEVCONFIG[UNLATCHED_FAULT] bit." + }, + {"error_type": "COM Fault", "flag": "0x0200", + "description": "One or more of the individual fault bits of this type are currently set.\nThese bits always reflect the state of the underlying bits in the other fault registers, which may be\nlatched or not, depending on the setting of the DEVCONFIG[UNLATCHED_FAULT] bit." + }, + {"error_type": "CMP OV Fault", "flag": "0x0400", + "description": "One or more of the individual fault bits of this type are currently set.\nThese bits always reflect the state of the underlying bits in the other fault registers, which may be\nlatched or not, depending on the setting of the DEVCONFIG[UNLATCHED_FAULT] bit." + }, + {"error_type": "CMP UV Fault", "flag": "0x0800", + "description": "One or more of the individual fault bits of this type are currently set.\nThese bits always reflect the state of the underlying bits in the other fault registers, which may be\nlatched or not, depending on the setting of the DEVCONFIG[UNLATCHED_FAULT] bit." + }, + {"error_type": "AUX OV Fault", "flag": "0x1000", + "description": "One or more of the individual fault bits of this type are currently set.\nThese bits always reflect the state of the underlying bits in the other fault registers, which may be\nlatched or not, depending on the setting of the DEVCONFIG[UNLATCHED_FAULT] bit." + }, + {"error_type": "AUX UV Fault", "flag": "0x2000", + "description": "One or more of the individual fault bits of this type are currently set.\nThese bits always reflect the state of the underlying bits in the other fault registers, which may be\nlatched or not, depending on the setting of the DEVCONFIG[UNLATCHED_FAULT] bit." + }, + {"error_type": "OV Fault", "flag": "0x4000", + "description": "One or more of the individual fault bits of this type are currently set.\nThese bits always reflect the state of the underlying bits in the other fault registers, which may be\nlatched or not, depending on the setting of the DEVCONFIG[UNLATCHED_FAULT] bit." + }, + {"error_type": "UV Fault", "flag": "0x8000", + "description": "One or more of the individual fault bits of this type are currently set.\nThese bits always reflect the state of the underlying bits in the other fault registers, which may be\nlatched or not, depending on the setting of the DEVCONFIG[UNLATCHED_FAULT] bit." + } +] \ No newline at end of file diff --git a/control_panel/json/bms_state.json b/control_panel/json/bms_state.json new file mode 100644 index 0000000..36154e3 --- /dev/null +++ b/control_panel/json/bms_state.json @@ -0,0 +1,10 @@ +[ + {"description": "Charge", "flag": 0}, + {"description": "Idle", "flag": 1}, + {"description": "Precharge", "flag": 2}, + {"description": "Ready", "flag": 3}, + {"description": "Run", "flag": 4}, + {"description": "Balancing", "flag": 5}, + {"description": "Emergency", "flag": 8}, + {"description": "Disconnected", "flag": 10} +] \ No newline at end of file diff --git a/control_panel/json/bms_sys_fault.json b/control_panel/json/bms_sys_fault.json new file mode 100644 index 0000000..c5a5e08 --- /dev/null +++ b/control_panel/json/bms_sys_fault.json @@ -0,0 +1,29 @@ +[ + {"error_type": "No Error", "flag": "0x00", + "description": "-" + }, + {"error_type": "VP Clamp", "flag": "0x01", + "description": "NPNB pin is monitored and clamped to keep the NPNB pin from going over voltage." + }, + {"error_type": "VP Fault", "flag": "0x02", + "description": "VP supply failure detected in analog die." + }, + {"error_type": "VM Fault", "flag": "0x04", + "description": "VM supply failure detected in analog die." + }, + {"error_type": "V_DIG Fault", "flag": "0x08", + "description": "VDIG supply failure detected in the analog die." + }, + {"error_type": "Int Temp", "flag": "0x10", + "description": "Overtemperature condition in the digital die.\n\nIf UNLATCHED_FAULT is set, this bit is self-clearing." + }, + {"error_type": "V_DIG Wake Fault", "flag": "0x20", + "description": "VDIG supply was already high on wakeup. This could happen if the NPN transistor\nwere leaking and preventing VDIG from going away when VP/VDIG shuts down. It could also\noccur if the chip is reset (in which case the VDIG supply remained on) or if it was shut down too\nbriefly to allow the supply time to ramp down. This bit is provided to allow detection of leakage\ncurrent into the supply during shutdown.\nThis bit is checked only set by the device during the device initialization sequence. Once\ncleared, it will not set again until the block is reset." + }, + {"error_type": "COM Timeout", "flag": "0x40", + "description": "Communications timeout has been detected." + }, + {"error_type": "SYS Reset", "flag": "0x80", + "description": "A system reset has been detected." + } +] \ No newline at end of file diff --git a/control_panel/json/bms_uv_ov_fault.json b/control_panel/json/bms_uv_ov_fault.json new file mode 100644 index 0000000..f16335a --- /dev/null +++ b/control_panel/json/bms_uv_ov_fault.json @@ -0,0 +1,101 @@ +[ + {"error_type": "No Error", "flag": "0x00000000", + "description": "-" + }, + {"error_type": "Cell 1 UV", "flag": "0x00000001", + "description": "The stored result for the corresponding battery channel is less than UV_THRES_CELL.\nUV_FAULT[0] corresponds to cell 1.\nIf UNLATCHED_FAULT is set, this register is self-clearing." + }, + {"error_type": "Cell 2 UV", "flag": "0x00000002", + "description": "The stored result for the corresponding battery channel is less than UV_THRES_CELL.\nUV_FAULT[0] corresponds to cell 1.\nIf UNLATCHED_FAULT is set, this register is self-clearing." + }, + {"error_type": "Cell 3 UV", "flag": "0x00000004", + "description": "The stored result for the corresponding battery channel is less than UV_THRES_CELL.\nUV_FAULT[0] corresponds to cell 1.\nIf UNLATCHED_FAULT is set, this register is self-clearing." + }, + {"error_type": "Cell 4 UV", "flag": "0x00000008", + "description": "The stored result for the corresponding battery channel is less than UV_THRES_CELL.\nUV_FAULT[0] corresponds to cell 1.\nIf UNLATCHED_FAULT is set, this register is self-clearing." + }, + {"error_type": "Cell 5 UV", "flag": "0x00000010", + "description": "The stored result for the corresponding battery channel is less than UV_THRES_CELL.\nUV_FAULT[0] corresponds to cell 1.\nIf UNLATCHED_FAULT is set, this register is self-clearing." + }, + {"error_type": "Cell 6 UV", "flag": "0x00000020", + "description": "The stored result for the corresponding battery channel is less than UV_THRES_CELL.\nUV_FAULT[0] corresponds to cell 1.\nIf UNLATCHED_FAULT is set, this register is self-clearing." + }, + {"error_type": "Cell 7 UV", "flag": "0x00000040", + "description": "The stored result for the corresponding battery channel is less than UV_THRES_CELL.\nUV_FAULT[0] corresponds to cell 1.\nIf UNLATCHED_FAULT is set, this register is self-clearing." + }, + {"error_type": "Cell 8 UV", "flag": "0x00000080", + "description": "The stored result for the corresponding battery channel is less than UV_THRES_CELL.\nUV_FAULT[0] corresponds to cell 1.\nIf UNLATCHED_FAULT is set, this register is self-clearing." + }, + {"error_type": "Cell 9 UV", "flag": "0x00000100", + "description": "The stored result for the corresponding battery channel is greater than\nOV_THRES_CELL. OV_FAULT[0] corresponds to cell 1.\nIf UNLATCHED_FAULT is set, this register is self-clearing." + }, + {"error_type": "Cell 10 UV", "flag": "0x00000200", + "description": "The stored result for the corresponding battery channel is greater than\nOV_THRES_CELL. OV_FAULT[0] corresponds to cell 1.\nIf UNLATCHED_FAULT is set, this register is self-clearing." + }, + {"error_type": "Cell 11 UV", "flag": "0x00000400", + "description": "The stored result for the corresponding battery channel is greater than\nOV_THRES_CELL. OV_FAULT[0] corresponds to cell 1.\nIf UNLATCHED_FAULT is set, this register is self-clearing." + }, + {"error_type": "Cell 12 UV", "flag": "0x00000800", + "description": "The stored result for the corresponding battery channel is greater than\nOV_THRES_CELL. OV_FAULT[0] corresponds to cell 1.\nIf UNLATCHED_FAULT is set, this register is self-clearing." + }, + {"error_type": "Cell 13 UV", "flag": "0x00001000", + "description": "The stored result for the corresponding battery channel is greater than\nOV_THRES_CELL. OV_FAULT[0] corresponds to cell 1.\nIf UNLATCHED_FAULT is set, this register is self-clearing." + }, + {"error_type": "Cell 14 UV", "flag": "0x00002000", + "description": "The stored result for the corresponding battery channel is greater than\nOV_THRES_CELL. OV_FAULT[0] corresponds to cell 1.\nIf UNLATCHED_FAULT is set, this register is self-clearing." + }, + {"error_type": "Cell 15 UV", "flag": "0x00004000", + "description": "The stored result for the corresponding battery channel is greater than\nOV_THRES_CELL. OV_FAULT[0] corresponds to cell 1.\nIf UNLATCHED_FAULT is set, this register is self-clearing." + }, + {"error_type": "Cell 16 UV", "flag": "0x00008000", + "description": "The stored result for the corresponding battery channel is greater than\nOV_THRES_CELL. OV_FAULT[0] corresponds to cell 1.\nIf UNLATCHED_FAULT is set, this register is self-clearing." + }, + {"error_type": "Cell 1 OV", "flag": "0x00010000", + "description": "The stored result for the corresponding battery channel is less than UV_THRES_CELL.\nUV_FAULT[0] corresponds to cell 1.\nIf UNLATCHED_FAULT is set, this register is self-clearing." + }, + {"error_type": "Cell 2 OV", "flag": "0x00020000", + "description": "The stored result for the corresponding battery channel is less than UV_THRES_CELL.\nUV_FAULT[0] corresponds to cell 1.\nIf UNLATCHED_FAULT is set, this register is self-clearing." + }, + {"error_type": "Cell 3 OV", "flag": "0x00040000", + "description": "The stored result for the corresponding battery channel is less than UV_THRES_CELL.\nUV_FAULT[0] corresponds to cell 1.\nIf UNLATCHED_FAULT is set, this register is self-clearing." + }, + {"error_type": "Cell 4 OV", "flag": "0x00080000", + "description": "The stored result for the corresponding battery channel is less than UV_THRES_CELL.\nUV_FAULT[0] corresponds to cell 1.\nIf UNLATCHED_FAULT is set, this register is self-clearing." + }, + {"error_type": "Cell 5 OV", "flag": "0x00100000", + "description": "The stored result for the corresponding battery channel is less than UV_THRES_CELL.\nUV_FAULT[0] corresponds to cell 1.\nIf UNLATCHED_FAULT is set, this register is self-clearing." + }, + {"error_type": "Cell 6 OV", "flag": "0x00200000", + "description": "The stored result for the corresponding battery channel is less than UV_THRES_CELL.\nUV_FAULT[0] corresponds to cell 1.\nIf UNLATCHED_FAULT is set, this register is self-clearing." + }, + {"error_type": "Cell 7 OV", "flag": "0x00400000", + "description": "The stored result for the corresponding battery channel is less than UV_THRES_CELL.\nUV_FAULT[0] corresponds to cell 1.\nIf UNLATCHED_FAULT is set, this register is self-clearing." + }, + {"error_type": "Cell 8 OV", "flag": "0x00800000", + "description": "The stored result for the corresponding battery channel is less than UV_THRES_CELL.\nUV_FAULT[0] corresponds to cell 1.\nIf UNLATCHED_FAULT is set, this register is self-clearing." + }, + {"error_type": "Cell 9 OV", "flag": "0x01000000", + "description": "The stored result for the corresponding battery channel is greater than\nOV_THRES_CELL. OV_FAULT[0] corresponds to cell 1.\nIf UNLATCHED_FAULT is set, this register is self-clearing." + }, + {"error_type": "Cell 10 OV", "flag": "0x02000000", + "description": "The stored result for the corresponding battery channel is greater than\nOV_THRES_CELL. OV_FAULT[0] corresponds to cell 1.\nIf UNLATCHED_FAULT is set, this register is self-clearing." + }, + {"error_type": "Cell 11 OV", "flag": "0x04000000", + "description": "The stored result for the corresponding battery channel is greater than\nOV_THRES_CELL. OV_FAULT[0] corresponds to cell 1.\nIf UNLATCHED_FAULT is set, this register is self-clearing." + }, + {"error_type": "Cell 12 OV", "flag": "0x08000000", + "description": "The stored result for the corresponding battery channel is greater than\nOV_THRES_CELL. OV_FAULT[0] corresponds to cell 1.\nIf UNLATCHED_FAULT is set, this register is self-clearing." + }, + {"error_type": "Cell 13 OV", "flag": "0x10000000", + "description": "The stored result for the corresponding battery channel is greater than\nOV_THRES_CELL. OV_FAULT[0] corresponds to cell 1.\nIf UNLATCHED_FAULT is set, this register is self-clearing." + }, + {"error_type": "Cell 14 OV", "flag": "0x20000000", + "description": "The stored result for the corresponding battery channel is greater than\nOV_THRES_CELL. OV_FAULT[0] corresponds to cell 1.\nIf UNLATCHED_FAULT is set, this register is self-clearing." + }, + {"error_type": "Cell 15 OV", "flag": "0x40000000", + "description": "The stored result for the corresponding battery channel is greater than\nOV_THRES_CELL. OV_FAULT[0] corresponds to cell 1.\nIf UNLATCHED_FAULT is set, this register is self-clearing." + }, + {"error_type": "Cell 16 OV", "flag": "0x80000000", + "description": "The stored result for the corresponding battery channel is greater than\nOV_THRES_CELL. OV_FAULT[0] corresponds to cell 1.\nIf UNLATCHED_FAULT is set, this register is self-clearing." + } +] \ No newline at end of file diff --git a/control_panel/json/brake_state.json b/control_panel/json/brake_state.json new file mode 100644 index 0000000..557cb5f --- /dev/null +++ b/control_panel/json/brake_state.json @@ -0,0 +1,5 @@ +[ + {"description": "Default", "flag": 0}, + {"description": "ControlledBrake", "flag": 1}, + {"description": "EmergencyBrake", "flag": 2} + ] \ No newline at end of file diff --git a/control_panel/json/brake_status.json b/control_panel/json/brake_status.json new file mode 100644 index 0000000..6a9f917 --- /dev/null +++ b/control_panel/json/brake_status.json @@ -0,0 +1,4 @@ +[ + {"description": "Disengaged", "flag": 0}, + {"description": "Engaged", "flag": 1} +] \ No newline at end of file diff --git a/control_panel/json/fpga_control_method.json b/control_panel/json/fpga_control_method.json new file mode 100644 index 0000000..f43b2cc --- /dev/null +++ b/control_panel/json/fpga_control_method.json @@ -0,0 +1,4 @@ +[ + {"description": "Freewheel Diode", "flag": 10}, + {"description": "Freewheel Mosfet", "flag": 11} +] \ No newline at end of file diff --git a/control_panel/json/fpga_control_status.json b/control_panel/json/fpga_control_status.json new file mode 100644 index 0000000..9edb5aa --- /dev/null +++ b/control_panel/json/fpga_control_status.json @@ -0,0 +1,6 @@ +[ + {"description": "-", "flag": 0}, + {"description": "On", "flag": 1}, + {"description": "Pull-up", "flag": 2}, + {"description": "Pull-up + On", "flag": 3} + ] \ No newline at end of file diff --git a/control_panel/json/fpga_state.json b/control_panel/json/fpga_state.json new file mode 100644 index 0000000..7bca5dc --- /dev/null +++ b/control_panel/json/fpga_state.json @@ -0,0 +1,10 @@ +[ + {"description": "Idle", "flag": 0}, + {"description": "Setup", "flag": 4}, + {"description": "Ready", "flag": 1}, + {"description": "Run", "flag": 2}, + {"description": "Shutdown", "flag": 5}, + {"description": "Emergency", "flag": 3}, + {"description": "Reset", "flag": 6}, + {"description": "Disconnected", "flag": 10} +] \ No newline at end of file diff --git a/control_panel/json/fpga_status_bits.json b/control_panel/json/fpga_status_bits.json new file mode 100644 index 0000000..79d10ba --- /dev/null +++ b/control_panel/json/fpga_status_bits.json @@ -0,0 +1,30 @@ +[ + {"error_type": "CurrentCalibrator_ExceededAcceptableOffset0", "description": "CurrentCalibrator_ExceededAcceptableOffset0", "flag": "0x0000001"}, + {"error_type": "CurrentCalibrator_ExceededCalibrationNoise0", "description": "CurrentCalibrator_ExceededCalibrationNoise0", "flag": "0x0000002"}, + {"error_type": "CurrentCalibrator_ExceededAcceptableOffset1", "description": "CurrentCalibrator_ExceededAcceptableOffset1", "flag": "0x0000004"}, + {"error_type": "CurrentCalibrator_ExceededCalibrationNoise1", "description": "CurrentCalibrator_ExceededCalibrationNoise1", "flag": "0x0000008"}, + {"error_type": "CurrentCalibrator_ExceededAcceptableOffset2", "description": "CurrentCalibrator_ExceededAcceptableOffset2", "flag": "0x0000010"}, + {"error_type": "CurrentCalibrator_ExceededCalibrationNoise2", "description": "CurrentCalibrator_ExceededCalibrationNoise2", "flag": "0x0000020"}, + {"error_type": "CurrentCalibrator_ExceededAcceptableOffset3", "description": "CurrentCalibrator_ExceededAcceptableOffset3", "flag": "0x0000040"}, + {"error_type": "CurrentCalibrator_ExceededCalibrationNoise3", "description": "CurrentCalibrator_ExceededCalibrationNoise3", "flag": "0x0000080"}, + {"error_type": "CurrentCalibrator_ExceededAcceptableOffset4", "description": "CurrentCalibrator_ExceededAcceptableOffset4", "flag": "0x0000100"}, + {"error_type": "CurrentCalibrator_ExceededCalibrationNoise4", "description": "CurrentCalibrator_ExceededCalibrationNoise4", "flag": "0x0000200"}, + {"error_type": "CurrentCalibrator_ExceededAcceptableOffset5", "description": "CurrentCalibrator_ExceededAcceptableOffset5", "flag": "0x0000400"}, + {"error_type": "CurrentCalibrator_ExceededCalibrationNoise5", "description": "CurrentCalibrator_ExceededCalibrationNoise5", "flag": "0x0000800"}, + {"error_type": "CurrentCalibrator_ExceededAcceptableOffset6", "description": "CurrentCalibrator_ExceededAcceptableOffset6", "flag": "0x0001000"}, + {"error_type": "CurrentCalibrator_ExceededCalibrationNoise6", "description": "CurrentCalibrator_ExceededCalibrationNoise6", "flag": "0x0002000"}, + {"error_type": "CurrentCalibrator_ExceededAcceptableOffset7", "description": "CurrentCalibrator_ExceededAcceptableOffset7", "flag": "0x0004000"}, + {"error_type": "CurrentCalibrator_ExceededCalibrationNoise7", "description": "CurrentCalibrator_ExceededCalibrationNoise7", "flag": "0x0008000"}, + {"error_type": "OverCurrent_0A", "description": "OverCurrent_0A", "flag": "0x0010000"}, + {"error_type": "OverCurrent_0B", "description": "OverCurrent_0B", "flag": "0x0020000"}, + {"error_type": "OverCurrent_1A", "description": "OverCurrent_1A", "flag": "0x0040000"}, + {"error_type": "OverCurrent_1B", "description": "OverCurrent_1B", "flag": "0x0080000"}, + {"error_type": "OverCurrent_2A", "description": "OverCurrent_2A", "flag": "0x0100000"}, + {"error_type": "OverCurrent_2B", "description": "OverCurrent_2B", "flag": "0x0200000"}, + {"error_type": "OverCurrent_3A", "description": "OverCurrent_3A", "flag": "0x0400000"}, + {"error_type": "OverCurrent_3B", "description": "OverCurrent_3B", "flag": "0x0800000"}, + {"error_type": "CurrentSPI_InvalidPacketsExceeded", "description": "CurrentSPI_InvalidPacketsExceeded", "flag": "0x1000000"}, + {"error_type": "PhaseProcesser_ReverseVelocity", "description": "PhaseProcesser_ReverseVelocity", "flag": "0x2000000"}, + {"error_type": "PhaseProcesser_InvalidVelocity", "description": "PhaseProcesser_InvalidVelocity", "flag": "0x4000000"}, + {"error_type": "PhaseProcesser_InvalidLightgateSequence", "description": "PhaseProcesser_InvalidLightgateSequence", "flag": "0x8000000"} +] diff --git a/control_panel/json/gatedriver.json b/control_panel/json/gatedriver.json new file mode 100644 index 0000000..d6ced87 --- /dev/null +++ b/control_panel/json/gatedriver.json @@ -0,0 +1,23 @@ +[ + {"error_type": "No Error", "description": "-", "flag": "0x00000000"}, + {"error_type": "GateDriver_0_Flt_0", "description": "GateDriver_0_Flt_0", "flag": "0x00000001"}, + {"error_type": "GateDriver_0_Flt_1", "description": "GateDriver_0_Flt_1", "flag": "0x00000002"}, + {"error_type": "GateDriver_0_Flt_2", "description": "GateDriver_0_Flt_2", "flag": "0x00000004"}, + {"error_type": "GateDriver_1_Flt_0", "description": "GateDriver_1_Flt_0", "flag": "0x00000008"}, + {"error_type": "GateDriver_1_Flt_1", "description": "GateDriver_1_Flt_1", "flag": "0x00000010"}, + {"error_type": "GateDriver_1_Flt_2", "description": "GateDriver_1_Flt_2", "flag": "0x00000020"}, + {"error_type": "GateDriver_2_Flt_0", "description": "GateDriver_2_Flt_0", "flag": "0x00000040"}, + {"error_type": "GateDriver_2_Flt_1", "description": "GateDriver_2_Flt_1", "flag": "0x00000080"}, + {"error_type": "GateDriver_2_Flt_2", "description": "GateDriver_2_Flt_2", "flag": "0x00000100"}, + {"error_type": "GateDriver_3_Flt_0", "description": "GateDriver_3_Flt_0", "flag": "0x00000200"}, + {"error_type": "GateDriver_3_Flt_1", "description": "GateDriver_3_Flt_1", "flag": "0x00000400"}, + {"error_type": "GateDriver_3_Flt_2", "description": "GateDriver_3_Flt_2", "flag": "0x00000800"}, + {"error_type": "GateDriver_0_OCD", "description": "GateDriver_0_OCD", "flag": "0x00001000"}, + {"error_type": "GateDriver_1_OCD", "description": "GateDriver_1_OCD", "flag": "0x00002000"}, + {"error_type": "GateDriver_2_OCD", "description": "GateDriver_2_OCD", "flag": "0x00004000"}, + {"error_type": "GateDriver_3_OCD", "description": "GateDriver_3_OCD", "flag": "0x00008000"}, + {"error_type": "GateDriver_0_Rdy", "description": "GateDriver_0_Rdy", "flag": "0x00010000"}, + {"error_type": "GateDriver_1_Rdy", "description": "GateDriver_1_Rdy", "flag": "0x00020000"}, + {"error_type": "GateDriver_2_Rdy", "description": "GateDriver_2_Rdy", "flag": "0x00040000"}, + {"error_type": "GateDriver_3_Rdy", "description": "GateDriver_3_Rdy", "flag": "0x00080000"} +] \ No newline at end of file diff --git a/control_panel/json/icu_mcu_errors.json b/control_panel/json/icu_mcu_errors.json new file mode 100644 index 0000000..3928737 --- /dev/null +++ b/control_panel/json/icu_mcu_errors.json @@ -0,0 +1,92 @@ +[ + {"error_type": "No Error", "flag": "0x0000000", + "description": "-" + }, + {"error_type": "HAL Error (0x00000001)", "flag": "0x00000001", + "description": "Error occurred while using a HAL driver" + }, + {"error_type": "Timer Error (0x00000002)", "flag": "0x00000002", + "description": "Timerfunctions caused an error" + }, + {"error_type": "Task Busy (0x00000004)", "flag": "0x00000004", + "description": "The Task is busy." + }, + {"error_type": "Timeout (0x00000008)", "flag": "0x00000008", + "description": "A timeout occurred while waiting for a semaphore to take." + }, + {"error_type": "Overflow (0x00000010)", "flag": "0x00000010", + "description": "An overflow occurred." + }, + {"error_type": "Range Error(0x00000020)", "flag": "0x00000020", + "description": "A parameter is out of the allowed range." + }, + {"error_type": "Memory Error (0x00000040)", "flag": "0x00000040", + "description": "Insufficient Heap Memory." + }, + {"error_type": "Task notify Error (0x00000080)", "flag": "0x00000080", + "description": "A task notification failed." + }, + {"error_type": "Bad Instance (0x000000100)", "flag": "0x000000100", + "description": "An instance out of range has been chosen." + }, + {"error_type": "FPGA Fingerprint (0x00000200)", "flag": "0x00000200", + "description": "FPGA config SPI did not send correct fingerprint." + }, + {"error_type": "FPGA Echo (0x00000400)", "flag": "0x00000400", + "description": "FPGA config SPI did not echo data sent." + }, + {"error_type": "FPGA Config (0x00000800)", "flag": "0x00000800", + "description": "Unsuccessful setting of a value, Get does not correspond to value in Set." + }, + {"error_type": "FPGA Reset Timeout (0x00001000)", "flag": "0x00001000", + "description": "FPGA did not return to Idle after reset." + }, + {"error_type": "FPGA (0x00002000)", "flag": "0x00002000", + "description": "FPGA error without category." + }, + {"error_type": "Fpga No Heartbeat (0x00004000)", "flag": "0x00004000", + "description": "ICU recieved no Heartbeat from FPGA." + }, + {"error_type": "Fpga False Heartbeat (0x00008000)", "flag": "0x00008000", + "description": "ICU recieved not the correct Heartbeat command from FPGA." + }, + {"error_type": "VCU (0x00010000)", "flag": "0x00010000", + "description": "Recieved an Emergency from the VCU." + }, + {"error_type": "Temperature (0x00020000)", "flag": "0x00020000", + "description": "MOSFET Temperatures are too high or incorrect/missing." + }, + {"error_type": "High Voltage (0x00040000)", "flag": "0x00040000", + "description": "Overvoltage at a HV Board." + }, + {"error_type": "SD Mount Error (0x00080000)", "flag": "0x00080000", + "description": "The SD Card is not inside the IDU." + }, + {"error_type": "Open Dir on SD Error (0x00100000)", "flag": "0x00100000", + "description": "Open directory on SD Card." + }, + {"error_type": "Open File on SD Error (0x00200000)", "flag": "0x00200000", + "description": "A file is open on the SD card." + }, + {"error_type": "Write SD Error (0x00400000)", "flag": "0x00400000", + "description": "A SD Card write error occurred." + }, + {"error_type": "SD Sync Error (0x00800000)", "flag": "0x00800000", + "description": "SD Card Sync Error." + }, + {"error_type": "Invalid Argument (0x01000000)", "flag": "0x01000000", + "description": "Function call with invalid argument at ICU." + }, + {"error_type": "CAN FD (0x02000000)", "flag": "0x02000000", + "description": "Timeout with CAN FD at ICU." + }, + {"error_type": "CAN FD Queue Empty (0x04000000)", "flag": "0x04000000", + "description": "CAN Queue is empty." + }, + {"error_type": "Heartbeat VCU (0x08000000)", "flag": "0x08000000", + "description": "Heartbeat was missing from VCU." + }, + {"error_type": "SPI Invalid Packets Exceeded (0x10000000)", "flag": "0x10000000", + "description": "Temp/Voltage SPI received too many invalid packets with invalid padding." + } +] \ No newline at end of file diff --git a/control_panel/json/icu_state.json b/control_panel/json/icu_state.json new file mode 100644 index 0000000..b3b7646 --- /dev/null +++ b/control_panel/json/icu_state.json @@ -0,0 +1,10 @@ +[ + {"description": "Idle", "flag": 0}, + {"description": "Setup", "flag": 1}, + {"description": "Ready", "flag": 2}, + {"description": "Run", "flag": 3}, + {"description": "Shutdown", "flag": 4}, + {"description": "Emergency", "flag": 5}, + {"description": "Reset", "flag": 6}, + {"description": "Disconnected", "flag": 10} +] \ No newline at end of file diff --git a/control_panel/json/main_emergencies.json b/control_panel/json/main_emergencies.json new file mode 100644 index 0000000..4845959 --- /dev/null +++ b/control_panel/json/main_emergencies.json @@ -0,0 +1,98 @@ +[ + {"error_type": "EMERGENCY_NONE", "flag": "0x00000000", + "description": "No Emergency" + }, + {"error_type": "EMERGENCY_ASSERTION_FAILED", "flag": "0x00000001", + "description": "Assertion Failed" + }, + {"error_type": "EMERGENCY_USER_EMERGENCY", "flag": "0x00000002", + "description": "User Emergency" + }, + {"error_type": "EMERGENCY_HEARTBEAT_NETWORK", "flag": "0x00000004", + "description": "Heartbeat from Control Panel Missing" + }, + {"error_type": "EMERGENCY_HV_UNDER_VOLTAGE", "flag": "0x00000008", + "description": "High voltage under voltage" + }, + {"error_type": "EMERGENCY_BRAKE_ENGAGED_DURING_RUN", "flag": "0x00000010", + "description": "Brake engaged during run" + }, + {"error_type": "EMERGENCY_NAVIGATION_FAILURE", "flag": "0x00000020", + "description": "Navigation failure" + }, + {"error_type": "EMERGENCY_FROM_ICU", "flag": "0x00000040", + "description": "Inverter ICU" + }, + {"error_type": "EMERGENCY_ICU_HEARTBEAT", "flag": "0x00000080", + "description": "Inverter Heartbeat missing" + }, + {"error_type": "EMERGENCY_FROM_BMS ", "flag": "0x00000100", + "description": "BMS Emergency" + }, + {"error_type": "EMERGENCY_BMS_UNDERVOLTAGE ", "flag": "0x00000200", + "description": "BMS undervoltage" + }, + {"error_type": "EMERGENCY_BMS_HEARTBEAT ", "flag": "0x00000400", + "description": "BMS Heartbeat missing" + }, + {"error_type": "EMERGENCY_HV_ISOLATION ", "flag": "0x00000800", + "description": "Isolation Error" + }, + {"error_type": "EMERGENCY_HV_OVER_CURRENT ", "flag": "0x00001000", + "description": "HV overcurrent" + }, + {"error_type": "EMERGENCY_HV_OVER_TEMPERATURE ", "flag": "0x00002000", + "description": "HV over temperature" + }, + {"error_type": "EMERGENCY_STAG_OVER_TEMPERATURE ", "flag": "0x00004000", + "description": "Stag over temperature" + }, + {"error_type": "EMERGENCY_BMS_BALANCING", "flag": "0x00008000", + "description": "Object recognized by Radar" + }, + {"error_type": "EMERGENCY_MAX_VELOCITY ", "flag": "0x00010000", + "description": "Too fast" + }, + {"error_type": "EMERGENCY_LVPS_UNDERVOLTAGE ", "flag": "0x00020000", + "description": "LV batteries empty" + }, + {"error_type": "INVERTER_EN_LINE_RESET_DURING_RUN ", "flag": "0x00040000", + "description": "Inverter EN lines got reset during run" + }, + {"error_type": "MSG_RECEIVED ", "flag": "0x00080000", + "description": "High Priority Message received from CAN" + }, + {"error_type": "SENSOR_DATA_MISSING ", "flag": "0x00100000", + "description": "Sensor Data is missing" + }, + {"error_type": "SENSOR ", "flag": "0x00200000", + "description": "Sensor caused an Emergency" + }, + {"error_type": "ICU_NREADY ", "flag": "0x00400000", + "description": "Ready line ICU low" + }, + {"error_type": "BMS_LEFT_NREADY ", "flag": "0x00800000", + "description": "Ready line BMS Left low" + }, + {"error_type": "BMS_RIGHT_NREADY ", "flag": "0x01000000", + "description": "Ready line BMS Right low" + }, + {"error_type": "HV_BOX_VOLTAGE_DIFFERENCE ", "flag": "0x02000000", + "description": "Big Voltage Difference of left and right box caused an emergency" + }, + {"error_type": "SETUP_TIMEOUT ", "flag": "0x04000000", + "description": "Setup took more than 20 seconds" + }, + {"error_type": "SHUTDOWN_TIMEOUT ", "flag": "0x08000000", + "description": "Shutdown took more than 20 seconds" + }, + {"error_type": "CONTROLLED_BRAKE ", "flag": "0x10000000", + "description": "Controlled braking was evaluated too risky" + }, + {"error_type": "LOW_PRESSURE ", "flag": "0x20000000", + "description": "Low pressure in brake tank" + }, + {"error_type": "BAT_VOLTAGE_OVER_MAX_VOLTAGE ", "flag": "0x40000000", + "description": "Battery voltages are higher then configured max voltage" + } +] \ No newline at end of file diff --git a/control_panel/json/main_errors.json b/control_panel/json/main_errors.json new file mode 100644 index 0000000..092c64a --- /dev/null +++ b/control_panel/json/main_errors.json @@ -0,0 +1,101 @@ +[ + {"error_type": "ERROR_NONE", "flag": "0x00000000", + "description": "ERROR_NONE" + }, + {"error_type": "ERROR_HAL", "flag": "0x00000001", + "description": "ERROR_HAL" + }, + {"error_type": "ERROR_INVALID_ARGUMENT", "flag": "0x00000002", + "description": "ERROR_INVALID_ARGUMENT" + }, + {"error_type": "ERROR_OVERFLOW", "flag": "0x00000004", + "description": "ERROR_OVERFLOW" + }, + {"error_type": "ERROR_NOT_IMPL", "flag": "0x00000008", + "description": "ERROR_NOT_IMPL" + }, + {"error_type": "ERROR_NOT_INIT", "flag": "0x00000010", + "description": "ERROR_NOT_INIT" + }, + {"error_type": "ERROR_NETWORK_TIMEOUT", "flag": "0x00000020", + "description": "ERROR_NETWORK_TIMEOUT" + }, + {"error_type": "ERROR_CAN_FD", "flag": "0x00000040", + "description": "ERROR_CAN_FD" + }, + {"error_type": "ERROR_CAN_FD_QUEUE_EMPTY", "flag": "0x00000080", + "description": "ERROR_CAN_FD_QUEUE_EMPTY" + }, + {"error_type": "ERROR_SI", "flag": "0x00000100", + "description": "ERROR_SI" + }, + {"error_type": "ERROR_SI_STARTUP", "flag": "0x00000200", + "description": "ERROR_SI_STARTUP" + }, + {"error_type": "ERROR_SI_CMD", "flag": "0x00000400", + "description": "ERROR_SI_CMD" + }, + {"error_type": "ERROR_SI_CONFIG", "flag": "0x00000800", + "description": "ERROR_SI_CONFIG" + }, + {"error_type": "ERROR_SI_INVALID_DATA", "flag": "0x00001000", + "description": "ERROR_SI_INVALID_DATA" + }, + {"error_type": "ERROR_SI_DECODE", "flag": "0x00002000", + "description": "ERROR_SI_DECODE" + }, + {"error_type": "ERROR_SI_OM20", "flag": "0x00004000", + "description": "ERROR_SI_OM20" + }, + {"error_type": "ERROR_SI_PBM4", "flag": "0x00008000", + "description": "ERROR_SI_PBM4" + }, + {"error_type": "ERROR_SI_CORRAIL", "flag": "0x00010000", + "description": "ERROR_SI_CORRAIL" + }, + {"error_type": "ERROR_SI_THERMISTOR", "flag": "0x00020000", + "description": "ERROR_SI_THERMISTOR" + }, + {"error_type": "ERROR_SI_TELEMETRY_FRAME", "flag": "0x00040000", + "description": "Telemetry frame is not equal to global frame" + }, + {"error_type": "ERROR_BRAKE", "flag": "0x00040000", + "description": "Brake caused an error" + }, + {"error_type": "ERROR_STATE_ILLEGAL_TRANSITION", "flag": "0x00100000", + "description": "ERROR_STATE_ILLEGAL_TRANSITION" + }, + {"error_type": "ERROR_LVPS_UNDERVOLTAGE", "flag": "0x00200000", + "description": "ERROR_LVPS_UNDERVOLTAGE" + }, + {"error_type": "ERROR_SD", "flag": "0x00400000", + "description": "ERROR_SD" + }, + {"error_type": "ERROR_RUN_CONFIG", "flag": "0x00800000", + "description": "ERROR_RUN_CONFIG" + }, + {"error_type": "ERROR_MOTOR_OVERTEMP", "flag": "0x01000000", + "description": "ERROR_MOTOR_OVERTEMP" + }, + {"error_type": "Brake_Distance_reached", "flag": "0x02000000", + "description": "Brake_Distance_reached" + }, + {"error_type": "Brake_Time_reached", "flag": "0x04000000", + "description": "Brake_Time_reached" + }, + {"error_type": "Brake_MotorStop", "flag": "0x08000000", + "description": "Brake_MotorStop" + }, + {"error_type": "Brake_User", "flag": "0x10000000", + "description": "Brake_User" + }, + {"error_type": "Brake_ControlledBrakingEvaluation", "flag": "0x20000000", + "description": "Brake_ControlledBrakingEvaluation" + }, + {"error_type": "ERROR_EMERGENCY", "flag": "0x40000000", + "description": "ERROR_EMERGENCY" + }, + {"error_type": "ERROR_UNKNOWN", "flag": "0x80000000", + "description": "Unknown error" + } +] \ No newline at end of file diff --git a/control_panel/json/main_state.json b/control_panel/json/main_state.json new file mode 100644 index 0000000..ccdeb3f --- /dev/null +++ b/control_panel/json/main_state.json @@ -0,0 +1,10 @@ +[ + {"description": "Idle", "flag": 1}, + {"description": "Setup", "flag": 2}, + {"description": "Ready", "flag": 3}, + {"description": "Run", "flag": 4}, + {"description": "Braking", "flag": 5}, + {"description": "Shutdown", "flag": 6}, + {"description": "Emergency", "flag": 7}, + {"description": "Reset", "flag": 8} +] \ No newline at end of file diff --git a/control_panel/json/run_modes.json b/control_panel/json/run_modes.json new file mode 100644 index 0000000..985310e --- /dev/null +++ b/control_panel/json/run_modes.json @@ -0,0 +1,5 @@ +[ + {"description": "OnePulse", "flag": 1}, + {"description": "TwoPulse", "flag": 2}, + {"description": "Phase", "flag": 3} +] \ No newline at end of file diff --git a/control_panel/json/true_false.json b/control_panel/json/true_false.json new file mode 100644 index 0000000..1d08c3c --- /dev/null +++ b/control_panel/json/true_false.json @@ -0,0 +1,4 @@ +[ + {"description": "Forward", "flag": 3}, + {"description": "Backward", "flag": 4} + ] \ No newline at end of file diff --git a/control_panel/mp3/connection.mp3 b/control_panel/mp3/connection.mp3 new file mode 100644 index 0000000..7f5ed77 Binary files /dev/null and b/control_panel/mp3/connection.mp3 differ diff --git a/control_panel/mp3/emergency.mp3 b/control_panel/mp3/emergency.mp3 new file mode 100644 index 0000000..21812ca Binary files /dev/null and b/control_panel/mp3/emergency.mp3 differ diff --git a/control_panel/mp3/success.mp3 b/control_panel/mp3/success.mp3 new file mode 100644 index 0000000..06be038 Binary files /dev/null and b/control_panel/mp3/success.mp3 differ diff --git a/control_panel/mp3/waiting.mp3 b/control_panel/mp3/waiting.mp3 new file mode 100644 index 0000000..3036a38 Binary files /dev/null and b/control_panel/mp3/waiting.mp3 differ diff --git a/control_panel/riot-tags/status-display.tag b/control_panel/riot-tags/status-display.tag new file mode 100644 index 0000000..f0f3c0a --- /dev/null +++ b/control_panel/riot-tags/status-display.tag @@ -0,0 +1,150 @@ + +
+
{ errorStatus }
+
{ mainStatus }
+
{ heartbeatStatus }
+
{ logStatus }
+ + + + + +
diff --git a/mock_server/config/config.json b/mock_server/config/config.json new file mode 100644 index 0000000..fabc5e0 --- /dev/null +++ b/mock_server/config/config.json @@ -0,0 +1,9 @@ +{ + "verbosity": 6, + "udp_port_listen": 1337, + "udp_host_send": "127.0.0.1", + "udp_port_send": 1338, + "packet_frequency": 200, + "heartbeat_timeout": 500 +} + diff --git a/mock_server/css/app.css b/mock_server/css/app.css new file mode 100644 index 0000000..ebeddd8 --- /dev/null +++ b/mock_server/css/app.css @@ -0,0 +1,92 @@ +/** + * @file app.css + * @brief Cascading Style Sheets for mock server + * + * @author Philip Wiese, philip.wiese@swissloop.ch + * + * @license + * Copyright (C) 2022 Swissloop + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + */ + +body { + margin: 0; + padding: 0; + font-family: Helvetica, arial, sans-serif; +} + +#header { + height: 64px; + background-color: #DDDDDD; + box-shadow: 0 0.5px 1px 0 #9E9E9E; +} + +#header .version { + font-size: 8px; + color: #999; + text-align: center; + margin-top: -9px; +} + +table tr td:nth-child(1) { + font-weight: bold; + width: 300px; + padding: 0; + text-align: left; +} + +table tr td:nth-child(2) { + width: 80px; + text-align: center; + padding:0; +} +table tr td:nth-child(2) input{ + width: 80px; +} + +table tr td:nth-child(3) { + width: 50px; + padding: 0; + text-align: center; +} + +table tr td:nth-child(5) { + width: 150px; + padding: 0; + text-align: center; +} + + +.slider { + width: 100% !important; +} + +.card-body { + padding: 0; +} + +/* +* === FOOTER === +*/ + +#footer { + height: 18px; + background-color: #DDDDDD; + box-shadow: 0 0.5px 1px 0 #9E9E9E; + font-size: 12px; + vertical-align:middle; + color: black; +} diff --git a/mock_server/js/com.js b/mock_server/js/com.js new file mode 100644 index 0000000..8624ca4 --- /dev/null +++ b/mock_server/js/com.js @@ -0,0 +1,159 @@ +/** + * @file com.js + * @brief Communication module via UDP + * + * @author Philip Wiese, philip.wiese@swissloop.ch + * + * @license + * Copyright (C) 2022 Swissloop + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * @module mock_server/Communication + * @version 3.0.0 + * + */ + +const jBinary = require('jbinary'); +const util = require('../../control_panel/js/util.js'); + +const dgram = require('dgram'); +const EventEmitter = require('events').EventEmitter; + +const config = require('../config/config'); + +const typeset_config_frame = require('../../control_panel/js/typesets/typeset_ctrl_frame.js').typeset; + +class Communication { + /** + * Constructor + * @param {jBinary} binary_frame jBinary TelemetryFrame + * @param {object} frame Parsed jBinary TelemetryFrame + */ + constructor(binary_frame, frame) { + this.binary_frame = binary_frame; + this.frame = frame; + this.commandEmitter = new EventEmitter(); + this.commandEmitter.setMaxListeners(Infinity); + + this.udp_host_send = config.udp_host_send; + this.udp_port_send = config.udp_port_send; + this.udp_port_listen = config.udp_port_listen; + + + $("#udp_host_send").val(this.udp_host_send); + $("#udp_port_send").val(this.udp_port_send ); + $("#udp_port_listen").val(this.udp_port_listen ); + + this.socket = dgram.createSocket('udp4'); + + this.connected = false; + + // Setup event listeners + this.setupNetworkListeners(); + this.setupConfigListeners(this); + + this.socket.bind(this.udp_port_listen); + + // Send heartbeat every 200ms + setInterval(this.send_telemetry_frame.bind(this), config.packet_frequency); + this.heartbeatTimeout = setTimeout(this.missingHeartbeat.bind(this), config.heartbeat_timeout); + } + + + /** + * Setup listener for UDP networking + * @package + */ + setupNetworkListeners() { + this.socket.on('error',(err) => { + console.log("[WARN] ", err) + }); + + this.socket.on('close',() => { + console.log("[WARN] Socket close!"); + }); + + // The message event is fired, when a UDP packet arrives destined for this server.. + this.socket.on('message', (data) => { + this.commandEmitter.emit("packet"); + let frame_parser = new jBinary(data, typeset_config_frame); + let frame = frame_parser.readAll(); + + // Check for heartbeats (empty frames) + if (!util.ctr_frame_isEmpty(frame) && config.verbosity) console.log("Control Frame", frame) + }); + + // The listening event is fired, when the server has initialized and all ready to receive UDP packets + this.socket.on('listening', () => { + const address = this.socket.address(); + console.log(`[INFO] Pod Address: ${address.address}:${address.port}`); + console.log(`[INFO] Server Address: ${config.udp_host_send}:${config.udp_port_send}`); + }); + + this.commandEmitter.on("packet", () =>{ + + if (this.heartbeatTimeout) { + $("#connStatus").css({"fill": "green"}); + clearTimeout(this.heartbeatTimeout); + this.heartbeatTimeout = setTimeout(this.missingHeartbeat.bind(this), config.heartbeat_timeout); + } + + if (!this.connected) { + this.commandEmitter.emit("connected"); + this.connected = true; + } + }); + } + + /** + * Setup listener for configuration in GUI + * @package + */ + setupConfigListeners(context) { + $("#save_config").on('click', function () { + context.udp_host_send = $("#udp_host_send").val(); + context.udp_port_send = $("#udp_port_send").val(); + const address = context.socket.address(); + console.log(`[INFO] Pod Address: ${address.address}:${address.port}`); + console.log(`[INFO] Server Address: ${context.udp_host_send}:${context.udp_port_send}`); + }); + } + + /** + * Send telemetry data to ControlPanel + * + * @package + */ + send_telemetry_frame() { + this.frame.Sync.SYNC = 0xCAFE + // Parse frame object to jBinary + this.binary_frame.writeAll(this.frame); + + if (config.verbosity > 7) console.log("Telemetry Frame", this.frame) + this.socket.send(this.binary_frame.view.buffer, this.udp_port_send, this.udp_host_send, function(err) { + if (err) throw err; + }); + } + + + missingHeartbeat (){ + $("#connStatus").css({"fill": "red"}); + console.log("Heartbeat Timeout") + } +} + +module.exports = { + Communication: Communication +}; \ No newline at end of file diff --git a/mock_server/js/gui/renderer.js b/mock_server/js/gui/renderer.js new file mode 100644 index 0000000..418a709 --- /dev/null +++ b/mock_server/js/gui/renderer.js @@ -0,0 +1,487 @@ +/** + * @file renderer.js + * @brief Main renderer for testing GUI + * + * @author Philip Wiese, philip.wiese@swissloop.ch + * + * @license + * Copyright (C) 2022 Swissloop + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * @module mock_server/GUI + * @version 3.0.0 + * + * @listens window~load + */ + +const jBinary = require('jbinary'); + +const typeset_telemetry_frame = require('../../../control_panel/js/typesets/typeset_telemetry_frame.js').typeset; +const default_values = require('../../json/default_values.json'); + +let binary_frame = jBinary(typeset_telemetry_frame.Length, typeset_telemetry_frame); +let frame = binary_frame.readAll() + +const Communication = require('../com.js').Communication; +const communication = new Communication(binary_frame, frame); + +const mappings = require('../../../control_panel/config/mappings.js'); + +/** @ToDo automatically generate this from mapping.js (based on errors and enumerations) */ +const enum_values = { + "State.STATE": { + "json": mappings.main_state, + "multi": false + }, + "State.VCU_EMERGENCY_REASON": { + "json": mappings.main_emergencies, + "multi": true + }, + "State.VCU_ERRORS": { + "json": mappings.main_error, + "multi": true + }, + "Brake.BRAKE_STATE": { + "json": mappings.brake_state, + "multi": false + }, + "HV_Left.HV_L_STATE": { + "json": mappings.bms_state, + "multi": false + }, + "HV_Right.HV_R_STATE": { + "json": mappings.bms_state, + "multi": false + }, + "HV_Left.HV_L_ERROR": { + "json": mappings.bms_error, + "multi": true + }, + "HV_Right.HV_R_ERROR": { + "json": mappings.bms_error, + "multi": true + }, + "FPGA.FPGA_CURRENT_STATUS.0": { + "json": mappings.fpga_control_status, + "multi": false + }, + "FPGA.FPGA_CURRENT_STATUS.1": { + "json": mappings.fpga_control_status, + "multi": false + }, + "FPGA.FPGA_CURRENT_STATUS.2": { + "json": mappings.fpga_control_status, + "multi": false + }, + "FPGA.FPGA_CURRENT_STATUS.3": { + "json": mappings.fpga_control_status, + "multi": false + }, + "FPGA.FPGA_CURRENT_STATUS.4": { + "json": mappings.fpga_control_status, + "multi": false + }, + "FPGA.FPGA_CURRENT_STATUS.5": { + "json": mappings.fpga_control_status, + "multi": false + }, + "FPGA.FPGA_CURRENT_STATUS.6": { + "json": mappings.fpga_control_status, + "multi": false + }, + "FPGA.FPGA_CURRENT_STATUS.7": { + "json": mappings.fpga_control_status, + "multi": false + }, + "Inverter.ICUFPGASTATUS": { + "json": mappings.fpga_status, + "multi": true + }, + "Inverter.ICUMCUSTATUS": { + "json": mappings.icu_error, + "multi": true + }, + "Inverter.GD_STATUS": { + "json": mappings.gatedriver_status, + "multi": true + }, + "Inverter.ICUMCUSTATE": { + "json": mappings.icu_state, + "multi": false + }, + "Inverter.ICUFPGASTATE": { + "json": mappings.fpga_state, + "multi": false + }, +} + + +window.addEventListener("load", () => { + generate_html(); +}); + +/** + * Initially generate skeleton and add event listeners + */ +function generate_html() { + for (let category in typeset_telemetry_frame.Data) { + // Check for property + if (!typeset_telemetry_frame.Data.hasOwnProperty(category)) { + continue; + } + + // Create new category with a table + let html = `
+ +
+
+ + + +
+
+
+
` + + // Append to main frame + $("#main").append(html); + let table = document.getElementById(`${category}-body`); + + // Append values to table + for (let key in typeset_telemetry_frame.Data[category]) { + // Check for property + if (!typeset_telemetry_frame.Data[category].hasOwnProperty(key)) { + continue; + } + + if (key === "DUMMY") continue; + + let type = typeset_telemetry_frame.Data[category][key] + let unit = typeset_telemetry_frame.Unit[category][key]; + let factor = typeset_telemetry_frame.Factor[category][key]; + + let row = table.insertRow(); + + switch (unit) { + case "boolean": + generate_row_boolean(row, category, key, type, unit,factor); + break; + case "enum": + generate_row_enum(row, category, key, type, unit,factor) + break; + case "0xCAFE": + row.insertCell(0).innerHTML = key; + row.insertCell(1).innerHTML = unit; + row.insertCell(2).innerHTML = ""; + row.insertCell(3).innerHTML = ""; + row.insertCell(4).innerHTML = type; + break; + default: + generate_row_integer(row, category, key, type, unit,factor); + break; + } + } + } +} + +function generate_row_enum(row, category, key, type, unit, factor) { + let name = row.insertCell(0); + let value = row.insertCell(1); + let note1 = row.insertCell(2); + let slider = row.insertCell(3); + let note2 = row.insertCell(4); + + name.innerHTML = key; + note1.innerHTML = unit; + note2.innerHTML = type; + if (type === "uint8" || type === "int8" || type === "uint16" || type === "int16" || type === "uint32" || type === "int32") { + // Load default value + let default_value = (typeof default_values[category][key] == "undefined") ? 0 : default_values[category][key]; + if (default_value === undefined) default_value = 0; + frame[category][key] = default_value; + + value.innerHTML = `
${default_value}
`; + + // Dropdown List + if (enum_values.hasOwnProperty(`${category}.${key}`) && enum_values[`${category}.${key}`].multi === false) { + let enum_value = enum_values[`${category}.${key}`] + // Create Dropdown List + let selectList = document.createElement("select"); + selectList.id = `${category}-${key}`; + selectList.classList.add(...["custom-select", "mr-sm-2", "form-control"]); + + // Skip first state (UNDEFINED) + for (let i = 0; i < enum_value.json.length; i++) { + let option = document.createElement("option"); + option.value = enum_value.json[i].flag; + option.text = enum_value.json[i].description; + selectList.appendChild(option); + } + slider.appendChild(selectList); + + // Select values + const options = Array.from(selectList.options); + const optionToSelect = options.find(item => item.value === default_value.toString()); + if (optionToSelect !== undefined) optionToSelect.selected = true; + + } else if (enum_values.hasOwnProperty(`${category}.${key}`) && enum_values[`${category}.${key}`].multi === true ){ + // Multiselect List + let enum_value = enum_values[`${category}.${key}`] + // Create Dropdown List + let selectList = document.createElement("select"); + selectList.id = `${category}-${key}`; + selectList.multiple = true; + selectList.classList.add(...["custom-select", "mr-sm-2", "form-control"]); + + for (let i = 0; i < enum_value.json.length; i++) { + let option = document.createElement("option"); + option.value = enum_value.json[i].flag; + option.text = enum_value.json[i].error_type; + selectList.appendChild(option); + } + slider.appendChild(selectList); + + // Select values + const options = Array.from(selectList.options); + options.forEach( (item) => { + if (parseInt(item.value) & default_value) item.selected = true; + }); + + } else { + slider.innerHTML = `` + } + // Event listener for value changes + $(`#${category}-${key}`).on('input', function (slideEvt) { + if ($(`#${category}-${key}`).prop("multiple")) { + value = $(this).val().reduce((a,b) => a+parseInt(b),0); + $(`#${category}-${key}-value`).text(value); + frame[category][key] = value; + } else { + $(`#${category}-${key}-value`).text($(this).val()); + frame[category][key] = $(this).val(); + } + }); + + } else { + value.innerHTML = ""; + for (let i = 0; i < type[2]; i++) { + // Load default value + let default_value = (typeof default_values[category][key] == "undefined") ? 0 : default_values[category][key][i]; + if (default_value === undefined) default_value = 0; + frame[category][key][i] = default_value; + + value.innerHTML += `
${default_value}
`; + + + if (enum_values.hasOwnProperty(`${category}.${key}.${i}`) && enum_values[`${category}.${key}.${i}`].multi === false) { + let enum_value = enum_values[`${category}.${key}.${i}`] + // Create Dropdown List + let selectList = document.createElement("select"); + selectList.id = `${category}-${key}-${i}`; + selectList.classList.add(...["custom-select", "mr-sm-2", "form-control"]); + + for (let i = 0; i < enum_value.json.length; i++) { + let option = document.createElement("option"); + option.value = enum_value.json[i].flag; + option.text = enum_value.json[i].description; + selectList.appendChild(option); + } + slider.appendChild(selectList); + + // Select values + const options = Array.from(selectList.options); + const optionToSelect = options.find(item => item.value === default_value.toString()); + if (optionToSelect !== undefined) optionToSelect.selected = true; + + } else if (enum_values.hasOwnProperty(`${category}.${key}.${i}`) && enum_values[`${category}.${key}.${i}`].multi === true ){ + // Multiselect List + let enum_value = enum_values[`${category}.${key}.${i}`] + + // Create Dropdown List + let selectList = document.createElement("select"); + selectList.id = `${category}-${key}-${i}`; + selectList.multiple = true; + selectList.classList.add(...["custom-select", "mr-sm-2", "form-control"]); + + for (let i = 0; i < enum_value.json.length; i++) { + let option = document.createElement("option"); + option.value = enum_value.json[i].flag; + option.text = enum_value.json[i].error_type; + selectList.appendChild(option); + } + slider.appendChild(selectList); + + // Select values + const options = Array.from(selectList.options); + options.forEach( (item) => { + if (parseInt(item.value) & default_value) item.selected = true; + }); + + } else { + slider.innerHTML += `` + } + + + } + for (let i = 0; i < type[2]; i++) { + $(`#${category}-${key}-${i}`).on('input', function (slideEvt) { + if ($(`#${category}-${key}-${i}`).prop("multiple")) { + value = $(this).val().reduce((a, b) => a + parseInt(b), 0); + $(`#${category}-${key}-${i}-value`).text(value); + frame[category][key][i] = value; + } else { + $(`#${category}-${key}-${i}-value`).text($(this).val()); + frame[category][key][i] = $(this).val(); + } + }); + } + + } +} + +function generate_row_boolean(row, category, key, type, unit, factor) { + let name = row.insertCell(0); + let value = row.insertCell(1); + let note1 = row.insertCell(2); + let slider = row.insertCell(3); + let note2 = row.insertCell(4); + + name.innerHTML = key; + note1.innerHTML = unit; + note2.innerHTML = type; + + if (type === "uint8" || type === "int8" || type === "uint16" || type === "int16" || type === "uint32" || type === "int32") { + // Load default value + let default_value = (typeof default_values[category] == "undefined") ? 0 : default_values[category][key]; + if (default_value === undefined) default_value = 0; + frame[category][key] = default_value; + + value.innerHTML = `
${default_value}
`; + slider.innerHTML = `
+ +
` + // Event listener for value changes + $(`#${category}-${key}`).change(function (slideEvt) { + $(`#${category}-${key}-value`).text(+$(this).is(':checked')); + frame[category][key] = $(this).is(':checked'); + }); + } else { + value.innerHTML = ""; + for (let i = 0; i < type[2]; i++) { + // Load default value + let default_value = (typeof default_values[category] == "undefined") ? 0 : default_values[category][key][i]; + if (default_value === undefined) default_value = 0; + frame[category][key][i] = default_value; + + value.innerHTML += `
${default_value}
`; + slider.innerHTML += `
+ +
` + + } + for (let i = 0; i < type[2]; i++) { + $(`#${category}-${key}-${i}`).change(function (slideEvt) { + $(`#${category}-${key}-${i}-value`).text(+$(this).is(':checked')); + frame[category][key][i] = $(this).is(':checked'); + }); + } + } +} + +function generate_row_integer(row, category, key, type, unit, factor) { + let name = row.insertCell(0); + let value = row.insertCell(1); + let note1 = row.insertCell(2); + let slider = row.insertCell(3); + let note2 = row.insertCell(4); + + name.innerHTML = key; + note1.innerHTML = (factor === 1) ? unit : unit + "/" + factor; + note2.innerHTML = type; + + // Single Values + if (typeof type == "string") { + // Load default value + let default_value = (typeof default_values[category] == "undefined") ? 0 : default_values[category][key]; + if (default_value === undefined) default_value = 0; + frame[category][key] = default_value; + + value.innerHTML = ``; + + if (type === "uint16" || type === "uint32") { + slider.innerHTML = `` + } else if ( type === "int16" || type === "int32") { + slider.innerHTML = `` + } else if (type === "uint8") { + slider.innerHTML = `` + } else { + slider.innerHTML = `` + } + + let slider_object = $(`#${category}-${key}`); + slider_object.slider(); + slider_object.on("slide", function (slideEvt) { + $(`#${category}-${key}-value`).val(slideEvt.value); + frame[category][key] = slideEvt.value; + }); + + $(`#${category}-${key}-value`).on("change", function(value) { + slider_object.slider( "setValue", $(`#${category}-${key}-value`).val()); + frame[category][key] = $(`#${category}-${key}-value`).val(); + }); + + } else { + // Arrays + value.innerHTML = ""; + for (let i = 0; i < type[2]; i++) { + // Load default value + let default_value = (typeof default_values[category][key] == "undefined") ? 0 : default_values[category][key][i]; + if (default_value === undefined) default_value = 0; + frame[category][key][i] = default_value; + + if (type[1] === "uint16" || type[1] === "uint32") { + slider.innerHTML += `` + } else if (type[1] === "int16" || type[1] === "int32") { + slider.innerHTML += `` + } else if (type[1] === "uint8") { + slider.innerHTML += `` + } else if (type[1] === "int8") { + slider.innerHTML += `` + } + value.innerHTML += ``; + + } + for (let i = 0; i < type[2]; i++) { + let slider_object = $(`#${category}-${key}-${i}`); + slider_object.slider(); + slider_object.on("slide", function (slideEvt) { + $(`#${category}-${key}-${i}-value`).val(slideEvt.value); + frame[category][key][i] = slideEvt.value; + }); + + $(`#${category}-${key}-${i}-value`).on("change", function(value) { + slider_object.slider( "setValue", $(`#${category}-${key}-${i}-value`).val()); + frame[category][key][i] = $(`#${category}-${key}-${i}-value`).val(); + }); + } + } + +} \ No newline at end of file diff --git a/mock_server/json/default_values.json b/mock_server/json/default_values.json new file mode 100644 index 0000000..a53c056 --- /dev/null +++ b/mock_server/json/default_values.json @@ -0,0 +1,477 @@ +{ + "State": { + "STATE": 3, + "VCU_EMERGENCY_REASON": 7, + "VCU_ERRORS": 0, + "RUN_TIMER": 1000, + "LOG_FILE_NUM": 1, + "LOG_DISCARDED_DATA": 1 + }, + "Brake": { + "BRAKE_ENGAGE": 0, + "BRAKE_STATE": 2, + "BRAKE_VALVEEMERGENCYBRAKE": 0, + "BRAKE_VALVEENGAGEBRAKE": 0, + "BRAKE_CONTROLLEDBRAKINGPRESSURE": 4, + "BRAKE_PRESSUREREGULATORIN": 4, + "BRAKE_SENSORLEFTENGAGED": 0, + "BRAKE_SENSORRIGHTENGAGED": 0, + "BRAKE_BRAKEDISTANCE": 300, + "BRAKE_BRAKEENGAGETIMELEFT": 50, + "BRAKE_BRAKEENGAGETIMERIGHT": 50, + "BRAKE_TANK": 50000, + "BRAKE_LEFT_ACTION": 100, + "BRAKE_LEFT_RELEASE": 8000, + "BRAKE_RIGHT_ACTION": 100, + "BRAKE_RIGHT_RELEASE": 8000 + }, + "Velocity": { + "CORRAIL_VELOCITY": 10, + "CORRAIL_DISTANCE": 2000, + "CORRAIL_MAX_VELOCITY": 5000, + "CORRAIL_OFFSET_AFTER_RESET": 0, + "GAM900_ACCELERATION_X": 0, + "GAM900_ACCELERATION_Y": 0, + "COMBINED_VELOCITY": 10 + }, + "OM20": { + "OM20_FRONT_FINDOWN": 100, + "OM20_FRONT_FINLEFT": 10, + "OM20_FRONT_FINRIGHT": 10, + "OM20_FRONT_CHASSISLEFT": 100, + "OM20_FRONT_CHASSISRIGHT": 100, + "OM20_BACK_FINDOWN": 100, + "OM20_BACK_FINLEFT": 10, + "OM20_BACK_FINRIGHT": 10, + "OM20_BACK_CHASSISLEFT": 100, + "OM20_BACK_CHASSISRIGHT": 100 + }, + "Motor_Temperatures": { + "TEMP_PT100_2": [ + 20, + 20, + 20, + 20, + 20, + 20, + 20 + ], + "TEMP_PTC_1": [ + 50, + 50, + 50, + 50, + 50, + 50, + 50, + 50 + ], + "TEMP_PT100_MAX": 55, + "TEMP_PTC_MAX": 50 + }, + "LV_Batteries": { + "LV_BAT1_CURRENT": 1.2, + "LV_BAT1_VOLTAGE": 24.5, + "LV_BAT1_POWER": 29.4, + "LV_BAT2_CURRENT": 1.2, + "LV_BAT2_VOLTAGE": 24.5, + "LV_BAT2_POWER": 29.4 + }, + "HV_Batteries": { "HV_ISOLATION": 1, "HV_DO_PRECHARGE": 1, "HV_PRECHARGE_DONE": 1, "NUMBER_OF_BATTERIES": 20 }, + "HV_Left": { + "HV_L_READY": 1, + "HV_L_EMERGENCY": 0, + "HV_L_ERROR": 0, + "HV_L_STATE": 1, + "HV_L_VOLTAGE": 2040, + "HV_L_CURRENT": 80, + "HV_L_MIN_CELL_VOLTAGE": 3.6, + "HV_L_MAX_CELL_VOLTAGE": 3.7, + "HV_L_MAX_CELL_TEMP": 20, + "HV_L_VOLTAGES": [ + 167, + 167, + 167, + 167, + 167, + 167, + 167, + 167, + 175, + 175, + 175, + 175, + 175, + 175, + 175, + 175, + 180, + 180, + 180, + 180, + 180, + 180, + 180, + 180, + 177, + 177, + 177, + 177, + 177, + 177, + 177, + 177, + 169, + 169, + 169, + 169, + 169, + 169, + 169, + 169, + 156, + 156, + 156, + 156, + 156, + 156, + 156, + 156, + 164, + 164, + 164, + 164, + 164, + 164, + 164, + 164, + 188, + 188, + 188, + 188, + 188, + 188, + 188, + 188, + 178, + 178, + 178, + 178, + 178, + 178, + 178, + 178, + 168, + 168, + 168, + 168, + 168, + 168, + 168, + 168, + 169, + 169, + 169, + 169, + 169, + 169, + 169, + 169, + 156, + 156, + 156, + 156, + 156, + 156, + 156, + 156 + ], + "HV_L_TEMPERATURES": [ + 36, + 45, + 40, + 40, + 36, + 37, + 43, + 40, + 40, + 40, + 36, + 37, + 43, + 40, + 40, + 40, + 36, + 37, + 43, + 40, + 45, + 40, + 40, + 36, + 37, + 43, + 40, + 36, + 45, + 40, + 40, + 36, + 37, + 43, + 40, + 40, + 40, + 36, + 37, + 43, + 40, + 40, + 40, + 36, + 37, + 43, + 40, + 45 + ] + }, + "HV_Right": { + "HV_R_READY": 1, + "HV_R_EMERGENCY": 1, + "HV_R_ERROR": 0, + "HV_R_STATE": 1, + "HV_R_VOLTAGE": 2037, + "HV_R_CURRENT": 1, + "HV_R_MIN_CELL_VOLTAGE": 3.6, + "HV_R_MAX_CELL_VOLTAGE": 3.7, + "HV_R_MAX_CELL_TEMP": 20, + "HV_R_VOLTAGES": [ + 167, + 167, + 167, + 167, + 167, + 167, + 167, + 167, + 175, + 175, + 175, + 175, + 175, + 175, + 175, + 175, + 180, + 180, + 180, + 180, + 180, + 180, + 180, + 180, + 177, + 177, + 177, + 177, + 177, + 177, + 177, + 177, + 169, + 169, + 169, + 169, + 169, + 169, + 169, + 169, + 156, + 156, + 156, + 156, + 156, + 156, + 156, + 156, + 164, + 164, + 164, + 164, + 164, + 164, + 164, + 164, + 188, + 188, + 188, + 188, + 188, + 188, + 188, + 188, + 178, + 178, + 178, + 178, + 178, + 178, + 178, + 178, + 168, + 168, + 168, + 168, + 168, + 168, + 168, + 168, + 169, + 169, + 169, + 169, + 169, + 169, + 169, + 169, + 156, + 156, + 156, + 156, + 156, + 156, + 156, + 156 + ], + "HV_R_TEMPERATURES": [ + 36, + 45, + 40, + 40, + 36, + 37, + 43, + 40, + 40, + 40, + 36, + 37, + 43, + 40, + 40, + 40, + 36, + 37, + 43, + 40, + 45, + 40, + 40, + 36, + 37, + 43, + 40, + 36, + 45, + 40, + 40, + 36, + 37, + 43, + 40, + 40, + 40, + 36, + 37, + 43, + 40, + 40, + 40, + 36, + 37, + 43, + 40, + 45 + ] + }, + "FPGA": { "FPGA_CURRENT_ADC_VALUES": [ + 8195, + 8195, + 8195, + 8195, + 8195, + 8195, + 8195, + 8195 + ], "FPGA_CURRENT_STATUS": [ + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3 + ], "FPGA_POSITION": 25 , "FPGA_PHASE": 3, "FPGA_SUBPHASE": 52, "FPGA_FULL_POSITION": 0}, + "Inverter": { + "READY": 1, + "SHUTDOWNDONE": 0, + "ICUFPGAREADY": 1, + "ICUFPGASTATE": 0, + "ICUFPGASTATUS": 0, + "ICUFPGAREADYBITS": 0, + "ICUMCUSTATE": 0, + "ICUMCUSTATUS": 0, + "GD_STATUS": 0, + "MAX_CURRENT": 58, + "GD_OCD": 1, + "BOARD_VOLTAGES": [ + 3.5, + 3.5, + 3.5, + 3.5, + 3.5, + 3.5, + 3.5, + 3.5 + ], + "TEMPERATURE_MOSFETS": [ + 33, + 39, + 38, + 31, + 30, + 33, + 32, + 36, + 31, + 30, + 33, + 32 + ], + "MAX_TEMP": 39, + "REAL_POWER": 500, + "REACTIVE_POWER": 3000 + }, + "Configuration": { + "CONFIG_RUNTYPE": 1, + "CONFIG_MAXVOLTAGE": 350, + "CONFIG_BANDWIDTHLOW": 2, + "CONFIG_BANDWIDTHHIGH": 1, + "CONFIG_TARGETCURRENT": 75, + "CONFIG_ZEROCURRENT": 0, + "CONFIG_OVERCURRENTLIMIT_LO": -100, + "CONFIG_OVERCURRENTLIMIT_HI": 100, + "CONFIG_ADC_MAXOFFSET": 10, + "CONFIG_ADC_MAXNOISERANGE": 6, + "CONFIG_RUNFORWARD": 1, + "CONFIG_RUNDURATION": 5, + "CONFIG_TRACKLENGTH": 20, + "CONFIG_SETPOSITION": 5, + "CONFIG_RUNLENGTH": 5, + "CONFIG_PROPTRACKLENGTH": 20 + }, + "END": { "TELEMETRY_FRAME_END": 1 } +} \ No newline at end of file diff --git a/mock_server/mockserver.html b/mock_server/mockserver.html new file mode 100644 index 0000000..d01b6b5 --- /dev/null +++ b/mock_server/mockserver.html @@ -0,0 +1,105 @@ + + + + + + + Pod MockServer + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + diff --git a/mock_server/mockserver.js b/mock_server/mockserver.js new file mode 100644 index 0000000..8bef5b1 --- /dev/null +++ b/mock_server/mockserver.js @@ -0,0 +1,113 @@ +/** + * @file mockserver.js + * @brief Mockserver to emulate telemetry data of the Swissloop Pod + * + * @author Philip Wiese, philip.wiese@swissloop.ch + * + * @license + * Copyright (C) 2022 Swissloop + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * @version 3.0.0 + * + */ + +const electron = require('electron'); +// Module to control application life. +const app = electron.app; +// attach debugger command line port +app.commandLine.appendSwitch('remote-debugging-port', '9223') +// Module to create native browser window. +const BrowserWindow = electron.BrowserWindow; + +const path = require('path'); +const url = require('url'); + +if (process.env.DEV === 'true') { + console.log("Dev Mode") + require('electron-reload')(__dirname, { + electron: path.join(__dirname,'../', 'node_modules', '.bin', 'electron'), + hardResetMethod: 'exit' + }); +} + +// Keep a global reference of the window object, if you don't, the window will +// be closed automatically when the JavaScript object is garbage collected. +let mainWindow; + +function createWindow () { + // Prevent from + app.commandLine.appendSwitch("disable-background-timer-throttling") + // Prevents Chromium from lowering the priority of invisible pages' renderer processes. + app.commandLine.appendSwitch("disable-renderer-backgrounding"); + + // Create the browser window. + mainWindow = new BrowserWindow({ + width: 1440, + height: 840, + webPreferences: { + nodeIntegration: true, + contextIsolation: false, + webSecurity: false, + allowRunningInsecureContent: true + }, + backgroundThrottling:false + }); + + // Dangerous but this app is only a prototype + process.env['ELECTRON_DISABLE_SECURITY_WARNINGS'] = 'true'; + + // mainWindow.openDevTools(); + + mainWindow.maximize(); + mainWindow.setIcon(path.join(__dirname, '../control_panel', 'img', 'Icon.png')); + + // and load the mockserver.html of the app. + mainWindow.loadURL(url.format({ + pathname: path.join(__dirname, 'mockserver.html'), + protocol: 'file:', + slashes: true + })); + + // Emitted when the window is closed. + mainWindow.on('closed', function () { + // Dereference the window object, usually you would store windows + // in an array if your app supports multi windows, this is the time + // when you should delete the corresponding element. + mainWindow = null; + }); +} + +// This method will be called when Electron has finished +// initialization and is ready to create browser windows. +// Some APIs can only be used after this event occurs. +app.on('ready', createWindow); + +// Quit when all windows are closed. +app.on('window-all-closed', function () { + // On OS X it is common for applications and their menu bar + // to stay active until the user quits explicitly with Cmd + Q + if (process.platform !== 'darwin') { + app.quit(); + } +}); + +app.on('activate', function () { + // On OS X it's common to re-create a window in the app when the + // dock icon is clicked and there are no other windows open. + if (mainWindow === null) { + createWindow(); + } +}); diff --git a/package.json b/package.json new file mode 100644 index 0000000..c399243 --- /dev/null +++ b/package.json @@ -0,0 +1,43 @@ +{ + "name": "control-panel", + "version": "3.0.0", + "description": "Displays telemetry and logs for Swissloop Pods.", + "main": "control_panel/app.js", + "homepage": "https://github.com/swissloop/ControlPanel", + "author": "Philip Wiese (http://https://github.com/Xeratec)", + "license": "LGPL-3.0-or-later", + "repository": { + "type": "git", + "url": "https://github.com/swissloop/ControlPanel" + }, + "scripts": { + "start": "run-script-os", + "start:nix": "DEV=true ELECTRON_DISABLE_SECURITY_WARNINGS=true electron .", + "start:win32": "set DEV=true&&set ELECTRON_DISABLE_SECURITY_WARNINGS=true&&electron .", + "mock": "run-script-os", + "mock:nix": "DEV=true ELECTRON_DISABLE_SECURITY_WARNINGS=true electron mock_server/mockserver.js", + "mock:win32": "set DEV=true&&set ELECTRON_DISABLE_SECURITY_WARNINGS=true&&electron mock_server/mockserver.js", + "generate_parser": "node control_panel/js/generator.js" + }, + "devDependencies": { + "electron-reload": "^1.5.0" + }, + "dependencies": { + "@arction/lcjs": "^3.4.0", + "@electron/remote": "^2.0.8", + "bootstrap-slider": "^11.0.2", + "bootstrap": "^4.6.0", + "dataframe-js": "^1.4.4", + "electron-localshortcut": "^3.2.1", + "electron": "^21.3.1", + "fast-csv": "^4.3.6", + "jbinary": "^2.1.5", + "jquery": "^3.6.1", + "moment": "^2.29.4", + "node-fetch": "^3.2.10", + "npm": "^8.19.3", + "popper.js": "^1.16.1", + "riot": "^3.13.2", + "run-script-os": "^1.1.6" + } +} diff --git a/screenshot.png b/screenshot.png new file mode 100644 index 0000000..1cd96c3 Binary files /dev/null and b/screenshot.png differ diff --git a/screenshot_full.png b/screenshot_full.png new file mode 100644 index 0000000..6fa0756 Binary files /dev/null and b/screenshot_full.png differ