First commit for gui branch
@@ -0,0 +1,25 @@
|
||||
.DS_Store
|
||||
node_modules
|
||||
/dist
|
||||
|
||||
# local env files
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# Log files
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
|
||||
# Editor directories and files
|
||||
.idea
|
||||
.vscode
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
#Electron-builder output
|
||||
/dist_electron
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2020 ODrive Robotics
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,45 @@
|
||||
# odrive_gui
|
||||
This is the repo for the ODrive GUI
|
||||
|
||||
Flask (python 3) backend with Vue.js frontend, packaged with Electron. Windows and Linux binaries can be found under the [Releases](https://github.com/PAJohnson/odrive_gui/releases) page.
|
||||
|
||||
Python requirements: `pip install flask flask-socketio flask-cors odrive`
|
||||
|
||||
If the default odrive python package is not desired, the path to the modules can be passed as command line arguments.
|
||||
|
||||
example on windows 10:
|
||||
```
|
||||
./odrive_gui_win.exe C:/Users/<you>/ODrive/tools C:/Users/<you>/ODrive/Firmware
|
||||
```
|
||||
|
||||
The first argument is for your local version of odrivetool, the second is for fibre.
|
||||
|
||||
## Development and testing instructions
|
||||
|
||||
### Project setup
|
||||
```
|
||||
npm install
|
||||
```
|
||||
|
||||
### Compiles and hot-reloads for development
|
||||
```
|
||||
npm run serve
|
||||
```
|
||||
|
||||
### Lints and fixes files
|
||||
```
|
||||
npm run lint
|
||||
```
|
||||
|
||||
### Serve electron version of GUI
|
||||
```
|
||||
npm run electron:serve
|
||||
```
|
||||
|
||||
### Package electron app into executable
|
||||
```
|
||||
npm run electron:build
|
||||
```
|
||||
|
||||
### Customize configuration
|
||||
See [Configuration Reference](https://cli.vuejs.org/config/).
|
||||
@@ -0,0 +1,5 @@
|
||||
module.exports = {
|
||||
presets: [
|
||||
'@vue/cli-plugin-babel/preset'
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
{
|
||||
"name": "odrive_gui",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"serve": "vue-cli-service serve",
|
||||
"build": "vue-cli-service build",
|
||||
"lint": "vue-cli-service lint",
|
||||
"electron:build": "vue-cli-service electron:build",
|
||||
"electron:serve": "vue-cli-service electron:serve",
|
||||
"postinstall": "electron-builder install-app-deps",
|
||||
"postuninstall": "electron-builder install-app-deps"
|
||||
},
|
||||
"main": "background.js",
|
||||
"dependencies": {
|
||||
"axios": "^0.19.2",
|
||||
"chart.js": "^2.9.3",
|
||||
"chartjs-plugin-streaming": "^1.8.0",
|
||||
"core-js": "^3.6.5",
|
||||
"electron": "^9.1.1",
|
||||
"file-saver": "^2.0.2",
|
||||
"socket.io": "^2.3.0",
|
||||
"socket.io-client": "^2.3.0",
|
||||
"uuid": "^8.2.0",
|
||||
"vue": "^2.6.11",
|
||||
"vue-chartjs": "^3.5.0",
|
||||
"vue-context": "^5.2.0",
|
||||
"vue-json-component": "^0.4.1",
|
||||
"vue-slider-component": "^3.2.2",
|
||||
"vue-socket.io": "^3.0.9",
|
||||
"vuex": "^3.5.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vue/cli-plugin-babel": "~4.4.0",
|
||||
"@vue/cli-plugin-eslint": "~4.4.0",
|
||||
"@vue/cli-plugin-router": "^4.4.6",
|
||||
"@vue/cli-service": "~4.4.0",
|
||||
"babel-eslint": "^10.1.0",
|
||||
"electron": "^9.0.0",
|
||||
"electron-devtools-installer": "^3.1.0",
|
||||
"eslint": "^6.7.2",
|
||||
"eslint-plugin-vue": "^6.2.2",
|
||||
"vue-cli-plugin-electron-builder": "~2.0.0-rc.4",
|
||||
"vue-cli-plugin-yaml": "^1.0.2",
|
||||
"vue-json-component": "^0.4.1",
|
||||
"vue-template-compiler": "^2.6.11"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"root": true,
|
||||
"env": {
|
||||
"node": true
|
||||
},
|
||||
"extends": [
|
||||
"plugin:vue/essential",
|
||||
"eslint:recommended"
|
||||
],
|
||||
"parserOptions": {
|
||||
"parser": "babel-eslint"
|
||||
},
|
||||
"rules": {}
|
||||
},
|
||||
"browserslist": [
|
||||
"> 1%",
|
||||
"last 2 versions",
|
||||
"not dead"
|
||||
]
|
||||
}
|
||||
|
After Width: | Height: | Size: 4.2 KiB |
@@ -0,0 +1,17 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
|
||||
<title><%= htmlWebpackPlugin.options.title %></title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>
|
||||
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
|
||||
</noscript>
|
||||
<div id="app"></div>
|
||||
<!-- built files will be auto injected -->
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,200 @@
|
||||
import sys
|
||||
import flask
|
||||
import os
|
||||
from flask import make_response, request, jsonify, session
|
||||
from flask_socketio import SocketIO, send, emit
|
||||
from flask_cors import CORS
|
||||
import json
|
||||
import time
|
||||
import argparse
|
||||
|
||||
|
||||
# interface for odrive GUI to get data from odrivetool
|
||||
|
||||
#better handling of websockets
|
||||
# eventlet.monkey_patch()
|
||||
|
||||
app = flask.Flask(__name__)
|
||||
app.config['SECRET_KEY'] = 'secret'
|
||||
app.config.update(
|
||||
SESSION_COOKIE_SECURE=True,
|
||||
SESSION_COOKIE_HTTPONLY=True,
|
||||
SESSION_COOKIE_SAMESITE='None'
|
||||
)
|
||||
CORS(app, support_credentials=True)
|
||||
socketio = SocketIO(app, cors_allowed_origins="*")
|
||||
odrives = []
|
||||
odriveDict = {}
|
||||
configDict = {}
|
||||
|
||||
def get_all_odrives():
|
||||
odrives = []
|
||||
odrives.append(odrive.find_any()) #, find_multiple=100)
|
||||
#odrives.append(odrive.find_any())
|
||||
return odrives
|
||||
|
||||
@socketio.on('enableSampling')
|
||||
def enableSampling(message):
|
||||
print("sampling enabled")
|
||||
session['samplingEnabled'] = True
|
||||
emit('samplingEnabled')
|
||||
|
||||
@socketio.on('stopSampling')
|
||||
def stopSampling(message):
|
||||
session['samplingEnabled'] = False
|
||||
emit('samplingDisabled')
|
||||
|
||||
@socketio.on('sampledVarNames')
|
||||
def sampledVarNames(message):
|
||||
session['sampledVars'] = message
|
||||
print(session['sampledVars'])
|
||||
|
||||
@socketio.on('startSampling')
|
||||
def sendSamples(message):
|
||||
print(session['samplingEnabled'])
|
||||
while session['samplingEnabled']:
|
||||
emit('sampledData', json.dumps(getSampledData(session['sampledVars'])))
|
||||
time.sleep(0.02)
|
||||
|
||||
@socketio.on('message')
|
||||
def handle_message(message):
|
||||
print(message)
|
||||
emit('response', 'hello from server!')
|
||||
|
||||
@app.route('/', methods=['GET'])
|
||||
def home():
|
||||
return "<h1>ODrive GUI Server</h1>"
|
||||
|
||||
|
||||
@app.route('/api/odrives', methods=["GET"])
|
||||
def api_odrives():
|
||||
for (index, odrv) in enumerate(odrives):
|
||||
odriveDict["odrive" + str(index)] = dictFromRO(odrv)
|
||||
response = jsonify(odriveDict)
|
||||
response.headers.add('Access-Control-Allow-Origin', '*')
|
||||
return response
|
||||
|
||||
|
||||
@app.route('/api/property', methods=["GET", "PUT"])
|
||||
def api_property():
|
||||
# here, reqDict["key"] is a list of keys from the query
|
||||
# ?key=odrive0&key=axis0&key=config...
|
||||
if request.method == 'PUT':
|
||||
reqDict = request.args.to_dict(flat=False)
|
||||
postVal(odrives, reqDict["key"], reqDict["val"][0], reqDict["type"][0])
|
||||
response = make_response(jsonify({"message": "success"}), 200)
|
||||
return response
|
||||
else:
|
||||
print("request: " + str(request))
|
||||
reqDict = request.args.to_dict(flat=False)
|
||||
response = jsonify(getVal(odrives, reqDict["key"]))
|
||||
response.headers.add('Access-Control-Allow-Origin', '*')
|
||||
return response
|
||||
|
||||
|
||||
@app.route('/api/function', methods=["PUT"])
|
||||
def api_function():
|
||||
# execute a function from the odrive config dict?
|
||||
reqDict = request.args.to_dict(flat=False)
|
||||
callFunc(odrives, reqDict["key"])
|
||||
response = make_response(jsonify({"message": "success"}), 200)
|
||||
return response
|
||||
|
||||
|
||||
def dictFromRO(RO):
|
||||
# create dict from an odrive RemoteObject that's suitable for sending as JSON
|
||||
returnDict = {}
|
||||
for key in RO._remote_attributes.keys():
|
||||
if isinstance(RO._remote_attributes[key], fibre.remote_object.RemoteObject):
|
||||
# recurse
|
||||
returnDict[key] = dictFromRO(RO._remote_attributes[key])
|
||||
elif isinstance(RO._remote_attributes[key], fibre.remote_object.RemoteProperty):
|
||||
# grab value of that property
|
||||
# indicate if this property can be written or not
|
||||
returnDict[key] = {"val": str(RO._remote_attributes[key].get_value()),
|
||||
"readonly": not RO._remote_attributes[key]._can_write,
|
||||
"type": str(RO._remote_attributes[key]._property_type.__name__)}
|
||||
elif isinstance(RO._remote_attributes[key], fibre.remote_object.RemoteFunction):
|
||||
# this is a function - do nothing for now.
|
||||
returnDict[key] = "function"
|
||||
else:
|
||||
returnDict[key] = RO._remote_attributes[key]
|
||||
return returnDict
|
||||
|
||||
# set a value from a POST http request
|
||||
|
||||
|
||||
def postVal(odrives, keyList, value, argType):
|
||||
# expect a list of keys in the form of ["key1", "key2", "keyN"]
|
||||
# "key1" will be "odriveN"
|
||||
# like this: postVal(odrives, ["odrive0","axis0","config","calibration_lockin","accel"], 17.0)
|
||||
index = int(''.join([char for char in keyList.pop(0) if char.isnumeric()]))
|
||||
|
||||
RO = odrives[index]
|
||||
for key in keyList:
|
||||
RO = RO._remote_attributes[key]
|
||||
if argType == "numeric":
|
||||
RO.set_value(float(value))
|
||||
elif argType == "boolean":
|
||||
RO.set_value(value == "true")
|
||||
else:
|
||||
pass # dont support that type yet
|
||||
|
||||
def getVal(odrives, keyList):
|
||||
index = int(''.join([char for char in keyList.pop(0) if char.isnumeric()]))
|
||||
RO = odrives[index]
|
||||
try:
|
||||
for key in keyList:
|
||||
RO = RO._remote_attributes[key]
|
||||
if isinstance(RO, fibre.remote_object.RemoteObject):
|
||||
return dictFromRO(RO)
|
||||
else:
|
||||
return RO.get_value()
|
||||
except:
|
||||
return 0
|
||||
|
||||
def getSampledData(vars):
|
||||
#use getVal to populate a dict
|
||||
#return a dict {path:value}
|
||||
samples = {}
|
||||
for path in vars["paths"]:
|
||||
keys = path.split('.')
|
||||
samples[path] = getVal(odrives, keys)
|
||||
|
||||
return samples
|
||||
|
||||
# call a function from a GET request
|
||||
|
||||
|
||||
def callFunc(odrives, keyList):
|
||||
index = int(''.join([char for char in keyList.pop(0) if char.isnumeric()]))
|
||||
RO = odrives[index]
|
||||
for key in keyList:
|
||||
RO = RO._remote_attributes[key]
|
||||
if isinstance(RO, fibre.remote_object.RemoteFunction):
|
||||
RO.__call__()
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("args from python:")
|
||||
print(sys.argv[1:])
|
||||
# try to import based on command line arguments or config file
|
||||
|
||||
for optPath in sys.argv[1:]:
|
||||
print("adding " + str(optPath.rstrip()) + " to import path for odrive_server.py")
|
||||
sys.path.insert(0,optPath.rstrip())
|
||||
|
||||
import odrive
|
||||
import odrive.utils # for dump_errors()
|
||||
import fibre
|
||||
|
||||
|
||||
# busy wait for connection
|
||||
while len(odrives) == 0:
|
||||
print("looking for odrives...")
|
||||
odrives = get_all_odrives()
|
||||
|
||||
print("found odrives!")
|
||||
|
||||
for (index, odrv) in enumerate(odrives):
|
||||
odriveDict["odrive" + str(index)] = dictFromRO(odrv)
|
||||
socketio.run(app, host='0.0.0.0', port=5000)
|
||||
@@ -0,0 +1,342 @@
|
||||
<template>
|
||||
<div id="app">
|
||||
<!-- HEADER -->
|
||||
<div class="header">
|
||||
<button
|
||||
class="dash-button"
|
||||
@click="startsample"
|
||||
v-bind:class="[{active: sampling === true}]"
|
||||
>start sampling</button>
|
||||
<button class="dash-button" @click="stopsample">stop sampling</button>
|
||||
<button class="dash-button" @click="exportDash">export dash</button>
|
||||
<button class="dash-button" @click="importDashWrapper">
|
||||
import dash
|
||||
<input
|
||||
type="file"
|
||||
id="inputDash"
|
||||
ref="fileInput"
|
||||
@change="importDashFile($event.target.files);$refs.fileInput.value=null"
|
||||
value
|
||||
style="display:none"
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
v-for="dash in dashboards"
|
||||
v-bind:key="dash.id"
|
||||
v-bind:class="['dash-button', { active: currentDash === dash.name}]"
|
||||
v-on:click.self="currentDash = dash.name"
|
||||
v-on:dblclick="changeDashName(dash.id)"
|
||||
>
|
||||
<button
|
||||
v-if="dash.name !== 'Start' && dash.name !== 'Config' && dash.name !== 'Wizard'"
|
||||
class="close-button"
|
||||
v-on:click="deleteDash(dash.id)"
|
||||
>X</button>
|
||||
{{ dash.name }}
|
||||
</button>
|
||||
<button class="dash-button dash-add" @click="addDash">+</button>
|
||||
<button class="emergency-stop" @click="estop">STOP</button>
|
||||
</div>
|
||||
|
||||
<!-- PAGE CONTENT -->
|
||||
<component
|
||||
v-bind:is="currentDashName"
|
||||
v-bind:odrives="odrives"
|
||||
v-bind:dash="dash"
|
||||
></component>
|
||||
|
||||
<!-- FOOTER -->
|
||||
<div class="footer">
|
||||
<Axis v-for="axis in axes" v-bind:key="axis.name" v-bind:axis="axis" v-bind:odrives="odrives"></Axis>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Start from "./views/Start.vue";
|
||||
import Dashboard from "./views/Dashboard.vue";
|
||||
import Axis from "./components/Axis.vue";
|
||||
import Wizard from "./views/Wizard.vue"
|
||||
import * as socketio from "./comms/socketio";
|
||||
import { saveAs } from "file-saver";
|
||||
import ConfigDash from "./assets/dashboards/Config.json";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
//let propSamplePeriod = 100; //sampling period for properties in ms
|
||||
|
||||
export default {
|
||||
name: "App",
|
||||
components: {
|
||||
Start,
|
||||
Dashboard,
|
||||
Axis,
|
||||
Wizard,
|
||||
},
|
||||
data: function () {
|
||||
return {
|
||||
currentDash: "Start",
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
currentDashName: function () {
|
||||
//get the appropriate component name from the currentDash variable
|
||||
let comp = {};
|
||||
for (const dash of this.dashboards) {
|
||||
if (dash.name === this.currentDash) {
|
||||
comp = dash.component;
|
||||
}
|
||||
}
|
||||
return comp;
|
||||
},
|
||||
dash: function () {
|
||||
let comp = {};
|
||||
for (const dash of this.dashboards) {
|
||||
if (dash.name === this.currentDash) {
|
||||
comp = dash;
|
||||
}
|
||||
}
|
||||
return comp;
|
||||
},
|
||||
currentCtrlList: function () {
|
||||
let comp = {};
|
||||
for (const dash of this.dashboards) {
|
||||
if (dash.name === this.currentDash) {
|
||||
comp = dash.controls;
|
||||
}
|
||||
}
|
||||
return comp;
|
||||
},
|
||||
axes: function () {
|
||||
return this.$store.state.axes;
|
||||
},
|
||||
odrives: function () {
|
||||
return this.$store.state.odrives;
|
||||
},
|
||||
dashboards: function () {
|
||||
return this.$store.state.dashboards;
|
||||
},
|
||||
sampling: function () {
|
||||
return this.$store.state.sampling;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
updateOdrives() {
|
||||
if (this.$store.state.serverConnected == true) {
|
||||
//} && this.sampling == false) {
|
||||
this.$store.dispatch("getOdrives");
|
||||
}
|
||||
setTimeout(() => {
|
||||
this.updateOdrives();
|
||||
}, 1000);
|
||||
|
||||
//console.log("updating data...");
|
||||
},
|
||||
addDash() {
|
||||
let dashname = "Dashboard " + (this.dashboards.length - 2);
|
||||
this.dashboards.push({
|
||||
component: "Dashboard",
|
||||
name: dashname,
|
||||
id: uuidv4(),
|
||||
controls: [],
|
||||
actions: [],
|
||||
plots: [],
|
||||
});
|
||||
},
|
||||
deleteDash(dashID) {
|
||||
this.currentDash = "Start";
|
||||
console.log("Deleting dash " + dashID);
|
||||
for (const dash of this.dashboards) {
|
||||
if (dashID === dash.id) {
|
||||
this.dashboards.splice(this.dashboards.indexOf(dash), 1);
|
||||
}
|
||||
}
|
||||
},
|
||||
exportDash() {
|
||||
console.log("exporting dashboard");
|
||||
const blob = new Blob([JSON.stringify(this.dash, null, 2)], {
|
||||
type: "application/json",
|
||||
});
|
||||
saveAs(blob, this.dash.name);
|
||||
},
|
||||
importDashWrapper() {
|
||||
const inputElem = document.getElementById("inputDash");
|
||||
if (inputElem) {
|
||||
console.log("importing dashboard");
|
||||
inputElem.click();
|
||||
}
|
||||
},
|
||||
importDashFile(files) {
|
||||
console.log("file handler callback");
|
||||
let file = files[0];
|
||||
const reader = new FileReader();
|
||||
// this is ugly, but it gets around scoping problems the "load" callback
|
||||
let dashes = this.dashboards;
|
||||
let addImportedDash = (dash) => {
|
||||
console.log(dash);
|
||||
dashes.push(dash);
|
||||
// plots will have variables associated, add them to sampled variables list
|
||||
for (const plot of dash.plots) {
|
||||
console.log(plot);
|
||||
for (const plotVar of plot.vars) {
|
||||
console.log(plotVar);
|
||||
//addsampledprop(path);
|
||||
this.$store.commit("addSampledProperty", plotVar.path);
|
||||
}
|
||||
}
|
||||
};
|
||||
reader.addEventListener("load", function (e) {
|
||||
addImportedDash(JSON.parse(e.target.result));
|
||||
});
|
||||
reader.readAsText(file);
|
||||
},
|
||||
changeDashName(e) {
|
||||
console.log(e);
|
||||
console.log("double clicked dashboard name");
|
||||
},
|
||||
startsample() {
|
||||
socketio.sendEvent({
|
||||
type: "sampledVarNames",
|
||||
data: {
|
||||
paths: this.$store.state.sampledProperties,
|
||||
},
|
||||
});
|
||||
socketio.sendEvent({
|
||||
type: "enableSampling",
|
||||
});
|
||||
this.$store.state.timeSampleStart = Date.now();
|
||||
this.$store.state.sampling = true;
|
||||
},
|
||||
stopsample() {
|
||||
socketio.sendEvent({
|
||||
type: "stopSampling",
|
||||
});
|
||||
this.$store.state.sampling = false;
|
||||
},
|
||||
estop() {
|
||||
// send stop command to odrives
|
||||
// behavior on reset?
|
||||
},
|
||||
},
|
||||
created() {
|
||||
//grab full JSON
|
||||
//this.getOdrives();
|
||||
|
||||
this.$store.dispatch("setServerAddress", "http://127.0.0.1:5000");
|
||||
// connect to socketio on server for sampled data
|
||||
this.updateOdrives();
|
||||
this.dashboards.push(ConfigDash);
|
||||
// plots will have variables associated, add them to sampled variables list
|
||||
for (const plot of ConfigDash.plots) {
|
||||
console.log(plot);
|
||||
for (const plotVar of plot.vars) {
|
||||
console.log(plotVar);
|
||||
//addsampledprop(path);
|
||||
this.$store.commit("addSampledProperty", plotVar.path);
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@import url("https://fonts.googleapis.com/css2?family=Roboto+Mono:wght@400;700&family=Roboto:wght@400;700&display=swap");
|
||||
@import "./assets/styles/vars.css";
|
||||
@import "./assets/styles/style.css";
|
||||
|
||||
* {
|
||||
/* font-family: Arial, Helvetica, sans-serif; */
|
||||
font-family: "Roboto", sans-serif;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
#app {
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.header {
|
||||
/* want this fixed and full width */
|
||||
position: fixed;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
width: 100vw;
|
||||
display: flex;
|
||||
background-color: var(--fg-color);
|
||||
box-shadow: 0 0px 8px 0 rgba(0, 0, 0, 0.4);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
button {
|
||||
font-size: 1rem;
|
||||
color: #2c3e50;
|
||||
text-decoration: none;
|
||||
padding: 10px;
|
||||
background-color: var(--fg-color);
|
||||
border-style: none;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.dash-button:active {
|
||||
background-color: var(--bg-color);
|
||||
}
|
||||
|
||||
.active {
|
||||
color: #000000;
|
||||
background-color: var(--bg-color);
|
||||
}
|
||||
|
||||
.footer {
|
||||
position: fixed;
|
||||
width: 100vw;
|
||||
left: 0px;
|
||||
bottom: 0px;
|
||||
display: flex;
|
||||
background-color: var(--fg-color);
|
||||
box-shadow: 0 0px 8px 0 rgba(0, 0, 0, 0.4);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.footer .left,
|
||||
.right {
|
||||
/* flex-grow: 1; */
|
||||
display: flex;
|
||||
background-color: var(--fg-color);
|
||||
font-family: "Roboto Mono", monospace;
|
||||
margin: auto 5px;
|
||||
}
|
||||
|
||||
.odrvSer,
|
||||
.errorState {
|
||||
font-weight: bold;
|
||||
font-family: "Roboto Mono", monospace;
|
||||
margin: auto 5px;
|
||||
background-color: var(--fg-color);
|
||||
}
|
||||
|
||||
.errorState {
|
||||
color: #13a100;
|
||||
}
|
||||
|
||||
.dash-add {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.parameter-button {
|
||||
border-right: 1px solid lightgrey;
|
||||
}
|
||||
|
||||
.emergency-stop {
|
||||
margin-left: auto;
|
||||
padding-left: 2rem;
|
||||
padding-right: 2rem;
|
||||
background-color: red;
|
||||
font-weight: bold;
|
||||
color: white;
|
||||
display: none;
|
||||
}
|
||||
|
||||
json-view {
|
||||
z-index: 2;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,97 @@
|
||||
{
|
||||
"name": "Config",
|
||||
"component": "Dashboard",
|
||||
"id": "deadb33f",
|
||||
"controls": [
|
||||
{
|
||||
"controlType": "CtrlNumeric",
|
||||
"path": "odrives.odrive0.axis0.motor.config.current_lim"
|
||||
},
|
||||
{
|
||||
"controlType": "CtrlNumeric",
|
||||
"path": "odrives.odrive0.axis0.controller.config.vel_limit"
|
||||
},
|
||||
{
|
||||
"controlType": "CtrlNumeric",
|
||||
"path": "odrives.odrive0.config.brake_resistance"
|
||||
},
|
||||
{
|
||||
"controlType": "CtrlNumeric",
|
||||
"path": "odrives.odrive0.axis0.motor.config.pole_pairs"
|
||||
},
|
||||
{
|
||||
"controlType": "CtrlNumeric",
|
||||
"path": "odrives.odrive0.axis0.encoder.config.cpr"
|
||||
},
|
||||
{
|
||||
"controlType": "CtrlSlider",
|
||||
"path": "odrives.odrive0.axis0.controller.config.pos_gain"
|
||||
},
|
||||
{
|
||||
"controlType": "CtrlSlider",
|
||||
"path": "odrives.odrive0.axis0.controller.config.vel_gain"
|
||||
},
|
||||
{
|
||||
"controlType": "CtrlSlider",
|
||||
"path": "odrives.odrive0.axis0.controller.config.vel_integrator_gain"
|
||||
},
|
||||
{
|
||||
"controlType": "CtrlFunction",
|
||||
"path": "odrives.odrive0.axis0.clear_errors"
|
||||
},
|
||||
{
|
||||
"controlType": "CtrlFunction",
|
||||
"path": "odrives.odrive0.save_configuration"
|
||||
}
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"id": "407aea0a-2b16-4d77-8043-2c43b27e4810",
|
||||
"path": "odrives.odrive0.axis0.requested_state",
|
||||
"val": 3
|
||||
},
|
||||
{
|
||||
"id": "91b2c1b3-f1bb-4473-9e7a-dd89615cecc9",
|
||||
"path": "odrives.odrive0.axis0.requested_state",
|
||||
"val": 8
|
||||
},
|
||||
{
|
||||
"id": "ad0ebe2a-a495-4e33-9cb8-b2ab17dc3a83",
|
||||
"path": "odrives.odrive0.axis0.controller.pos_setpoint",
|
||||
"val": 0
|
||||
},
|
||||
{
|
||||
"id": "d7abba09-b0aa-438a-8220-6126e467004e",
|
||||
"path": "odrives.odrive0.axis0.controller.pos_setpoint",
|
||||
"val": 50000
|
||||
}
|
||||
],
|
||||
"plots": [
|
||||
{
|
||||
"name": "65a2768d-e61e-4dd5-bf80-dba153838f74",
|
||||
"vars": [
|
||||
{
|
||||
"path": "odrives.odrive0.axis0.controller.pos_setpoint",
|
||||
"color": "#195bd7"
|
||||
},
|
||||
{
|
||||
"path": "odrives.odrive0.axis0.encoder.pos_estimate",
|
||||
"color": "#d6941a"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "d9ee2474-3e53-408f-9eab-1656342eb531",
|
||||
"vars": [
|
||||
{
|
||||
"path": "odrives.odrive0.axis0.controller.vel_setpoint",
|
||||
"color": "#195bd7"
|
||||
},
|
||||
{
|
||||
"path": "odrives.odrive0.axis0.encoder.vel_estimate",
|
||||
"color": "#d6941a"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
After Width: | Height: | Size: 934 KiB |
|
After Width: | Height: | Size: 82 KiB |
|
After Width: | Height: | Size: 1.0 MiB |
|
After Width: | Height: | Size: 81 KiB |
|
After Width: | Height: | Size: 694 KiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 6.7 KiB |
|
After Width: | Height: | Size: 52 KiB |
@@ -0,0 +1,18 @@
|
||||
.card {
|
||||
padding: 10px;
|
||||
margin: 10px;
|
||||
background-color: #fff;
|
||||
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.close-button {
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
padding: 0 5px;
|
||||
margin-right: 10px;
|
||||
border: 1px solid black;
|
||||
}
|
||||
|
||||
.close-button:active {
|
||||
background-color: var(--bg-color);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
:root {
|
||||
--bg-color: rgb(235, 235, 235);
|
||||
--fg-color: #fff;
|
||||
--top-height: 39px;
|
||||
--bottom-height: 29px;
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
'use strict'
|
||||
|
||||
import { app, protocol, BrowserWindow } from 'electron'
|
||||
import { createProtocol } from 'vue-cli-plugin-electron-builder/lib'
|
||||
import installExtension, { VUEJS_DEVTOOLS } from 'electron-devtools-installer'
|
||||
const isDevelopment = process.env.NODE_ENV !== 'production'
|
||||
|
||||
const { spawnSync, execSync } = require('child_process');
|
||||
const spawn = require('child_process').spawn;
|
||||
const path = require('path');
|
||||
|
||||
// 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 win
|
||||
|
||||
// Scheme must be registered before the app is ready
|
||||
protocol.registerSchemesAsPrivileged([
|
||||
{ scheme: 'app', privileges: { secure: true, standard: true } }
|
||||
])
|
||||
|
||||
// function to get determine correct command for python
|
||||
function getPyCmd() {
|
||||
let spawnRet = spawnSync('python',['-V']);
|
||||
let vString;
|
||||
if (spawnRet.stdout.toString().length > 1){
|
||||
vString = spawnRet.stdout.toString();
|
||||
}
|
||||
else {
|
||||
vString = spawnRet.stderr.toString();
|
||||
}
|
||||
|
||||
if (vString.split(' ')[1].split('.')[0] == '2') {
|
||||
return 'python3';
|
||||
}
|
||||
else {
|
||||
return 'python';
|
||||
}
|
||||
}
|
||||
|
||||
function createWindow() {
|
||||
// Spawn the python server
|
||||
let scriptFilename = path.join(app.getAppPath(), '../server', 'odrive_server.py');
|
||||
const args = process.argv;
|
||||
let effectiveCommand = [];
|
||||
effectiveCommand.push(scriptFilename);
|
||||
if (app.isPackaged === true) {
|
||||
for (const arg of args.slice(1)) {
|
||||
effectiveCommand.push(arg);
|
||||
}
|
||||
}
|
||||
else {
|
||||
for (const arg of args.slice(2)) {
|
||||
effectiveCommand.push(arg);
|
||||
}
|
||||
}
|
||||
var python = spawn(getPyCmd(), effectiveCommand);
|
||||
python.stdout.on('data',function(data) {
|
||||
console.log(data.toString('utf8'));
|
||||
});
|
||||
python.stderr.on('data',function(data) {
|
||||
console.log(data.toString('utf8'));
|
||||
});
|
||||
// Create the browser window.
|
||||
win = new BrowserWindow({
|
||||
width: 800,
|
||||
height: 600,
|
||||
webPreferences: {
|
||||
// Use pluginOptions.nodeIntegration, leave this alone
|
||||
// See nklayman.github.io/vue-cli-plugin-electron-builder/guide/security.html#node-integration for more info
|
||||
nodeIntegration: process.env.ELECTRON_NODE_INTEGRATION
|
||||
}
|
||||
})
|
||||
win.maximize();
|
||||
win.setMenu(null);
|
||||
|
||||
if (process.env.WEBPACK_DEV_SERVER_URL) {
|
||||
// Load the url of the dev server if in development mode
|
||||
win.loadURL(process.env.WEBPACK_DEV_SERVER_URL)
|
||||
if (!process.env.IS_TEST) win.webContents.openDevTools()
|
||||
} else {
|
||||
createProtocol('app')
|
||||
// Load the index.html when not in development
|
||||
win.loadURL('app://./index.html')
|
||||
}
|
||||
|
||||
win.on('closed', () => {
|
||||
win = null
|
||||
})
|
||||
}
|
||||
|
||||
// Quit when all windows are closed.
|
||||
app.on('window-all-closed', () => {
|
||||
// kill flask server
|
||||
if (process.platform !== 'win32') {
|
||||
execSync('kill $(ps aux | grep \'[o]drive_server.py\' | awk \'{print $2}\')');
|
||||
console.log("killed flask server");
|
||||
}
|
||||
// On macOS 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', () => {
|
||||
// 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 (win === null) {
|
||||
createWindow()
|
||||
}
|
||||
})
|
||||
|
||||
// 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', async () => {
|
||||
if (isDevelopment && !process.env.IS_TEST) {
|
||||
// Install Vue Devtools
|
||||
try {
|
||||
await installExtension(VUEJS_DEVTOOLS)
|
||||
} catch (e) {
|
||||
console.error('Vue Devtools failed to install:', e.toString())
|
||||
}
|
||||
}
|
||||
createWindow()
|
||||
})
|
||||
|
||||
// Exit cleanly on request from parent process in development mode.
|
||||
if (isDevelopment) {
|
||||
if (process.platform === 'win32') {
|
||||
process.on('message', (data) => {
|
||||
if (data === 'graceful-exit') {
|
||||
app.quit()
|
||||
}
|
||||
})
|
||||
} else {
|
||||
process.on('SIGTERM', () => {
|
||||
app.quit()
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import io from 'socket.io-client';
|
||||
|
||||
let socket = undefined;
|
||||
let url = 'https://0.0.0.0:8080';
|
||||
|
||||
function initSocket(url) {
|
||||
socket = io(url);
|
||||
}
|
||||
|
||||
export function setUrl(path) {
|
||||
if (socket) {
|
||||
socket.close();
|
||||
socket = undefined;
|
||||
}
|
||||
url = path;
|
||||
initSocket(url);
|
||||
}
|
||||
|
||||
export function closeSocket() {
|
||||
socket.close();
|
||||
socket = undefined;
|
||||
}
|
||||
|
||||
export function addEventListener(event) {
|
||||
if (!socket) {
|
||||
initSocket(url);
|
||||
}
|
||||
socket.on(event.type, event.callback);
|
||||
}
|
||||
|
||||
export function sendEvent(event) {
|
||||
socket.emit(event.type, event.data);
|
||||
}
|
||||
@@ -0,0 +1,216 @@
|
||||
<template>
|
||||
<div class="axis" @click="showError = !showError;" v-bind:class="{ noError: !axisError, error: axisError}">
|
||||
{{ axis.name }}
|
||||
<div v-show="showError" class="error-popup card">
|
||||
axis: <span v-bind:class="{ noError: !axisError, error: axisError}">{{axisErrorMsg}}</span> <br>
|
||||
motor: <span v-bind:class="{ noError: !motorError, error: motorError}">{{motorErrorMsg}}</span> <br>
|
||||
encoder: <span v-bind:class="{ noError: !encoderError, error: encoderError}">{{encoderErrorMsg}}</span> <br>
|
||||
controller: <span v-bind:class="{ noError: !controllerError, error: controllerError}">{{controllerErrorMsg}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
const axisErrors = {
|
||||
0x00000000: "AXIS_ERROR_NONE",
|
||||
0x00000001: "AXIS_ERROR_INVALID_STATE",
|
||||
0x00000002: "AXIS_ERROR_DC_BUS_UNDER_VOLTAGE",
|
||||
0x00000004: "AXIS_ERROR_DC_BUS_OVER_VOLTAGE",
|
||||
0x00000008: "AXIS_ERROR_CURRENT_MEASUREMENT_TIMEOUT",
|
||||
0x00000010: "AXIS_ERROR_BRAKE_RESISTOR_DISARMED",
|
||||
0x00000020: "AXIS_ERROR_MOTOR_DISARMED",
|
||||
0x00000040: "AXIS_ERROR_MOTOR_FAILED",
|
||||
0x00000080: "AXIS_ERROR_SENSORLESS_ESTIMATOR_FAILED",
|
||||
0x00000100: "AXIS_ERROR_ENCODER_FAILED",
|
||||
0x00000200: "AXIS_ERROR_CONTROLLER_FAILED",
|
||||
0x00000400: "AXIS_ERROR_POS_CTRL_DURING_SENSORLESS",
|
||||
0x00000800: "AXIS_ERROR_WATCHDOG_TIMER_EXPIRED",
|
||||
0x00001000: "AXIS_ERROR_MIN_ENDSTOP_PRESSED",
|
||||
0x00002000: "AXIS_ERROR_MAX_ENDSTOP_PRESSED",
|
||||
0x00004000: "AXIS_ERROR_ESTOP_REQUESTED",
|
||||
0x00020000: "AXIS_ERROR_HOMING_WITHOUT_ENDSTOP",
|
||||
0x00040000: "AXIS_ERROR_OVER_TEMP",
|
||||
};
|
||||
|
||||
const motorErrors = {
|
||||
0x00000000: "MOTOR_ERROR_NONE",
|
||||
0x00000001: "MOTOR_ERROR_PHASE_RESISTANCE_OUT_OF_RANGE",
|
||||
0x00000002: "MOTOR_ERROR_PHASE_INDUCTANCE_OUT_OF_RANGE",
|
||||
0x00000004: "MOTOR_ERROR_ADC_FAILED",
|
||||
0x00000008: "MOTOR_ERROR_DRV_FAULT",
|
||||
0x00000010: "MOTOR_ERROR_CONTROL_DEADLINE_MISSED",
|
||||
0x00000020: "MOTOR_ERROR_NOT_IMPLEMENTED_MOTOR_TYPE",
|
||||
0x00000040: "MOTOR_ERROR_BRAKE_CURRENT_OUT_OF_RANGE",
|
||||
0x00000080: "MOTOR_ERROR_MODULATION_MAGNITUDE",
|
||||
0x00000100: "MOTOR_ERROR_BRAKE_DEADTIME_VIOLATION",
|
||||
0x00000200: "MOTOR_ERROR_UNEXPECTED_TIMER_CALLBACK",
|
||||
0x00000400: "MOTOR_ERROR_CURRENT_SENSE_SATURATION",
|
||||
0x00001000: "MOTOR_ERROR_CURRENT_LIMIT_VIOLATION",
|
||||
0x00002000: "MOTOR_ERROR_BRAKE_DUTY_CYCLE_NAN",
|
||||
0x00004000: "MOTOR_ERROR_DC_BUS_OVER_REGEN_CURRENT",
|
||||
0x00008000: "MOTOR_ERROR_DC_BUS_OVER_CURRENT",
|
||||
};
|
||||
|
||||
let encoderErrors = {
|
||||
0x00000000: "ENCODER_ERROR_NONE",
|
||||
0x00000001: "ENCODER_ERROR_UNSTABLE_GAIN",
|
||||
0x00000002: "ENCODER_ERROR_CPR_POLEPAIRS_MISMATCH",
|
||||
0x00000004: "ENCODER_ERROR_NO_RESPONSE",
|
||||
0x00000008: "ENCODER_ERROR_UNSUPPORTED_ENCODER_MODE",
|
||||
0x00000010: "ENCODER_ERROR_ILLEGAL_HALL_STATE",
|
||||
0x00000020: "ENCODER_ERROR_INDEX_NOT_FOUND_YET",
|
||||
0x00000040: "ENCODER_ERROR_ABS_SPI_TIMEOUT",
|
||||
0x00000080: "ENCODER_ERROR_ABS_SPI_COM_FAIL",
|
||||
0x00000100: "ENCODER_ERROR_ABS_SPI_NOT_READY",
|
||||
};
|
||||
|
||||
let controllerErrors = {
|
||||
0x00000000: "ENCODER_ERROR_NONE",
|
||||
0x00000001: "ENCODER_ERROR_UNSTABLE_GAIN",
|
||||
0x00000002: "ENCODER_ERROR_CPR_POLEPAIRS_MISMATCH",
|
||||
0x00000004: "ENCODER_ERROR_NO_RESPONSE",
|
||||
0x00000008: "ENCODER_ERROR_UNSUPPORTED_ENCODER_MODE",
|
||||
0x00000010: "ENCODER_ERROR_ILLEGAL_HALL_STATE",
|
||||
0x00000020: "ENCODER_ERROR_INDEX_NOT_FOUND_YET",
|
||||
0x00000040: "ENCODER_ERROR_ABS_SPI_TIMEOUT",
|
||||
0x00000080: "ENCODER_ERROR_ABS_SPI_COM_FAIL",
|
||||
0x00000100: "ENCODER_ERROR_ABS_SPI_NOT_READY",
|
||||
}
|
||||
|
||||
export default {
|
||||
name: "Axis",
|
||||
props: ["axis", "odrives"],
|
||||
data() {
|
||||
return {
|
||||
showError: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
axisErrorMsg() {
|
||||
let retMsg = "none";
|
||||
let errCode = parseInt(this.axis.ref.error.val);
|
||||
|
||||
if (errCode != 0) {
|
||||
// we got an error!
|
||||
let errs = []
|
||||
for (const errKey of Object.keys(axisErrors)) {
|
||||
if (errCode & errKey) {
|
||||
errs.push(axisErrors[errKey]);
|
||||
}
|
||||
}
|
||||
retMsg = "";
|
||||
for (const err of errs){
|
||||
retMsg = retMsg + " " + err;
|
||||
}
|
||||
}
|
||||
|
||||
return retMsg;
|
||||
},
|
||||
motorErrorMsg() {
|
||||
let retMsg = "none";
|
||||
let errCode = parseInt(this.axis.ref.motor.error.val);
|
||||
|
||||
if (errCode != 0) {
|
||||
// we got an error!
|
||||
let errs = []
|
||||
for (const errKey of Object.keys(motorErrors)) {
|
||||
if (errCode & errKey) {
|
||||
errs.push(motorErrors[errKey]);
|
||||
}
|
||||
}
|
||||
retMsg = "";
|
||||
for (const err of errs){
|
||||
retMsg = retMsg + " " + err;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return retMsg;
|
||||
},
|
||||
encoderErrorMsg() {
|
||||
let retMsg = "none";
|
||||
let errCode = parseInt(this.axis.ref.encoder.error.val);
|
||||
|
||||
if (errCode != 0) {
|
||||
// we got an error!
|
||||
let errs = []
|
||||
for (const errKey of Object.keys(encoderErrors)) {
|
||||
if (errCode & errKey) {
|
||||
errs.push(encoderErrors[errKey]);
|
||||
}
|
||||
}
|
||||
retMsg = "";
|
||||
for (const err of errs){
|
||||
retMsg = retMsg + " " + err;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return retMsg;
|
||||
},
|
||||
controllerErrorMsg() {
|
||||
let retMsg = "none";
|
||||
let errCode = parseInt(this.axis.ref.controller.error.val);
|
||||
|
||||
if (errCode != 0) {
|
||||
// we got an error!
|
||||
let errs = []
|
||||
for (const errKey of Object.keys(controllerErrors)) {
|
||||
if (errCode & errKey) {
|
||||
errs.push(controllerErrors[errKey]);
|
||||
}
|
||||
}
|
||||
retMsg = "";
|
||||
for (const err of errs){
|
||||
retMsg = retMsg + " " + err;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return retMsg;
|
||||
},
|
||||
axisError() {
|
||||
return this.axis.ref.error.val !== "0";
|
||||
},
|
||||
motorError() {
|
||||
return this.axis.ref.motor.error.val !== "0";
|
||||
},
|
||||
encoderError() {
|
||||
return this.axis.ref.encoder.error.val !== "0";
|
||||
},
|
||||
controllerError() {
|
||||
return this.axis.ref.controller.error.val !== "0";
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.axis{
|
||||
padding: 5px 10px;
|
||||
border: 2px black;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.axis:active {
|
||||
background-color: var(--bg-color);
|
||||
}
|
||||
|
||||
.noError {
|
||||
color: green;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: red;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.error-popup {
|
||||
position: absolute;
|
||||
bottom: 2rem;
|
||||
color: black;
|
||||
margin-left: 0px;
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,104 @@
|
||||
<template>
|
||||
<div class="card action-card">
|
||||
<button class="close-button" @click=deleteAction>X</button>
|
||||
<span class="ctrlName">{{path}}:</span>
|
||||
<div class="right">
|
||||
<input type="number" v-on:change="newVal" :placeholder="initVal"/>
|
||||
<button class="action-button close-button" @click="putVal">Go</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
const axios = require("axios");
|
||||
|
||||
export default {
|
||||
name: "Action",
|
||||
props: {
|
||||
id: String,
|
||||
path: String,
|
||||
initVal: Number,
|
||||
dashID: String
|
||||
},
|
||||
data: function () {
|
||||
return {
|
||||
value: 0,
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
if (this.initVal !== undefined) {
|
||||
this.value = this.initVal;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
newVal: function (e) {
|
||||
this.value = parseFloat(e.target.value);
|
||||
// emit a signal - signal needs to float all the way up to Dash
|
||||
// set value of dash.actions.id.val to this.value
|
||||
// this way, action values will be kept when dashes are imported or exported
|
||||
//this.$emit('set-action-val', {id: this.id, val: this.value});
|
||||
this.$store.commit("setActionVal", {dashID: this.dashID, actionID: this.id, val: this.value});
|
||||
},
|
||||
putVal: function () {
|
||||
var params = new URLSearchParams();
|
||||
let keys = this.path.split(".");
|
||||
keys.shift();
|
||||
for (const key of keys) {
|
||||
params.append("key", key);
|
||||
}
|
||||
params.append("val", this.value);
|
||||
params.append("type", "numeric");
|
||||
console.log(params.toString());
|
||||
let request = {
|
||||
params: params,
|
||||
};
|
||||
console.log(request);
|
||||
axios.put(
|
||||
this.$store.state.odriveServerAddress + "/api/property",
|
||||
null,
|
||||
request
|
||||
);
|
||||
},
|
||||
deleteAction: function() {
|
||||
// commit a mutation to remove this action from the dashboard
|
||||
this.$store.commit("removeActionFromDash", {dashID: this.dashID, actionID: this.id});
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
input {
|
||||
border-style: none;
|
||||
border-bottom: 1px solid grey;
|
||||
width: 5rem;
|
||||
margin-left: 0.5rem;
|
||||
margin-right: 0.5rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.action-card {
|
||||
display: flex;
|
||||
/* border: 1px solid lightcoral; */
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.action-button {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.right {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
input::-webkit-outer-spin-button,
|
||||
input::-webkit-inner-spin-button {
|
||||
/* display: none; <- Crashes Chrome on hover */
|
||||
-webkit-appearance: none;
|
||||
margin: 0; /* <-- Apparently some margin are still there even though it's hidden */
|
||||
}
|
||||
|
||||
input[type=number] {
|
||||
-moz-appearance:textfield; /* Firefox */
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,101 @@
|
||||
<template>
|
||||
<div class="card">
|
||||
<button class="close-button" @click=deleteCtrl>X</button>
|
||||
<span class="ctrlName">{{name}}:</span>
|
||||
<div class="right">
|
||||
<span class="ctrlVal">{{value}}</span>
|
||||
<input
|
||||
class="ctrlInput"
|
||||
v-if="writeAccess"
|
||||
type="checkbox"
|
||||
v-bind:value="value"
|
||||
@click="putVal"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
const axios = require("axios");
|
||||
|
||||
export default {
|
||||
name: "CtrlBoolean",
|
||||
//type checking here for properties
|
||||
props: {
|
||||
path: String,
|
||||
odrives: Object,
|
||||
dashID: String,
|
||||
},
|
||||
computed: {
|
||||
value: function () {
|
||||
let keys = this.path.split(".");
|
||||
keys.shift(); // don't need first key here
|
||||
let odriveObj = this.odrives;
|
||||
for (const key of keys) {
|
||||
odriveObj = odriveObj[key];
|
||||
}
|
||||
return odriveObj["val"];
|
||||
},
|
||||
name: function () {
|
||||
let keys = this.path.split(".");
|
||||
keys.shift();
|
||||
return keys.join(".");
|
||||
},
|
||||
writeAccess: function () {
|
||||
let keys = this.path.split(".");
|
||||
keys.shift(); // don't need first key here
|
||||
let odriveObj = this.odrives;
|
||||
for (const key of keys) {
|
||||
odriveObj = odriveObj[key];
|
||||
}
|
||||
return odriveObj["readonly"] == false;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
putVal: function (e) {
|
||||
console.log(e.target.checked);
|
||||
var params = new URLSearchParams();
|
||||
let keys = this.path.split(".");
|
||||
keys.shift();
|
||||
for (const key of keys) {
|
||||
params.append("key", key);
|
||||
}
|
||||
params.append("val", e.target.checked);
|
||||
params.append("type", "boolean");
|
||||
console.log(params.toString());
|
||||
let request = {
|
||||
params: params,
|
||||
};
|
||||
axios.put(
|
||||
this.$store.state.odriveServerAddress + "/api/property",
|
||||
null,
|
||||
request
|
||||
);
|
||||
},
|
||||
deleteCtrl: function() {
|
||||
// commit a mutation in the store with the relevant information
|
||||
this.$store.commit("removeCtrlFromDash", {dashID: this.dashID, path: this.path});
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.ctrlVal {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.ctrlInput {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.right {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.card {
|
||||
display: flex;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,89 @@
|
||||
<template>
|
||||
<div class="card" @click.self="executeFunction">
|
||||
<button class="close-button" @click=deleteCtrl>X</button>
|
||||
<button class="execute">{{name}}()</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
const axios = require("axios");
|
||||
|
||||
export default {
|
||||
name: "CtrlFunction",
|
||||
//type checking here for properties
|
||||
props: {
|
||||
path: String,
|
||||
odrives: Object,
|
||||
dashID: String,
|
||||
},
|
||||
computed: {
|
||||
name: function() {
|
||||
let keys = this.path.split(".");
|
||||
keys.shift();
|
||||
return keys.join(".");
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
putVal: function(e) {
|
||||
var params = new URLSearchParams();
|
||||
let keys = this.path.split(".");
|
||||
keys.shift();
|
||||
for (const key of keys) {
|
||||
params.append("key", key);
|
||||
}
|
||||
params.append("val", e.target.value);
|
||||
console.log(params.toString());
|
||||
let request = {
|
||||
params: params
|
||||
};
|
||||
console.log(request);
|
||||
axios.put(
|
||||
this.$store.state.odriveServerAddress + "/api/property",
|
||||
null,
|
||||
request
|
||||
);
|
||||
},
|
||||
executeFunction: function(e) {
|
||||
//execute this function on the odrive
|
||||
console.log(e);
|
||||
var params = new URLSearchParams();
|
||||
let keys = this.path.split(".");
|
||||
keys.shift();
|
||||
for (const key of keys) {
|
||||
params.append("key", key);
|
||||
}
|
||||
console.log(params.toString());
|
||||
let request = {
|
||||
params: params
|
||||
};
|
||||
axios.put(
|
||||
this.$store.state.odriveServerAddress + "/api/function",
|
||||
null,
|
||||
request
|
||||
);
|
||||
},
|
||||
deleteCtrl: function() {
|
||||
// commit a mutation in the store with the relevant information
|
||||
this.$store.commit("removeCtrlFromDash", {dashID: this.dashID, path: this.path});
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.card {
|
||||
background-color: lightcyan;
|
||||
}
|
||||
|
||||
.execute {
|
||||
color: black;
|
||||
font-family: "Roboto Mono", monospace;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background-color: rgba(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
.card:active {
|
||||
background-color: lightblue;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,110 @@
|
||||
<template>
|
||||
<div class="card">
|
||||
<button class="close-button" @click=deleteCtrl>X</button>
|
||||
<span class="ctrlName">{{name}}:</span>
|
||||
<div class="right">
|
||||
<span class="ctrlVal">{{value}}</span>
|
||||
<input v-if="writeAccess" type="number" v-on:change="putVal" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
const axios = require("axios");
|
||||
|
||||
export default {
|
||||
name: "CtrlNumeric",
|
||||
//type checking here for properties
|
||||
props: {
|
||||
path: String,
|
||||
odrives: Object,
|
||||
dashID: String
|
||||
},
|
||||
computed: {
|
||||
value: function () {
|
||||
let keys = this.path.split(".");
|
||||
keys.shift(); // don't need first key here
|
||||
let odriveObj = this.$store.state.odrives;
|
||||
for (const key of keys) {
|
||||
odriveObj = odriveObj[key];
|
||||
}
|
||||
return parseFloat(odriveObj["val"]).toFixed(3);
|
||||
},
|
||||
name: function () {
|
||||
let keys = this.path.split(".");
|
||||
keys.shift();
|
||||
return keys.join(".");
|
||||
},
|
||||
writeAccess: function () {
|
||||
let keys = this.path.split(".");
|
||||
keys.shift(); // don't need first key here
|
||||
let odriveObj = this.odrives;
|
||||
for (const key of keys) {
|
||||
odriveObj = odriveObj[key];
|
||||
}
|
||||
return odriveObj["readonly"] === false;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
putVal: function (e) {
|
||||
var params = new URLSearchParams();
|
||||
let keys = this.path.split(".");
|
||||
keys.shift();
|
||||
for (const key of keys) {
|
||||
params.append("key", key);
|
||||
}
|
||||
params.append("val", e.target.value);
|
||||
params.append("type", "numeric");
|
||||
console.log(params.toString());
|
||||
let request = {
|
||||
params: params,
|
||||
};
|
||||
console.log(request);
|
||||
axios.put(
|
||||
this.$store.state.odriveServerAddress + "/api/property",
|
||||
null,
|
||||
request
|
||||
);
|
||||
},
|
||||
deleteCtrl: function() {
|
||||
// commit a mutation in the store with the relevant information
|
||||
this.$store.commit("removeCtrlFromDash", {dashID: this.dashID, path: this.path});
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.ctrlVal {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
input {
|
||||
width: 5rem;
|
||||
font-family: inherit;
|
||||
border-style: none;
|
||||
border-bottom: 1px solid grey;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.right {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.card {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
input::-webkit-outer-spin-button,
|
||||
input::-webkit-inner-spin-button {
|
||||
/* display: none; <- Crashes Chrome on hover */
|
||||
-webkit-appearance: none;
|
||||
margin: 0; /* <-- Apparently some margin are still there even though it's hidden */
|
||||
}
|
||||
|
||||
input[type=number] {
|
||||
-moz-appearance:textfield; /* Firefox */
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,153 @@
|
||||
<template>
|
||||
<div class="card">
|
||||
<div>
|
||||
<button class="close-button" @click=deleteCtrl>X</button>
|
||||
<span class="ctrlName">{{name}}</span>
|
||||
</div>
|
||||
<div class="slider-container">
|
||||
<input type="number" :value="min" v-on:change="setMin"/>
|
||||
<!-- <vue-slider v-model="value" :min="min" :max="max" :interval="interval" /> -->
|
||||
<vue-slider v-model="value" :data="data" @change="putVal"/>
|
||||
<input type="number" :value="max" v-on:change="setMax" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import VueSlider from "vue-slider-component";
|
||||
import "vue-slider-component/theme/default.css";
|
||||
const axios = require("axios");
|
||||
|
||||
export default {
|
||||
name: "CtrlSlider",
|
||||
components: {
|
||||
VueSlider,
|
||||
},
|
||||
//type checking here for properties
|
||||
props: {
|
||||
path: String,
|
||||
odrives: Object,
|
||||
dashID: String,
|
||||
},
|
||||
data: function () {
|
||||
return {
|
||||
value: 0,
|
||||
min: 0,
|
||||
max: 1,
|
||||
data: []
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
name: function () {
|
||||
let keys = this.path.split(".");
|
||||
keys.shift();
|
||||
return keys.join(".");
|
||||
},
|
||||
writeAccess: function () {
|
||||
let keys = this.path.split(".");
|
||||
keys.shift(); // don't need first key here
|
||||
let odriveObj = this.odrives;
|
||||
for (const key of keys) {
|
||||
odriveObj = odriveObj[key];
|
||||
}
|
||||
return odriveObj["readonly"] === false;
|
||||
},
|
||||
interval: function () {
|
||||
return (this.max - this.min) / 100;
|
||||
},
|
||||
sliderData: function () {
|
||||
let interval = (this.max - this.min) / 100;
|
||||
return Array.from(Array(101), (_, i) => this.min + interval * i);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
putVal: function (value, index) {
|
||||
console.log(value);
|
||||
console.log(index);
|
||||
var params = new URLSearchParams();
|
||||
let keys = this.path.split(".");
|
||||
keys.shift();
|
||||
for (const key of keys) {
|
||||
params.append("key", key);
|
||||
}
|
||||
params.append("val", value);
|
||||
params.append("type", "numeric");
|
||||
console.log(params.toString());
|
||||
let request = {
|
||||
params: params,
|
||||
};
|
||||
console.log(request);
|
||||
axios.put(
|
||||
this.$store.state.odriveServerAddress + "/api/property",
|
||||
null,
|
||||
request
|
||||
);
|
||||
},
|
||||
setMin: function (e) {
|
||||
this.min = parseFloat(e.target.value);
|
||||
this.data = Array.from(Array(101), (_, i) => this.min + (this.max-this.min) / 100 * i);
|
||||
},
|
||||
setMax: function (e) {
|
||||
this.max = parseFloat(e.target.value);
|
||||
this.data = Array.from(Array(101), (_, i) => this.min + (this.max-this.min) / 100 * i);
|
||||
},
|
||||
deleteCtrl: function() {
|
||||
// commit a mutation in the store with the relevant information
|
||||
this.$store.commit("removeCtrlFromDash", {dashID: this.dashID, path: this.path});
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
let initVal = () => {
|
||||
let keys = this.path.split(".");
|
||||
keys.shift(); // don't need first key here
|
||||
let odriveObj = this.$store.state.odrives;
|
||||
for (const key of keys) {
|
||||
odriveObj = odriveObj[key];
|
||||
}
|
||||
return parseFloat(odriveObj["val"]);
|
||||
};
|
||||
this.value = initVal();
|
||||
this.max = this.value * 4;
|
||||
this.min = this.value / 4;
|
||||
this.data = Array.from(Array(101), (_, i) => this.min + (this.max-this.min) / 100 * i);
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.ctrlVal {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.slider-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin-top: 0.4rem;
|
||||
}
|
||||
|
||||
.vue-slider {
|
||||
flex-grow: 3;
|
||||
margin-left: 0.4rem;
|
||||
margin-right: 0.4rem;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
input {
|
||||
width: 5rem;
|
||||
font-family: inherit;
|
||||
border-style: none;
|
||||
border-bottom: 1px solid grey;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
input::-webkit-outer-spin-button,
|
||||
input::-webkit-inner-spin-button {
|
||||
/* display: none; <- Crashes Chrome on hover */
|
||||
-webkit-appearance: none;
|
||||
margin: 0; /* <-- Apparently some margin are still there even though it's hidden */
|
||||
}
|
||||
|
||||
input[type=number] {
|
||||
-moz-appearance:textfield; /* Firefox */
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,14 @@
|
||||
import { Line, mixins } from 'vue-chartjs';
|
||||
import 'chartjs-plugin-streaming';
|
||||
const { reactiveProp } = mixins;
|
||||
|
||||
export default {
|
||||
extends: Line,
|
||||
mixins: [reactiveProp],
|
||||
props: ['options'],
|
||||
mounted () {
|
||||
// this.chartData is created in the mixin.
|
||||
// If you want to pass options please create a local options object
|
||||
this.renderChart(this.chartData, this.options)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
<template>
|
||||
<div class="card plot">
|
||||
<div class="plot-header">
|
||||
<button class="close-button" @click="deletePlot">X</button>
|
||||
<button class="close-button" @click="exportCSV">Export</button>
|
||||
<button class="close-button" @click="$emit('add-var', name)">+</button>
|
||||
</div>
|
||||
<line-chart v-if="loaded" :chart-data="datacollection" :options="dataOptions"></line-chart>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import LineChart from "./LineChart.js";
|
||||
import { saveAs } from "file-saver";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
LineChart,
|
||||
},
|
||||
props: ["name", "plot", "dashID"],
|
||||
data() {
|
||||
return {
|
||||
datacollection: null,
|
||||
timeStart: null,
|
||||
loaded: false,
|
||||
dataOptions: {
|
||||
animation: {
|
||||
duration: 0, // general animation time
|
||||
},
|
||||
hover: {
|
||||
animationDuration: 0, // duration of animations when hovering an item
|
||||
},
|
||||
responsiveAnimationDuration: 0, // animation duration after a resize
|
||||
elements: {
|
||||
point: {
|
||||
radius: 0,
|
||||
},
|
||||
line: {
|
||||
// borderColor: "rbga(0,0,0,0)",
|
||||
fill: false,
|
||||
borderWidth: 0,
|
||||
tension: 0,
|
||||
},
|
||||
},
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
},
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.timeStart = Date.now();
|
||||
//this.initData(); //set label for dataset and color
|
||||
this.fillData();
|
||||
this.liveData();
|
||||
},
|
||||
methods: {
|
||||
fillData() {
|
||||
let newData = {
|
||||
labels: this.$store.state.propSamples["time"],
|
||||
datasets: [],
|
||||
};
|
||||
for (const plotVar of this.plot.vars) {
|
||||
let newPath = plotVar.path.split(".");
|
||||
newPath.splice(0, 1);
|
||||
newPath = newPath.join(".");
|
||||
newData.datasets.push({
|
||||
label: newPath,
|
||||
borderColor: plotVar.color, //"rgba(0,0,0,0)",
|
||||
data: this.$store.state.propSamples[newPath],
|
||||
});
|
||||
}
|
||||
this.datacollection = newData;
|
||||
this.loaded = true;
|
||||
},
|
||||
initData() {
|
||||
this.datacollection = {
|
||||
labels: [0],
|
||||
datasets: [
|
||||
{
|
||||
label: "Sine wave",
|
||||
backgroundColor: "rgba(0,0,0,0)", //"#f87979",
|
||||
data: [0],
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
liveData() {
|
||||
setTimeout(() => {
|
||||
this.liveData();
|
||||
}, 50);
|
||||
//if (this.$store.state.sampling == true) {
|
||||
// this.fillData();
|
||||
//}
|
||||
this.fillData();
|
||||
},
|
||||
deletePlot: function () {
|
||||
// commit a mutation in the store with the relevant information
|
||||
this.$store.commit("removePlotFromDash", {
|
||||
dashID: this.dashID,
|
||||
plotID: this.name,
|
||||
});
|
||||
},
|
||||
exportCSV: function () {
|
||||
// make a sensible data structure
|
||||
let csvData = {};
|
||||
csvData["time"] = this.$store.state.propSamples["time"];
|
||||
for (const dataset of this.datacollection.datasets) {
|
||||
csvData[dataset.label] = dataset.data;
|
||||
}
|
||||
let csvString = "";
|
||||
let dataKeys = Object.keys(csvData);
|
||||
// turn data structure into a string that's suitable for exporting
|
||||
for (const label of dataKeys) {
|
||||
csvString += label;
|
||||
if (label != dataKeys.slice(-1)) {
|
||||
csvString += ",";
|
||||
} else {
|
||||
csvString += "\n";
|
||||
}
|
||||
}
|
||||
for (let idx = 0; idx < csvData["time"].length; idx = idx + 1) {
|
||||
for (const label of dataKeys) {
|
||||
csvString += csvData[label][idx];
|
||||
if (label != dataKeys.slice(-1)) {
|
||||
csvString += ",";
|
||||
} else {
|
||||
csvString += "\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
var blob = new Blob([csvString], { type: "text/plain;charset=utf-8" });
|
||||
saveAs(blob, "plot.csv");
|
||||
|
||||
console.log("exporting plot");
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
div {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.plot {
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.plot-header {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.plotname {
|
||||
flex-grow: 10;
|
||||
margin: auto 0;
|
||||
}
|
||||
|
||||
.delete {
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
padding: 0 5px;
|
||||
margin-right: 10px;
|
||||
border: 1px solid black;
|
||||
}
|
||||
|
||||
.add-var {
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
padding: 0 5px;
|
||||
margin-right: 10px;
|
||||
border: 1px solid black;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,57 @@
|
||||
<template>
|
||||
<div class="wizardAxis card">
|
||||
<div class="title">Which axis are you configuring?</div>
|
||||
<div class="choices">
|
||||
<div class="choice card">
|
||||
M0
|
||||
</div>
|
||||
<div class="choice card">
|
||||
M1
|
||||
</div>
|
||||
<div class="choice card">
|
||||
M0 and M1
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "wizardAxis"
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
.wizardAxis {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.choices {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.choice {
|
||||
text-align: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin: 2rem;
|
||||
}
|
||||
|
||||
.chosen {
|
||||
border: 2px solid black;
|
||||
}
|
||||
|
||||
.unchosen {
|
||||
border: 2px solid transparent;
|
||||
}
|
||||
|
||||
img {
|
||||
padding: 0px;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,64 @@
|
||||
<template>
|
||||
<div class="wizardEncoder card">
|
||||
<div class="title">What encoder are you using?</div>
|
||||
<div class="choices">
|
||||
<div class="choice card">
|
||||
CUI AMT102-V
|
||||
</div>
|
||||
<div class="choice card">
|
||||
Incremental
|
||||
</div>
|
||||
<div class="choice card">
|
||||
Incremental with index
|
||||
</div>
|
||||
<div class="choice card">
|
||||
Absolute
|
||||
</div>
|
||||
<div class="choice card">
|
||||
Hall Effect
|
||||
</div>
|
||||
<div class="choice card">
|
||||
Sin/Cos
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "wizardEncoder",
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.wizardEncoder {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.choices {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.choice {
|
||||
text-align: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin: 2rem;
|
||||
}
|
||||
|
||||
.chosen {
|
||||
border: 2px solid black;
|
||||
}
|
||||
|
||||
.unchosen {
|
||||
border: 2px solid transparent;
|
||||
}
|
||||
|
||||
img {
|
||||
padding: 0px;
|
||||
margin: auto;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,13 @@
|
||||
<template>
|
||||
<div class="wizardLimits card">What are your limits?</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "wizardLimits",
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,56 @@
|
||||
<template>
|
||||
<div class="wizardMotor card">
|
||||
<div class="title">What motor are you using?</div>
|
||||
<div class="choices">
|
||||
<div class="choice card">
|
||||
ODrive D5065
|
||||
</div>
|
||||
<div class="choice card">
|
||||
ODrive D6374
|
||||
</div>
|
||||
<div class="choice card">
|
||||
Other
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "wizardMotor",
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.wizardMotor {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.choices {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.choice {
|
||||
text-align: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin: 2rem;
|
||||
}
|
||||
|
||||
.chosen {
|
||||
border: 2px solid black;
|
||||
}
|
||||
|
||||
.unchosen {
|
||||
border: 2px solid transparent;
|
||||
}
|
||||
|
||||
img {
|
||||
padding: 0px;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,80 @@
|
||||
<template>
|
||||
<div class="wizardODrive card">
|
||||
<div class="title">What ODrive do you have?</div>
|
||||
<div class="choices">
|
||||
<div class="choice card" v-bind:class="{ unchosen: !select_24V, chosen: select_24V}" @click="select('24V')">
|
||||
<img alt="Odrive 24V" src="../../assets/images/24v_200_200.png" />
|
||||
ODrive v3.6 24V
|
||||
</div>
|
||||
|
||||
<div class="choice card" v-bind:class="{ unchosen: !select_56V, chosen: select_56V}" @click="select('56V')">
|
||||
<img alt="Odrive 56V" src="../../assets/images/56v_200_200.png" />
|
||||
ODrive v3.6 56V
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "wizardODrive",
|
||||
data: function() {
|
||||
return {
|
||||
select_24V: false,
|
||||
select_56V: false,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
select(choice) {
|
||||
this.select_24V = false;
|
||||
this.select_56V = false;
|
||||
if (choice == '24V') {
|
||||
this.select_24V = true;
|
||||
this.$emit('choice-odrive-24v');
|
||||
}
|
||||
if (choice == '56V') {
|
||||
this.$emit('choice-odrive-56v');
|
||||
this.select_56V = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.wizardODrive {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.choices {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.choice {
|
||||
text-align: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin: 2rem;
|
||||
}
|
||||
|
||||
.chosen {
|
||||
border: 2px solid black;
|
||||
}
|
||||
|
||||
.unchosen {
|
||||
border: 2px solid transparent;
|
||||
}
|
||||
|
||||
img {
|
||||
padding: 0px;
|
||||
margin: auto;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,10 @@
|
||||
import Vue from 'vue'
|
||||
import App from './App.vue'
|
||||
import store from './store';
|
||||
|
||||
Vue.config.productionTip = false
|
||||
|
||||
new Vue({
|
||||
store,
|
||||
render: h => h(App)
|
||||
}).$mount('#app')
|
||||
@@ -0,0 +1,266 @@
|
||||
import Vue from 'vue';
|
||||
import Vuex from 'vuex';
|
||||
const axios = require('axios');
|
||||
import * as socketio from "./comms/socketio";
|
||||
//import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
Vue.use(Vuex);
|
||||
|
||||
export default new Vuex.Store({
|
||||
// state is the data for this app
|
||||
state: {
|
||||
odrives: Object,
|
||||
odriveConfigs: Object,
|
||||
axes: Array,
|
||||
odriveServerAddress: String,
|
||||
serverConnected: Boolean,
|
||||
dashboards: [
|
||||
{
|
||||
name: "Start",
|
||||
component: "Start",
|
||||
},
|
||||
{
|
||||
name: "Wizard",
|
||||
component: "Wizard",
|
||||
},
|
||||
//{ name: "Config", id: uuidv4(), component: "Dashboard", controls: [], actions: [], plots: [] }
|
||||
],
|
||||
timeSampleStart: 0,
|
||||
sampledProperties: [], // make this an object where the full path is a key and the value is the sampled var
|
||||
propSamples: { time: [] }, // {time: [time values], ...path: [path var values]}
|
||||
newData: false,
|
||||
sampling: false,
|
||||
},
|
||||
// mutations are functions that change the data
|
||||
mutations: {
|
||||
setOdrives(state, odrives) {
|
||||
state.odrives = odrives;
|
||||
},
|
||||
setOdriveConfigs(state, odriveConfigs) {
|
||||
state.odriveConfigs = odriveConfigs;
|
||||
},
|
||||
setAxes(state, axes) {
|
||||
state.axes = axes;
|
||||
},
|
||||
setServerAddress(state, address) {
|
||||
state.odriveServerAddress = address;
|
||||
},
|
||||
updateOdriveProp(state, payload) {
|
||||
// need to use Vue.set!!!
|
||||
// payload is {path, value}
|
||||
//
|
||||
const createNestedObject = (odrive, path) => {
|
||||
let ref = odrive;
|
||||
let keys = path.split('.');
|
||||
for (const key of keys) {
|
||||
ref = ref[key];
|
||||
}
|
||||
return ref;
|
||||
};
|
||||
Vue.set(createNestedObject(state.odrives, payload.path), "val", payload.value);
|
||||
|
||||
},
|
||||
addSampledProperty(state, path) {
|
||||
if (!(path in state.sampledProperties)) {
|
||||
let newPath = path.split('.');
|
||||
newPath.splice(0, 1);
|
||||
state.sampledProperties.push(newPath.join('.'));
|
||||
state.propSamples[newPath.join('.')] = [];
|
||||
console.log(state.propSamples);
|
||||
}
|
||||
for (const path of state.sampledProperties) {
|
||||
console.log(path);
|
||||
}
|
||||
socketio.sendEvent({
|
||||
type: 'sampledVarNames',
|
||||
data: {
|
||||
paths: state.sampledProperties
|
||||
}
|
||||
});
|
||||
},
|
||||
removeSampledProperty(state, path) {
|
||||
let newPath = path.split('.');
|
||||
newPath.splice(0, 1);
|
||||
const index = state.sampledProperties.indexOf(newPath.join('.'));
|
||||
if (index > -1) {
|
||||
state.sampledProperties.splice(index, 1);
|
||||
}
|
||||
},
|
||||
updateSampledProperty(state, payload) {
|
||||
// payload is object of paths and values
|
||||
for (const path of Object.keys(payload)) {
|
||||
state.propSamples[path].push(payload[path]);
|
||||
if (state.propSamples[path].length > 250) {
|
||||
state.propSamples[path].splice(0, 1); // emulate circular buffer
|
||||
}
|
||||
}
|
||||
state.propSamples["time"].push((Date.now() - state.timeSampleStart) / 1000);
|
||||
if (state.propSamples["time"].length > 250) {
|
||||
state.propSamples["time"].splice(0, 1);
|
||||
}
|
||||
state.newData = true;
|
||||
},
|
||||
setServerStatus(state, val) {
|
||||
state.serverConnected = val;
|
||||
},
|
||||
removeCtrlFromDash(state, obj) {
|
||||
// obj is {dash: dashID, path: control path}
|
||||
for (const dash of state.dashboards) {
|
||||
if (obj.dashID == dash.id) {
|
||||
for (const control of dash.controls) {
|
||||
if (obj.path == control.path) {
|
||||
dash.controls.splice(dash.controls.indexOf(control), 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
removeActionFromDash(state, obj) {
|
||||
// obj is {dashID: dashID, actionID: action ID}
|
||||
for (const dash of state.dashboards) {
|
||||
if (obj.dashID == dash.id) {
|
||||
for (const action of dash.actions) {
|
||||
if (obj.actionID == action.id) {
|
||||
dash.actions.splice(dash.actions.indexOf(action), 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
removePlotFromDash(state, obj) {
|
||||
// obj is {dashID: dash ID, plotID: plot ID}
|
||||
for (const dash of state.dashboards) {
|
||||
if (obj.dashID == dash.id) {
|
||||
for (const plot of dash.plots) {
|
||||
if (obj.plotID == plot.name) {
|
||||
dash.plots.splice(dash.plots.indexOf(plot), 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
setActionVal(state, obj) {
|
||||
// obj is {dashID: dash ID, actionID: action ID, val: val}
|
||||
for (const dash of state.dashboards) {
|
||||
if (obj.dashID == dash.id) {
|
||||
for (const action of dash.actions) {
|
||||
if (obj.actionID == action.id) {
|
||||
action.val = obj.val;
|
||||
console.log("Setting action val to " + obj.val);
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
// actions trigger mutations
|
||||
actions: {
|
||||
getOdrives(context) {
|
||||
// grab ODrive JSON from odrive_server
|
||||
axios.get(context.state.odriveServerAddress + '/api/odrives').then((response) => {
|
||||
context.commit('setOdrives', JSON.parse(JSON.stringify(response.data)));
|
||||
context.dispatch('getOdriveConfigs');
|
||||
context.dispatch('getAxes');
|
||||
});
|
||||
},
|
||||
getOdriveConfigs(context) {
|
||||
// transform ODrive JSON
|
||||
function treeParse(odriveObj) {
|
||||
let retObj = {};
|
||||
for (const key of Object.keys(odriveObj)) {
|
||||
if (typeof odriveObj[key] === 'object' && odriveObj[key] !== null) {
|
||||
// check if "val" is a valid key
|
||||
if (Object.prototype.hasOwnProperty.call(odriveObj[key], "val")) {
|
||||
// parse from string to a type that we care about
|
||||
switch (odriveObj[key]["type"]) {
|
||||
case "float":
|
||||
retObj[key] = parseFloat(parseFloat(odriveObj[key]["val"]).toFixed(3));
|
||||
break;
|
||||
case "int":
|
||||
retObj[key] = parseInt(odriveObj[key]["val"]);
|
||||
break;
|
||||
case "bool":
|
||||
retObj[key] = odriveObj[key]["val"] == 'True';
|
||||
break;
|
||||
default:
|
||||
retObj[key] = odriveObj[key]["val"];
|
||||
}
|
||||
}
|
||||
else {
|
||||
retObj[key] = treeParse(odriveObj[key]);
|
||||
}
|
||||
}
|
||||
else if (odriveObj[key] == "function") {
|
||||
retObj[key] = "function";
|
||||
}
|
||||
else {
|
||||
retObj[key] = odriveObj[key];
|
||||
}
|
||||
}
|
||||
return retObj;
|
||||
}
|
||||
context.commit('setOdriveConfigs', treeParse(context.state.odrives));
|
||||
},
|
||||
getAxes(context) {
|
||||
let axes = [];
|
||||
//for each connected odrive, collect axes and display them
|
||||
for (const odrive of Object.keys(context.state.odrives)) {
|
||||
if ('axis0' in context.state.odrives[odrive]) {
|
||||
axes.push({
|
||||
name: `${odrive}.axis0`,
|
||||
ref: context.state.odrives[odrive]['axis0']
|
||||
});
|
||||
}
|
||||
if ('axis1' in context.state.odrives[odrive]) {
|
||||
axes.push({
|
||||
name: `${odrive}.axis1`,
|
||||
ref: context.state.odrives[odrive]['axis1']
|
||||
});
|
||||
}
|
||||
}
|
||||
context.commit('setAxes', axes);
|
||||
},
|
||||
setServerAddress(context, address) {
|
||||
context.commit('setServerAddress', address);
|
||||
socketio.setUrl(address);
|
||||
socketio.addEventListener({
|
||||
type: "connect",
|
||||
callback: () => {
|
||||
context.commit("setServerStatus", true);
|
||||
console.log('connected to server');
|
||||
}
|
||||
});
|
||||
socketio.addEventListener({
|
||||
type: "disconnect",
|
||||
callback: () => {
|
||||
context.commit("setServerStatus", false);
|
||||
console.log('server disconnect');
|
||||
socketio.closeSocket();
|
||||
context.commit('setAxes', []);
|
||||
}
|
||||
});
|
||||
socketio.addEventListener({
|
||||
type: "sampledData",
|
||||
callback: message => {
|
||||
context.commit("updateSampledProperty", JSON.parse(message));
|
||||
}
|
||||
});
|
||||
socketio.addEventListener({
|
||||
type: "samplingEnabled",
|
||||
callback: () => {
|
||||
socketio.sendEvent({
|
||||
type: "startSampling"
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,276 @@
|
||||
<template>
|
||||
<div class="dashboard">
|
||||
<!-- PARAMETER DROPDOWN MENU -->
|
||||
<div v-show="paramsVisible" class="dropdown">
|
||||
<div class="card dropdown-content">
|
||||
<div>
|
||||
<button class="close-button" @click="hideTree">X</button>
|
||||
Parameters
|
||||
</div>
|
||||
<json-view
|
||||
v-bind:data="odriveConfigs"
|
||||
v-bind:rootKey="'odrives'"
|
||||
v-on:selected="addVarToElement"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dashboard_container">
|
||||
<div class="controls">
|
||||
<template v-for="(control, index) in dash.controls">
|
||||
<component
|
||||
:is="control.controlType"
|
||||
:key="index + '-control'"
|
||||
:path="control.path"
|
||||
:name="control.name"
|
||||
:odrives="odrives"
|
||||
:dashID="dash.id"
|
||||
/>
|
||||
</template>
|
||||
<div class="control-buttons">
|
||||
<div class="add-button card" @click="addComponent('control')">Add Control</div>
|
||||
<div class="add-button card" @click="addComponent('slider')">Add Slider</div>
|
||||
</div>
|
||||
<template v-for="(action, index) in dash.actions">
|
||||
<action
|
||||
:id="action.id"
|
||||
:key="index + '-action'"
|
||||
:path="action.path"
|
||||
:odrives="odrives"
|
||||
:initVal="action.val"
|
||||
:dashID="dash.id"
|
||||
/>
|
||||
</template>
|
||||
<div class="add-button card" @click="addComponent('action')">Add Action</div>
|
||||
</div>
|
||||
<div class="plots">
|
||||
<template v-for="(plot, index) in dash.plots">
|
||||
<plot
|
||||
:plot="plot"
|
||||
:key="index"
|
||||
:name="plot.name"
|
||||
:dashID="dash.id"
|
||||
v-on:add-var="currentPlot=plot.name;addComponent('plot')"
|
||||
/>
|
||||
</template>
|
||||
<div class="add-button card" @click="addPlot">Add Plot</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
//this component will get passed a list of controls and plots
|
||||
//display controls on the left and plots on the right?
|
||||
//leave full names for plots
|
||||
//collate controls into individual cards based on the deepest common level
|
||||
import CtrlBoolean from "../components/controls/CtrlBoolean.vue";
|
||||
import CtrlNumeric from "../components/controls/CtrlNumeric.vue";
|
||||
import CtrlFunction from "../components/controls/CtrlFunction.vue";
|
||||
import CtrlSlider from "../components/controls/CtrlSlider.vue";
|
||||
import Plot from "../components/plots/Plot.vue";
|
||||
import Action from "../components/actions/Action.vue";
|
||||
import { JSONView } from "vue-json-component";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
let plotColors = [
|
||||
"#195bd7", // blue
|
||||
"#d6941a", // orange
|
||||
"#1ad636", // green
|
||||
"#d61aba", // purple
|
||||
"#d5241a", // red
|
||||
];
|
||||
|
||||
export default {
|
||||
name: "Dashboard",
|
||||
components: {
|
||||
CtrlBoolean,
|
||||
CtrlNumeric,
|
||||
CtrlFunction,
|
||||
CtrlSlider,
|
||||
Plot,
|
||||
Action,
|
||||
"json-view": JSONView,
|
||||
},
|
||||
props: ["dash", "odrives"],
|
||||
data() {
|
||||
return {
|
||||
paramsVisible: false,
|
||||
addCompType: undefined,
|
||||
currentPlot: undefined,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
odriveConfigs: function () {
|
||||
return this.$store.state.odriveConfigs;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
deleteAction(e) {
|
||||
this.$emit("delete-action", e);
|
||||
console.log(e);
|
||||
},
|
||||
deletePlot(e) {
|
||||
this.$emit("delete-plot", e);
|
||||
},
|
||||
addVar(e) {
|
||||
this.$emit("add-var", e);
|
||||
},
|
||||
showTree() {
|
||||
//show the parameter tree
|
||||
this.paramsVisible = true;
|
||||
},
|
||||
hideTree() {
|
||||
this.paramsVisible = false;
|
||||
this.addCompType = undefined;
|
||||
},
|
||||
addComponent(componentType) {
|
||||
this.addCompType = componentType;
|
||||
this.paramsVisible = true;
|
||||
},
|
||||
addVarToElement(e) {
|
||||
//when the parameter tree is open and a parameter is clicked,
|
||||
//add the clicked parameter to the list of controls for the
|
||||
//current dashboard
|
||||
switch (this.addCompType) {
|
||||
case "control":
|
||||
switch (typeof e.value) {
|
||||
case "boolean":
|
||||
this.dash.controls.push({
|
||||
controlType: "CtrlBoolean",
|
||||
path: e.path,
|
||||
});
|
||||
//this.$store.commit("addSampledProperty", e.path);
|
||||
break;
|
||||
case "number":
|
||||
this.dash.controls.push({
|
||||
controlType: "CtrlNumeric",
|
||||
path: e.path,
|
||||
});
|
||||
//this.$store.commit("addSampledProperty", e.path);
|
||||
break;
|
||||
case "string":
|
||||
this.dash.controls.push({
|
||||
controlType: "CtrlFunction",
|
||||
path: e.path,
|
||||
});
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case "plot":
|
||||
// add the selected element to the plot var list
|
||||
// add the selected element to the sampling var list
|
||||
// find the plot, append path to plot.vars
|
||||
console.log(e);
|
||||
for (const plot of this.dash.plots) {
|
||||
if (plot.name == this.currentPlot) {
|
||||
plot.vars.push({
|
||||
path: e.path,
|
||||
color: plotColors[plot.vars.length % plotColors.length],
|
||||
});
|
||||
this.$store.commit("addSampledProperty", e.path);
|
||||
console.log(plot);
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case "action":
|
||||
{
|
||||
// add an action to the current dash
|
||||
let id = uuidv4();
|
||||
this.dash.actions.push({
|
||||
id: id,
|
||||
path: e.path,
|
||||
val: undefined,
|
||||
});
|
||||
}
|
||||
break;
|
||||
case "slider":
|
||||
// add a slider to the list of controls if the selected item is valid (numeric)
|
||||
switch (typeof e.value) {
|
||||
case "number":
|
||||
this.dash.controls.push({
|
||||
controlType: "CtrlSlider",
|
||||
path: e.path,
|
||||
});
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
addPlot() {
|
||||
let plotId = uuidv4();
|
||||
this.dash.plots.push({
|
||||
name: plotId,
|
||||
vars: [],
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.dashboard {
|
||||
background-color: var(--bg-color);
|
||||
height: 100vh;
|
||||
max-height: 100vh;
|
||||
width: 100vw;
|
||||
padding-top: var(--top-height);
|
||||
padding-bottom: var(--bottom-height);
|
||||
}
|
||||
|
||||
.dashboard_container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
height: 95vh;
|
||||
}
|
||||
.controls {
|
||||
flex-grow: 1;
|
||||
border-right: 1px solid lightgrey;
|
||||
}
|
||||
|
||||
.add-button {
|
||||
background-color: lightcyan;
|
||||
width: 120px;
|
||||
margin: 10px auto;
|
||||
margin-left: 10px;
|
||||
margin-right: 10px;
|
||||
text-align: center;
|
||||
border-radius: 20px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.add-button:active {
|
||||
background-color: lightblue;
|
||||
}
|
||||
|
||||
.plots {
|
||||
flex-grow: 1;
|
||||
max-height: 94vh;
|
||||
overflow-y: scroll;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.control-buttons {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.dropdown {
|
||||
position: absolute;
|
||||
padding: var(--top-height) 0;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.dropdown-content {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
padding: var(--top-height) 0;
|
||||
max-height: 90vh;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,89 @@
|
||||
<template>
|
||||
<div class="home">
|
||||
<div class="logo">
|
||||
<img alt="Odrive Logo" src="../assets/odrive_logo.png">
|
||||
</div>
|
||||
<div class="home_text">
|
||||
To set up your ODrive, connect it and power it up.
|
||||
</div>
|
||||
<div class="connected-container">
|
||||
<input type="text" v-bind:class="{ notConnected: notConnected, connected: connected}" v-on:change="setUrl" :value="serverAddress">
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
name: 'Home',
|
||||
components: {
|
||||
},
|
||||
computed: {
|
||||
connected() {
|
||||
return this.$store.state.serverConnected == true;
|
||||
},
|
||||
notConnected() {
|
||||
return this.$store.state.serverConnected != true;
|
||||
},
|
||||
serverAddress() {
|
||||
return this.$store.state.odriveServerAddress;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
setUrl(e) {
|
||||
console.log(e.target.value);
|
||||
this.$store.dispatch("setServerAddress", e.target.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.home {
|
||||
padding: 10% 0;
|
||||
margin: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: var(--bg-color);
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
img {
|
||||
margin: auto;
|
||||
display: block;
|
||||
max-width: 50%;
|
||||
width: auto;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.home_text {
|
||||
font-size: 1.5rem;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.connected, .notConnected {
|
||||
width: 40vw;
|
||||
margin: 0;
|
||||
padding: 10px;
|
||||
border: none;
|
||||
border-bottom: 1px solid lightgrey;
|
||||
font-size: 1.5rem;
|
||||
background-color: var(--bg-color);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
input:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.connected {
|
||||
border-bottom: 1px solid lightgreen;
|
||||
background-color: lightgreen;
|
||||
}
|
||||
|
||||
.notConnected {
|
||||
border-bottom: 1px solid red;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,160 @@
|
||||
<template>
|
||||
<div class="wizard">
|
||||
<div class="wizard-container">
|
||||
<div class="wizard-page">
|
||||
<div>
|
||||
<!-- show the appropriate component -->
|
||||
<component v-bind:is="currentPage"></component>
|
||||
</div>
|
||||
<div class="wizard-controls">
|
||||
<!-- show breadcrumbs, back, apply, next buttons -->
|
||||
<button class="wizBtn card" @click="back">BACK</button>
|
||||
<button class="wizBtn card" @click="apply">APPLY</button>
|
||||
<button class="wizBtn card" @click="next">NEXT</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import wizardODrive from "../components/wizard/wizardODrive.vue";
|
||||
import wizardAxis from "../components/wizard/wizardAxis.vue";
|
||||
import wizardMotor from "../components/wizard/wizardMotor.vue";
|
||||
import wizardEncoder from "../components/wizard/wizardEncoder.vue";
|
||||
import wizardLimits from "../components/wizard/wizardLimits.vue";
|
||||
|
||||
let wizard_states = {
|
||||
PICK_ODRIVE: {
|
||||
val: 0,
|
||||
page: "wizardODrive"
|
||||
},
|
||||
PICK_AXIS: {
|
||||
val: 1,
|
||||
page: "wizardAxis",
|
||||
},
|
||||
MOTOR_0: {
|
||||
val: 2,
|
||||
page: "wizardMotor",
|
||||
},
|
||||
ENCODER_0: {
|
||||
val: 3,
|
||||
page: "wizardEncoder",
|
||||
},
|
||||
MOTOR_1: {
|
||||
val: 4,
|
||||
page: "wizardMotor",
|
||||
},
|
||||
ENCODER_1: {
|
||||
val: 5,
|
||||
page: "wizardEncoder",
|
||||
},
|
||||
LIMITS_0: {
|
||||
val: 6,
|
||||
page: "wizardLimits",
|
||||
},
|
||||
LIMITS_1: {
|
||||
val: 7,
|
||||
page: "wizardLimits",
|
||||
},
|
||||
};
|
||||
|
||||
export default {
|
||||
name: "Wizard",
|
||||
components: {
|
||||
wizardODrive,
|
||||
wizardAxis,
|
||||
wizardMotor,
|
||||
wizardEncoder,
|
||||
wizardLimits,
|
||||
},
|
||||
props: [],
|
||||
data: function () {
|
||||
return {
|
||||
currentStep: wizard_states.PICK_ODRIVE,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
currentPage() {
|
||||
return this.currentStep.page;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
next() {
|
||||
// go to the next page
|
||||
let state = this.currentStep;
|
||||
switch (this.currentStep) {
|
||||
case wizard_states.PICK_ODRIVE:
|
||||
this.currentStep = wizard_states.PICK_AXIS;
|
||||
break;
|
||||
case wizard_states.PICK_AXIS:
|
||||
this.currentStep = wizard_states.MOTOR_0;
|
||||
break;
|
||||
case wizard_states.MOTOR_0:
|
||||
this.currentStep = wizard_states.ENCODER_0;
|
||||
break;
|
||||
case wizard_states.ENCODER_0:
|
||||
this.currentStep = wizard_states.LIMITS_0;
|
||||
break;
|
||||
case wizard_states.LIMITS_0:
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
console.log("Going from " + state.val + " to " + this.currentStep.val);
|
||||
},
|
||||
apply() {
|
||||
// apply settings to odrive
|
||||
console.log("applying config");
|
||||
},
|
||||
back() {
|
||||
//go to the previous page
|
||||
let state = this.currentStep;
|
||||
switch (this.currentStep) {
|
||||
case wizard_states.PICK_ODRIVE:
|
||||
break;
|
||||
case wizard_states.PICK_AXIS:
|
||||
this.currentStep = wizard_states.PICK_ODRIVE;
|
||||
break;
|
||||
case wizard_states.MOTOR_0:
|
||||
this.currentStep = wizard_states.PICK_AXIS;
|
||||
break;
|
||||
case wizard_states.ENCODER_0:
|
||||
this.currentStep = wizard_states.MOTOR_0;
|
||||
break;
|
||||
case wizard_states.LIMITS_0:
|
||||
this.currentStep = wizard_states.ENCODER_0;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
console.log("Going from " + state.val + " to " + this.currentStep.val);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.wizard {
|
||||
background-color: var(--bg-color);
|
||||
height: 100vh;
|
||||
max-height: 100vh;
|
||||
width: 100vw;
|
||||
padding-top: var(--top-height);
|
||||
padding-bottom: var(--bottom-height);
|
||||
}
|
||||
|
||||
.wizard-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 95vh;
|
||||
}
|
||||
|
||||
.wizard-page {
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.wizard-controls {
|
||||
margin-top: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,29 @@
|
||||
// this file is used for configuring electron-builder
|
||||
|
||||
module.exports = {
|
||||
pluginOptions: {
|
||||
electronBuilder: {
|
||||
builderOptions: {
|
||||
"productName": "ODriveGUI",
|
||||
"asar": false,
|
||||
"extraResources": "server",
|
||||
"artifactName": "${name}_${os}.${ext}",
|
||||
"win" : {
|
||||
"target" : [
|
||||
{
|
||||
"target": "portable",
|
||||
}
|
||||
]
|
||||
},
|
||||
"linux" : {
|
||||
"target" : [
|
||||
{
|
||||
"target": "AppImage",
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
//mainProcessArgs: ['C:/Users/pajoh/Desktop/ODrive_work/ODrive/tools', 'C:/Users/pajoh/Desktop/ODrive_work/ODrive/Firmware']
|
||||
}
|
||||
}
|
||||
}
|
||||