@@ -65,4 +65,4 @@ Firmware/Tests/bin/
|
||||
# GUI
|
||||
GUI/dist_electron
|
||||
GUI/node_modules
|
||||
GUI/build
|
||||
GUI/build
|
||||
|
||||
@@ -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,42 @@
|
||||
# odrive_gui
|
||||
|
||||
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'
|
||||
]
|
||||
}
|
||||
|
After Width: | Height: | Size: 46 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 627 B |
|
After Width: | Height: | Size: 998 B |
|
After Width: | Height: | Size: 5.4 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 5.9 KiB |
|
After Width: | Height: | Size: 2.4 KiB |
|
After Width: | Height: | Size: 353 KiB |
@@ -0,0 +1,70 @@
|
||||
{
|
||||
"name": "ODriveGUI",
|
||||
"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",
|
||||
"electron:generate-icons": "electron-icon-builder --input=./public/icon.png --output=build --flatten"
|
||||
},
|
||||
"main": "background.js",
|
||||
"dependencies": {
|
||||
"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",
|
||||
"typeface-roboto": "0.0.75",
|
||||
"uuid": "^8.2.0",
|
||||
"vue": "^2.6.11",
|
||||
"vue-chartjs": "^3.5.0",
|
||||
"vue-context": "^5.2.0",
|
||||
"vue-directive-tooltip": "^1.6.3",
|
||||
"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",
|
||||
"electron-icon-builder": "^1.0.2",
|
||||
"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 |
|
After Width: | Height: | Size: 12 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,251 @@
|
||||
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
|
||||
from engineio.payload import Payload
|
||||
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)
|
||||
Payload.max_decode_packets = 100
|
||||
socketio = SocketIO(app, cors_allowed_origins="*", async_mode = "threading")
|
||||
|
||||
#def get_odrive():
|
||||
# globals()['odrives'] = []
|
||||
# globals()['odrives'].append(odrive.find_any())
|
||||
# globals()['odrives'][0].__channel__._channel_broken.subscribe(lambda: handle_disconnect())
|
||||
# print("odrives found")
|
||||
# socketio.emit('odrive-found')
|
||||
|
||||
def discovered_device(device):
|
||||
# when device is discovered, add it to list of serial numbers and global odrive list
|
||||
# shamelessly lifted from odrive python package
|
||||
serial_number = '{:012X}'.format(device.serial_number) if hasattr(device, 'serial_number') else "[unknown serial number]"
|
||||
if serial_number in globals()['discovered_devices']:
|
||||
index = globals()['discovered_devices'].index(serial_number)
|
||||
else:
|
||||
globals()['discovered_devices'].append(serial_number)
|
||||
index = len(globals()['discovered_devices']) - 1
|
||||
odrive_name = "odrive" + str(index)
|
||||
|
||||
# add to list of odrives
|
||||
while globals()['inUse']:
|
||||
time.sleep(0.1)
|
||||
globals()['odrives'][odrive_name] = device
|
||||
print("Found " + str(serial_number))
|
||||
print("odrive list: " + str([key for key in globals()['odrives'].keys()]))
|
||||
socketio.emit('odrive-found')
|
||||
|
||||
def start_discovery():
|
||||
print("starting disco loop...")
|
||||
log = fibre.Logger(verbose = False)
|
||||
shutdown = fibre.Event()
|
||||
fibre.find_all("usb", None, discovered_device, shutdown, shutdown, log)
|
||||
|
||||
def handle_disconnect():
|
||||
print("lost odrive")
|
||||
#socketio.emit('odrive-disconnected')
|
||||
|
||||
@socketio.on('findODrives')
|
||||
def getODrives(message):
|
||||
print("looking for odrive")
|
||||
start_discovery()
|
||||
|
||||
@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!')
|
||||
|
||||
@socketio.on('getODrives')
|
||||
def get_odrives(data):
|
||||
# spinlock
|
||||
while globals()['inUse']:
|
||||
time.sleep(0.1)
|
||||
|
||||
globals()['inUse'] = True
|
||||
odriveDict = {}
|
||||
#for (index, odrv) in enumerate(globals()['odrives']):
|
||||
# odriveDict["odrive" + str(index)] = dictFromRO(odrv)
|
||||
for key in globals()['odrives'].keys():
|
||||
odriveDict[key] = dictFromRO(globals()['odrives'][key])
|
||||
globals()['inUse'] = False
|
||||
emit('odrives', json.dumps(odriveDict))
|
||||
|
||||
@socketio.on('getProperty')
|
||||
def get_property(message):
|
||||
# message is dict natively
|
||||
# will be {"path": "odriveX.axisY.blah.blah"}
|
||||
while globals()['inUse']:
|
||||
time.sleep(0.1)
|
||||
globals()['inUse'] = True
|
||||
val = getVal(globals()['odrives'], message["path"].split('.'))
|
||||
emit('ODriveProperty', json.dumps({"path": message["path"], "val": val}))
|
||||
globals()['inUse'] = False
|
||||
|
||||
@socketio.on('setProperty')
|
||||
def set_property(message):
|
||||
# message is {"path":, "val":, "type":}
|
||||
while globals()['inUse']:
|
||||
time.sleep(0.1)
|
||||
globals()['inUse'] = True
|
||||
print("From setProperty event handler: " + str(message))
|
||||
postVal(globals()['odrives'], message["path"].split('.'), message["val"], message["type"])
|
||||
val = getVal(globals()['odrives'], message["path"].split('.'))
|
||||
emit('ODriveProperty', json.dumps({"path": message["path"], "val": val}))
|
||||
globals()['inUse'] = False
|
||||
|
||||
@socketio.on('callFunction')
|
||||
def call_function(message):
|
||||
# message is {"path"}, no args yet (do we know which functions accept arguments from the odrive tree directly?)
|
||||
while globals()['inUse']:
|
||||
time.sleep(0.1)
|
||||
globals()['inUse'] = True
|
||||
print("From callFunction event handler: " + str(message))
|
||||
callFunc(globals()['odrives'], message["path"].split('.'))
|
||||
globals()['inUse'] = False
|
||||
|
||||
@app.route('/', methods=['GET'])
|
||||
def home():
|
||||
return "<h1>ODrive GUI Server</h1>"
|
||||
|
||||
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
|
||||
|
||||
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)
|
||||
try:
|
||||
#index = int(''.join([char for char in keyList.pop(0) if char.isnumeric()]))
|
||||
odrv = keyList.pop(0)
|
||||
RO = odrives[odrv]
|
||||
for key in keyList:
|
||||
RO = RO._remote_attributes[key]
|
||||
if argType == "number":
|
||||
RO.set_value(float(value))
|
||||
elif argType == "boolean":
|
||||
RO.set_value(value == "true")
|
||||
else:
|
||||
pass # dont support that type yet
|
||||
except fibre.protocol.ChannelBrokenException:
|
||||
handle_disconnect()
|
||||
except:
|
||||
print("exception in postVal")
|
||||
|
||||
def getVal(odrives, keyList):
|
||||
try:
|
||||
#index = int(''.join([char for char in keyList.pop(0) if char.isnumeric()]))
|
||||
odrv = keyList.pop(0)
|
||||
RO = odrives[odrv]
|
||||
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 fibre.protocol.ChannelBrokenException:
|
||||
handle_disconnect()
|
||||
except:
|
||||
print("exception in getVal")
|
||||
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(globals()['odrives'], keys)
|
||||
|
||||
return samples
|
||||
|
||||
def callFunc(odrives, keyList):
|
||||
try:
|
||||
#index = int(''.join([char for char in keyList.pop(0) if char.isnumeric()]))
|
||||
odrv = keyList.pop(0)
|
||||
RO = odrives[odrv]
|
||||
for key in keyList:
|
||||
RO = RO._remote_attributes[key]
|
||||
if isinstance(RO, fibre.remote_object.RemoteFunction):
|
||||
RO.__call__()
|
||||
except fibre.protocol.ChannelBrokenException:
|
||||
handle_disconnect()
|
||||
except:
|
||||
print("fcn call failed")
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("args from python: " + str(sys.argv[1:0]))
|
||||
#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
|
||||
|
||||
globals()['odrives'] = {}
|
||||
globals()['discovered_devices'] = []
|
||||
# spinlock
|
||||
globals()['inUse'] = False
|
||||
|
||||
log = fibre.Logger(verbose=False)
|
||||
shutdown = fibre.Event()
|
||||
|
||||
socketio.run(app, host='0.0.0.0', port=5000)
|
||||
@@ -0,0 +1,417 @@
|
||||
<template>
|
||||
<div id="app">
|
||||
<!-- HEADER -->
|
||||
<div class="header">
|
||||
<button
|
||||
v-for="dash in dashboards"
|
||||
:key="dash.id"
|
||||
:class="['dash-button', { active: currentDash === dash.name }]"
|
||||
v-on:click.self="changeDash(dash.name)"
|
||||
v-on:dblclick="changeDashName(dash.id)"
|
||||
>
|
||||
<button
|
||||
v-if="
|
||||
dash.name !== 'Start' &&
|
||||
dash.name !== 'Tuning' &&
|
||||
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="dash-button sample-button" :class="[{ active: sampling === true }]" @click="sampleButton">{{samplingText}}</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>
|
||||
</div>
|
||||
|
||||
<!-- PAGE CONTENT -->
|
||||
<component
|
||||
:is="currentDashName"
|
||||
:odrives="odrives"
|
||||
:dash="dash"
|
||||
></component>
|
||||
|
||||
<!-- FOOTER -->
|
||||
<div class="footer">
|
||||
<Axis
|
||||
v-for="axis in axes"
|
||||
:key="axis.name"
|
||||
:axis="axis.name"
|
||||
:odrives="odrives"
|
||||
></Axis>
|
||||
<!--<div class="odrive-status">
|
||||
ODrive:{{ODriveConnected}}
|
||||
</div>-->
|
||||
</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 { v4 as uuidv4 } from "uuid";
|
||||
|
||||
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.$store.state.currentDash) {
|
||||
comp = dash.component;
|
||||
}
|
||||
}
|
||||
return comp;
|
||||
},
|
||||
dash: function () {
|
||||
let comp = {};
|
||||
for (const dash of this.dashboards) {
|
||||
if (dash.name === this.$store.state.currentDash) {
|
||||
comp = dash;
|
||||
}
|
||||
}
|
||||
return comp;
|
||||
},
|
||||
currentCtrlList: function () {
|
||||
let comp = {};
|
||||
for (const dash of this.dashboards) {
|
||||
if (dash.name === this.$store.state.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;
|
||||
},
|
||||
currentDash: function () {
|
||||
return this.$store.state.currentDash;
|
||||
},
|
||||
ODriveConnected: function () {
|
||||
// if server and odrive disconnected, disconnected
|
||||
// if server connected and odrive disco, connecting
|
||||
// if server and odrive connected, connected
|
||||
let ret;
|
||||
if (this.$store.state.serverConnected && this.$store.state.ODriveConnected) {
|
||||
ret = "connected";
|
||||
}
|
||||
else if (this.$store.state.serverConnected && !this.$store.state.ODriveConnected) {
|
||||
ret = "connecting...";
|
||||
}
|
||||
else {
|
||||
ret = "disconnected";
|
||||
}
|
||||
return ret;
|
||||
},
|
||||
samplingText: function () {
|
||||
let ret;
|
||||
if (this.$store.state.sampling) {
|
||||
ret = "stop sampling"
|
||||
}
|
||||
else {
|
||||
ret = "start sampling"
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
changeDash(dashName) {
|
||||
console.log(dashName);
|
||||
this.$store.commit("setDash", dashName);
|
||||
},
|
||||
//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.$store.commit("setDash", "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");
|
||||
},
|
||||
sampleButton() {
|
||||
if (this.$store.state.sampling) {
|
||||
// sampling acttive, stop
|
||||
socketio.sendEvent({
|
||||
type: "stopSampling",
|
||||
});
|
||||
this.$store.state.sampling = false;
|
||||
}
|
||||
else {
|
||||
// sampling inactive, start sampling
|
||||
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;
|
||||
}
|
||||
},
|
||||
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?
|
||||
},
|
||||
emitFindODrives() {
|
||||
socketio.sendEvent({
|
||||
type: "findODrives",
|
||||
data: {},
|
||||
});
|
||||
},
|
||||
},
|
||||
created() {
|
||||
// on app creation, set the address to the default
|
||||
this.$store.dispatch("setServerAddress", "http://127.0.0.1:5000");
|
||||
|
||||
// to allow running as web app
|
||||
if (window.ipcRenderer != undefined) {
|
||||
window.ipcRenderer.on('server-stdout', (event, arg) => {
|
||||
this.$store.commit('logServerMessage', arg);
|
||||
});
|
||||
window.ipcRenderer.on('server-stderr', (event, arg) => {
|
||||
this.$store.commit('logServerMessage', arg);
|
||||
});
|
||||
window.ipcRenderer.send('start-server');
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@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 {
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
color: black;
|
||||
text-decoration: none;
|
||||
padding: 10px;
|
||||
background-color: var(--fg-color);
|
||||
border-style: none;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.dash-button {
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
color: black;
|
||||
text-decoration: none;
|
||||
padding: 10px;
|
||||
background-color: var(--fg-color);
|
||||
border-style: none;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.dash-button:active {
|
||||
background-color: var(--bg-color);
|
||||
}
|
||||
|
||||
.dash-button:hover {
|
||||
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;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.emergency-stop {
|
||||
margin-left: auto;
|
||||
padding-left: 2rem;
|
||||
padding-right: 2rem;
|
||||
background-color: red;
|
||||
font-weight: bold;
|
||||
color: white;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.sample-button {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.odrive-status {
|
||||
margin-left: auto;
|
||||
font-family: "Roboto Mono", monospace;
|
||||
padding: 5px 10px;
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,127 @@
|
||||
{
|
||||
"name": "Tuning",
|
||||
"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": "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": "CtrlEnum",
|
||||
"path": "odrives.odrive0.axis0.requested_state",
|
||||
"options": [
|
||||
{
|
||||
"text": "Undefined",
|
||||
"value": 0
|
||||
},
|
||||
{
|
||||
"text": "Idle",
|
||||
"value": 1
|
||||
},
|
||||
{
|
||||
"text": "Starup Sequence",
|
||||
"value": 2
|
||||
},
|
||||
{
|
||||
"text": "Full Calibration Sequence",
|
||||
"value": 3
|
||||
},
|
||||
{
|
||||
"text": "Motor Calibration",
|
||||
"value": 4
|
||||
},
|
||||
{
|
||||
"text": "Sensorless Control",
|
||||
"value": 5
|
||||
},
|
||||
{
|
||||
"text": "Encoder Index Search",
|
||||
"value": 6
|
||||
},
|
||||
{
|
||||
"text": "Encoder Offset Calibration",
|
||||
"value": 7
|
||||
},
|
||||
{
|
||||
"text": "Closed Loop Control",
|
||||
"value": 8
|
||||
},
|
||||
{
|
||||
"text": "Lockin Spin",
|
||||
"value": 9
|
||||
},
|
||||
{
|
||||
"text": "Encoder Direction Find",
|
||||
"value": 10
|
||||
},
|
||||
{
|
||||
"text": "Homing",
|
||||
"value": 11
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"controlType": "CtrlFunction",
|
||||
"path": "odrives.odrive0.save_configuration"
|
||||
}
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"actionType": "Action",
|
||||
"id": "a699c462-cf8d-4b4e-aedf-0b200e84f97a",
|
||||
"path": "odrives.odrive0.axis0.controller.pos_setpoint",
|
||||
"val": 0
|
||||
},
|
||||
{
|
||||
"actionType": "Action",
|
||||
"id": "db267ae4-fdf4-4770-a2d3-eb61de026659",
|
||||
"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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
{
|
||||
"name": "Tuning",
|
||||
"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": "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": "CtrlEnum",
|
||||
"path": "odrives.odrive0.axis0.requested_state",
|
||||
"options": [
|
||||
{
|
||||
"text": "Undefined",
|
||||
"value": 0
|
||||
},
|
||||
{
|
||||
"text": "Idle",
|
||||
"value": 1
|
||||
},
|
||||
{
|
||||
"text": "Starup Sequence",
|
||||
"value": 2
|
||||
},
|
||||
{
|
||||
"text": "Full Calibration Sequence",
|
||||
"value": 3
|
||||
},
|
||||
{
|
||||
"text": "Motor Calibration",
|
||||
"value": 4
|
||||
},
|
||||
{
|
||||
"text": "Sensorless Control",
|
||||
"value": 5
|
||||
},
|
||||
{
|
||||
"text": "Encoder Index Search",
|
||||
"value": 6
|
||||
},
|
||||
{
|
||||
"text": "Encoder Offset Calibration",
|
||||
"value": 7
|
||||
},
|
||||
{
|
||||
"text": "Closed Loop Control",
|
||||
"value": 8
|
||||
},
|
||||
{
|
||||
"text": "Lockin Spin",
|
||||
"value": 9
|
||||
},
|
||||
{
|
||||
"text": "Encoder Direction Find",
|
||||
"value": 10
|
||||
},
|
||||
{
|
||||
"text": "Homing",
|
||||
"value": 11
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"controlType": "CtrlFunction",
|
||||
"path": "odrives.odrive0.save_configuration"
|
||||
}
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"actionType": "Action",
|
||||
"id": "a699c462-cf8d-4b4e-aedf-0b200e84f97a",
|
||||
"path": "odrives.odrive0.axis0.controller.input_pos",
|
||||
"val": 0
|
||||
},
|
||||
{
|
||||
"actionType": "Action",
|
||||
"id": "db267ae4-fdf4-4770-a2d3-eb61de026659",
|
||||
"path": "odrives.odrive0.axis0.controller.input_pos",
|
||||
"val": 10
|
||||
}
|
||||
],
|
||||
"plots": [
|
||||
{
|
||||
"name": "65a2768d-e61e-4dd5-bf80-dba153838f74",
|
||||
"vars": [
|
||||
{
|
||||
"path": "odrives.odrive0.axis0.controller.input_pos",
|
||||
"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: 169 KiB |
|
After Width: | Height: | Size: 171 KiB |
|
After Width: | Height: | Size: 109 KiB |
|
After Width: | Height: | Size: 122 KiB |
|
After Width: | Height: | Size: 130 KiB |
|
After Width: | Height: | Size: 166 KiB |
|
After Width: | Height: | Size: 165 KiB |
|
After Width: | Height: | Size: 112 KiB |
|
After Width: | Height: | Size: 46 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
@@ -0,0 +1,132 @@
|
||||
{
|
||||
"GPIO_MODE_DIGITAL" : 0,
|
||||
"GPIO_MODE_DIGITAL_PULL_UP" : 1,
|
||||
"GPIO_MODE_DIGITAL_PULL_DOWN" : 2,
|
||||
"GPIO_MODE_ANALOG_IN" : 3,
|
||||
"GPIO_MODE_UART0" : 4,
|
||||
"GPIO_MODE_UART1" : 5,
|
||||
"GPIO_MODE_UART2" : 6,
|
||||
"GPIO_MODE_CAN0" : 7,
|
||||
"GPIO_MODE_I2C0" : 8,
|
||||
"GPIO_MODE_SPI0" : 9,
|
||||
"GPIO_MODE_PWM0" : 10,
|
||||
"GPIO_MODE_ENC0" : 11,
|
||||
"GPIO_MODE_ENC1" : 12,
|
||||
"GPIO_MODE_ENC2" : 13,
|
||||
|
||||
"PROTOCOL_SIMPLE" : 0,
|
||||
|
||||
"AXIS_STATE_UNDEFINED" : 0,
|
||||
"AXIS_STATE_IDLE" : 1,
|
||||
"AXIS_STATE_STARTUP_SEQUENCE" : 2,
|
||||
"AXIS_STATE_FULL_CALIBRATION_SEQUENCE" : 3,
|
||||
"AXIS_STATE_MOTOR_CALIBRATION" : 4,
|
||||
"AXIS_STATE_SENSORLESS_CONTROL" : 5,
|
||||
"AXIS_STATE_ENCODER_INDEX_SEARCH" : 6,
|
||||
"AXIS_STATE_ENCODER_OFFSET_CALIBRATION" : 7,
|
||||
"AXIS_STATE_CLOSED_LOOP_CONTROL" : 8,
|
||||
"AXIS_STATE_LOCKIN_SPIN" : 9,
|
||||
"AXIS_STATE_ENCODER_DIR_FIND" : 10,
|
||||
"AXIS_STATE_HOMING" : 11,
|
||||
|
||||
"THERMISTOR_CURRENT_LIMITER_ERROR_NONE" : 0,
|
||||
"THERMISTOR_CURRENT_LIMITER_ERROR_OVER_TEMP" : 1,
|
||||
|
||||
"ENCODER_MODE_INCREMENTAL" : 0,
|
||||
"ENCODER_MODE_HALL" : 1,
|
||||
"ENCODER_MODE_SINCOS" : 2,
|
||||
"ENCODER_MODE_SPI_ABS_CUI" : 256,
|
||||
"ENCODER_MODE_SPI_ABS_AMS" : 257,
|
||||
"ENCODER_MODE_SPI_ABS_AEAT" : 258,
|
||||
"ENCODER_MODE_SPI_ABS_RLS" : 259,
|
||||
|
||||
"CONTROL_MODE_VOLTAGE_CONTROL" : 0,
|
||||
"CONTROL_MODE_TORQUE_CONTROL" : 1,
|
||||
"CONTROL_MODE_VELOCITY_CONTROL" : 2,
|
||||
"CONTROL_MODE_POSITION_CONTROL" : 3,
|
||||
|
||||
"INPUT_MODE_INACTIVE" : 0,
|
||||
"INPUT_MODE_PASSTHROUGH" : 1,
|
||||
"INPUT_MODE_VEL_RAMP" : 2,
|
||||
"INPUT_MODE_POS_FILTER" : 3,
|
||||
"INPUT_MODE_MIX_CHANNELS" : 4,
|
||||
"INPUT_MODE_TRAP_TRAJ" : 5,
|
||||
"INPUT_MODE_TORQUE_RAMP" : 6,
|
||||
"INPUT_MODE_MIRROR" : 7,
|
||||
|
||||
"MOTOR_TYPE_HIGH_CURRENT" : 0,
|
||||
"MOTOR_TYPE_GIMBAL" : 2,
|
||||
"MOTOR_TYPE_ACIM" : 3,
|
||||
|
||||
"CAN_ERROR_NONE" : 0,
|
||||
"CAN_ERROR_DUPLICATE_CAN_IDS" : 1,
|
||||
|
||||
"AXIS_ERROR_NONE" : 0,
|
||||
"AXIS_ERROR_INVALID_STATE" : 1,
|
||||
"AXIS_ERROR_DC_BUS_UNDER_VOLTAGE" : 2,
|
||||
"AXIS_ERROR_DC_BUS_OVER_VOLTAGE" : 4,
|
||||
"AXIS_ERROR_CURRENT_MEASUREMENT_TIMEOUT" : 8,
|
||||
"AXIS_ERROR_BRAKE_RESISTOR_DISARMED" : 16,
|
||||
"AXIS_ERROR_MOTOR_DISARMED" : 32,
|
||||
"AXIS_ERROR_MOTOR_FAILED" : 64,
|
||||
"AXIS_ERROR_SENSORLESS_ESTIMATOR_FAILED" : 128,
|
||||
"AXIS_ERROR_ENCODER_FAILED" : 256,
|
||||
"AXIS_ERROR_CONTROLLER_FAILED" : 512,
|
||||
"AXIS_ERROR_POS_CTRL_DURING_SENSORLESS" : 1024,
|
||||
"AXIS_ERROR_WATCHDOG_TIMER_EXPIRED" : 2048,
|
||||
"AXIS_ERROR_MIN_ENDSTOP_PRESSED" : 4096,
|
||||
"AXIS_ERROR_MAX_ENDSTOP_PRESSED" : 8192,
|
||||
"AXIS_ERROR_ESTOP_REQUESTED" : 16384,
|
||||
"AXIS_ERROR_HOMING_WITHOUT_ENDSTOP" : 131072,
|
||||
"AXIS_ERROR_OVER_TEMP" : 262144,
|
||||
|
||||
"LOCKIN_STATE_INACTIVE" : 0,
|
||||
"LOCKIN_STATE_RAMP" : 1,
|
||||
"LOCKIN_STATE_ACCELERATE" : 2,
|
||||
"LOCKIN_STATE_CONST_VEL" : 3,
|
||||
|
||||
"MOTOR_ERROR_NONE" : 0,
|
||||
"MOTOR_ERROR_PHASE_RESISTANCE_OUT_OF_RANGE" : 1,
|
||||
"MOTOR_ERROR_PHASE_INDUCTANCE_OUT_OF_RANGE" : 2,
|
||||
"MOTOR_ERROR_ADC_FAILED" : 4,
|
||||
"MOTOR_ERROR_DRV_FAULT" : 8,
|
||||
"MOTOR_ERROR_CONTROL_DEADLINE_MISSED" : 16,
|
||||
"MOTOR_ERROR_NOT_IMPLEMENTED_MOTOR_TYPE" : 32,
|
||||
"MOTOR_ERROR_BRAKE_CURRENT_OUT_OF_RANGE" : 64,
|
||||
"MOTOR_ERROR_MODULATION_MAGNITUDE" : 128,
|
||||
"MOTOR_ERROR_BRAKE_DEADTIME_VIOLATION" : 256,
|
||||
"MOTOR_ERROR_UNEXPECTED_TIMER_CALLBACK" : 512,
|
||||
"MOTOR_ERROR_CURRENT_SENSE_SATURATION" : 1024,
|
||||
"MOTOR_ERROR_CURRENT_LIMIT_VIOLATION" : 4096,
|
||||
"MOTOR_ERROR_BRAKE_DUTY_CYCLE_NAN" : 8192,
|
||||
"MOTOR_ERROR_DC_BUS_OVER_REGEN_CURRENT" : 16384,
|
||||
"MOTOR_ERROR_DC_BUS_OVER_CURRENT" : 32768,
|
||||
"MOTOR_ERROR_MODULATION_IS_NAN" : 65536,
|
||||
|
||||
"ARMED_STATE_DISARMED" : 0,
|
||||
"ARMED_STATE_WAITING_FOR_TIMINGS" : 1,
|
||||
"ARMED_STATE_WAITING_FOR_UPDATE" : 2,
|
||||
"ARMED_STATE_ARMED" : 3,
|
||||
|
||||
"CONTROLLER_ERROR_NONE" : 0,
|
||||
"CONTROLLER_ERROR_OVERSPEED" : 1,
|
||||
"CONTROLLER_ERROR_INVALID_INPUT_MODE" : 2,
|
||||
"CONTROLLER_ERROR_UNSTABLE_GAIN" : 4,
|
||||
"CONTROLLER_ERROR_INVALID_MIRROR_AXIS" : 8,
|
||||
"CONTROLLER_ERROR_INVALID_LOAD_ENCODER" : 16,
|
||||
"CONTROLLER_ERROR_INVALID_ESTIMATE" : 32,
|
||||
|
||||
"ENCODER_ERROR_NONE" : 0,
|
||||
"ENCODER_ERROR_UNSTABLE_GAIN" : 1,
|
||||
"ENCODER_ERROR_CPR_POLEPAIRS_MISMATCH" : 2,
|
||||
"ENCODER_ERROR_NO_RESPONSE" : 4,
|
||||
"ENCODER_ERROR_UNSUPPORTED_ENCODER_MODE" : 8,
|
||||
"ENCODER_ERROR_ILLEGAL_HALL_STATE" : 16,
|
||||
"ENCODER_ERROR_INDEX_NOT_FOUND_YET" : 32,
|
||||
"ENCODER_ERROR_ABS_SPI_TIMEOUT" : 64,
|
||||
"ENCODER_ERROR_ABS_SPI_COM_FAIL" : 128,
|
||||
"ENCODER_ERROR_ABS_SPI_NOT_READY" : 256,
|
||||
|
||||
"SENSORLESS_ESTIMATOR_ERROR_NONE" : 0,
|
||||
"SENSORLESS_ESTIMATOR_ERROR_UNSTABLE_GAIN" : 1
|
||||
}
|
||||
|
After Width: | Height: | Size: 52 KiB |
@@ -0,0 +1,23 @@
|
||||
.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);
|
||||
}
|
||||
|
||||
.choice-inactive {
|
||||
background-color: var(--bg-color);
|
||||
color: grey;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
:root {
|
||||
--bg-color: rgb(235, 235, 235);
|
||||
--fg-color: #fff;
|
||||
--top-height: 39px;
|
||||
--bottom-height: 29px;
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
{
|
||||
"config": {
|
||||
"brake_resistance": null
|
||||
},
|
||||
"axis0": {
|
||||
"motor": {
|
||||
"config": {
|
||||
"pole_pairs": null,
|
||||
"phase_resistance": null,
|
||||
"phase_inductance": null,
|
||||
"motor_type": null,
|
||||
"current_lim": null,
|
||||
"torque_constant": null
|
||||
}
|
||||
},
|
||||
"encoder": {
|
||||
"config": {
|
||||
"mode": null,
|
||||
"use_index": null,
|
||||
"cpr": null
|
||||
}
|
||||
},
|
||||
"controller": {
|
||||
"config": {
|
||||
"input_mode": null,
|
||||
"control_mode": null,
|
||||
"vel_limit": null,
|
||||
"inertia": null
|
||||
}
|
||||
},
|
||||
"trap_traj": {
|
||||
"config": {
|
||||
"vel_limit": null,
|
||||
"accel_limit": null,
|
||||
"decel_limit": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"axis1": {
|
||||
"motor": {
|
||||
"config": {
|
||||
"pole_pairs": null,
|
||||
"phase_resistance": null,
|
||||
"phase_inductance": null,
|
||||
"motor_type": null,
|
||||
"current_lim": null,
|
||||
"torque_constant": null
|
||||
}
|
||||
},
|
||||
"encoder": {
|
||||
"config": {
|
||||
"mode": null,
|
||||
"use_index": null,
|
||||
"cpr": null
|
||||
}
|
||||
},
|
||||
"controller": {
|
||||
"config": {
|
||||
"input_mode": null,
|
||||
"control_mode": null,
|
||||
"vel_limit": null,
|
||||
"inertia": null
|
||||
}
|
||||
},
|
||||
"trap_traj": {
|
||||
"config": {
|
||||
"vel_limit": null,
|
||||
"accel_limit": null,
|
||||
"decel_limit": null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
'use strict'
|
||||
|
||||
import { app, protocol, BrowserWindow, ipcMain } 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;
|
||||
|
||||
// var for python server
|
||||
let server;
|
||||
|
||||
// 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() {
|
||||
|
||||
// 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,
|
||||
preload: path.join(__dirname, 'preload.js')
|
||||
},
|
||||
icon: path.join(__static, 'icon.png')
|
||||
})
|
||||
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()
|
||||
// Figure out correct path and arguments to launch 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);
|
||||
}
|
||||
}
|
||||
// launch python server on event from renderer process (gui) and pipe stdout/stderr to it
|
||||
ipcMain.on('start-server', () => {
|
||||
server = spawn(getPyCmd(), effectiveCommand);
|
||||
server.stdout.on('data',function(data) {
|
||||
console.log(data.toString('utf8'));
|
||||
try {
|
||||
win.webContents.send('server-stdout', String(data.toString('utf8')));
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
});
|
||||
server.stderr.on('data',function(data) {
|
||||
console.log(data.toString('utf8'));
|
||||
try {
|
||||
win.webContents.send('server-stderr', String(data.toString('utf8')));
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
});
|
||||
})
|
||||
ipcMain.on('kill-server', () => {
|
||||
server.kill('SIGINT');
|
||||
})
|
||||
})
|
||||
|
||||
// Exit cleanly on request from parent process in development mode.
|
||||
if (isDevelopment) {
|
||||
if (process.platform === 'win32') {
|
||||
process.on('message', (data) => {
|
||||
if (data === 'graceful-exit') {
|
||||
server.kill('SIGINT');
|
||||
app.quit()
|
||||
}
|
||||
})
|
||||
} else {
|
||||
process.on('SIGTERM', () => {
|
||||
server.kill('SIGINT');
|
||||
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,282 @@
|
||||
<template>
|
||||
<div
|
||||
class="axis"
|
||||
@click.self="showError = !showError;"
|
||||
:class="{ noError: !error, error: error}"
|
||||
>
|
||||
{{ axis }}
|
||||
<div v-show="showError" class="error-popup card" @click.self="showError = !showError">
|
||||
<clear-errors :data="{axis: axis}"/>
|
||||
axis:
|
||||
<span :class="{ noError: !axisError, error: axisError}">{{axisErrorMsg}}</span>
|
||||
<br />motor:
|
||||
<span :class="{ noError: !motorError, error: motorError}">{{motorErrorMsg}}</span>
|
||||
<br />encoder:
|
||||
<span
|
||||
:class="{ noError: !encoderError, error: encoderError}"
|
||||
>{{encoderErrorMsg}}</span>
|
||||
<br />controller:
|
||||
<span
|
||||
:class="{ noError: !controllerError, error: controllerError}"
|
||||
>{{controllerErrorMsg}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import odriveEnums from "../assets/odriveEnums.json";
|
||||
import clearErrors from "./clearErrors.vue";
|
||||
import { getVal, fetchParam } from "../lib/odrive_utils";
|
||||
|
||||
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: "CONTROLLER_ERROR_NONE",
|
||||
0x00000001: "CONTROLLER_ERROR_OVERSPEED",
|
||||
0x00000002: "CONTROLLER_ERROR_INVALID_INPUT_MODE",
|
||||
0x00000004: "CONTROLLER_ERROR_UNSTABLE_GAIN",
|
||||
0x00000008: "CONTROLLER_ERROR_INVALID_MIRROR_AXIS",
|
||||
0x00000010: "CONTROLLER_ERROR_INVALID_LOAD_ENCODER",
|
||||
0x00000020: "CONTROLLER_ERROR_INVALID_ESTIMATE",
|
||||
};
|
||||
|
||||
export default {
|
||||
name: "Axis",
|
||||
components: {
|
||||
clearErrors,
|
||||
},
|
||||
props: ["axis", "odrives"],
|
||||
data() {
|
||||
return {
|
||||
showError: false,
|
||||
axisErr: undefined,
|
||||
motorErr: undefined,
|
||||
controllerErr: undefined,
|
||||
encoderErr: undefined,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
axisErrorMsg() {
|
||||
let retMsg = "none";
|
||||
let errCode = this.axisErr;
|
||||
|
||||
if (this.$store.state.currentDash == "Wizard") {
|
||||
errCode &= ~odriveEnums.AXIS_ERROR_MOTOR_FAILED;
|
||||
errCode &= ~odriveEnums.AXIS_ERROR_ENCODER_FAILED;
|
||||
}
|
||||
|
||||
if (errCode != 0) {
|
||||
// we got an error!
|
||||
// mask errors depending on which page is active
|
||||
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 = this.motorErr;
|
||||
|
||||
if (this.$store.state.currentDash == "Wizard") {
|
||||
errCode &= ~odriveEnums.MOTOR_ERROR_PHASE_RESISTANCE_OUT_OF_RANGE;
|
||||
errCode &= ~odriveEnums.MOTOR_ERROR_PHASE_INDUCTANCE_OUT_OF_RANGE;
|
||||
}
|
||||
|
||||
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 = this.encoderErr;
|
||||
|
||||
if (this.$store.state.currentDash == "Wizard") {
|
||||
errCode &= ~odriveEnums.ENCODER_ERROR_CPR_POLEPAIRS_MISMATCH;
|
||||
errCode &= ~odriveEnums.ENCODER_ERROR_NO_RESPONSE;
|
||||
}
|
||||
|
||||
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 = this.controllerErr;
|
||||
|
||||
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() {
|
||||
let errCode = this.axisErr;
|
||||
if (this.$store.state.currentDash == "Wizard") {
|
||||
errCode &= ~odriveEnums.AXIS_ERROR_MOTOR_FAILED;
|
||||
errCode &= ~odriveEnums.AXIS_ERROR_ENCODER_FAILED;
|
||||
}
|
||||
return errCode != 0;
|
||||
},
|
||||
motorError() {
|
||||
let errCode = this.motorErr;
|
||||
if (this.$store.state.currentDash == "Wizard") {
|
||||
errCode &= ~odriveEnums.MOTOR_ERROR_PHASE_RESISTANCE_OUT_OF_RANGE;
|
||||
errCode &= ~odriveEnums.MOTOR_ERROR_PHASE_INDUCTANCE_OUT_OF_RANGE;
|
||||
}
|
||||
return errCode != 0;
|
||||
},
|
||||
encoderError() {
|
||||
let errCode = this.encoderErr;
|
||||
if (this.$store.state.currentDash == "Wizard") {
|
||||
errCode &= ~odriveEnums.ENCODER_ERROR_CPR_POLEPAIRS_MISMATCH;
|
||||
errCode &= ~odriveEnums.ENCODER_ERROR_NO_RESPONSE;
|
||||
}
|
||||
return errCode != 0;
|
||||
},
|
||||
controllerError() {
|
||||
let errCode = this.controllerErr;
|
||||
return errCode != 0;
|
||||
},
|
||||
error() {
|
||||
return this.axisError || this.motorError || this.encoderError || this.controllerError;
|
||||
},
|
||||
},
|
||||
created() {
|
||||
// set up timeout loop for grabbing axis error values
|
||||
let update = () => {
|
||||
fetchParam(this.axis + ".error");
|
||||
fetchParam(this.axis + '.motor.error');
|
||||
fetchParam(this.axis + '.controller.error');
|
||||
fetchParam(this.axis + '.encoder.error');
|
||||
this.axisErr = getVal(this.axis + '.error');
|
||||
this.motorErr = getVal(this.axis + '.motor.error');
|
||||
this.controllerErr = getVal(this.axis + '.controller.error');
|
||||
this.encoderErr = getVal(this.axis + '.encoder.error');
|
||||
setTimeout(update, 1000);
|
||||
}
|
||||
update();
|
||||
}
|
||||
};
|
||||
</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">{{shortPath}}:</span>
|
||||
<div class="right">
|
||||
<input v-on:change="newVal" :placeholder="initVal"/>
|
||||
<button class="action-button close-button" @click="putVal">Send</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { putVal } from "../../lib/odrive_utils.js";
|
||||
|
||||
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;
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
shortPath() {
|
||||
let keys = this.path.split('.');
|
||||
keys.shift();
|
||||
return keys.join('.');
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
newVal: function (e) {
|
||||
let input = e.target.value;
|
||||
let allowedChars = "0123456789eE/*-+.()";
|
||||
let send = true;
|
||||
for (const c of input) {
|
||||
if (!allowedChars.includes(c)) {
|
||||
send = false;
|
||||
}
|
||||
}
|
||||
if (send) {
|
||||
this.value = eval(input);
|
||||
console.log("input = " + input + ", val = " + this.value);
|
||||
}
|
||||
this.$store.commit("setActionVal", {dashID: this.dashID, actionID: this.id, val: this.value});
|
||||
},
|
||||
putVal: function () {
|
||||
let keys = this.path.split(".");
|
||||
keys.shift();
|
||||
console.log('path = ' + keys.join('.') + ", val = " + this.value);
|
||||
putVal(keys.join('.'), this.value);
|
||||
},
|
||||
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,95 @@
|
||||
<template>
|
||||
<div class="card action-card">
|
||||
<button class="close-button" @click=deleteAction>X</button>
|
||||
<span class="ctrlName">{{shortPath}}:</span>
|
||||
<div class="right">
|
||||
<select class="action-enum" v-model="value" @change="newVal">
|
||||
<option v-for="option in options" :key="option.value" :value="option.value">
|
||||
{{ option.text }}
|
||||
</option>
|
||||
</select>
|
||||
<button class="action-button close-button" @click="putVal">Send</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { putVal } from "../../lib/odrive_utils.js";
|
||||
|
||||
export default {
|
||||
name: "ActionEnum",
|
||||
props: {
|
||||
id: String,
|
||||
path: String,
|
||||
initVal: Number,
|
||||
dashID: String,
|
||||
options: Array,
|
||||
},
|
||||
data: function () {
|
||||
return {
|
||||
value: 0,
|
||||
};
|
||||
},
|
||||
created() {
|
||||
if (this.initVal !== undefined) {
|
||||
this.value = this.initVal;
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
shortPath() {
|
||||
let keys = this.path.split('.');
|
||||
keys.shift();
|
||||
return keys.join('.');
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
newVal: function (e) {
|
||||
console.log(this.selected);
|
||||
this.value = parseFloat(e.target.value);
|
||||
this.$store.commit("setActionVal", {dashID: this.dashID, actionID: this.id, val: this.value});
|
||||
},
|
||||
putVal: function () {
|
||||
let keys = this.path.split(".");
|
||||
keys.shift();
|
||||
console.log('path = ' + keys.join('.') + ", val = " + this.value);
|
||||
putVal(keys.join('.'), this.value);
|
||||
},
|
||||
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;
|
||||
}
|
||||
|
||||
.action-enum {
|
||||
border: none;
|
||||
border-bottom: 1px solid grey;
|
||||
background-color: var(--fg-color);
|
||||
text-align-last: right;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,38 @@
|
||||
<template>
|
||||
<div>
|
||||
<button class="measure-button card" @click="clear">Clear Errors</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { putVal } from "../lib/odrive_utils.js";
|
||||
|
||||
export default {
|
||||
name: "clearErrors.vue",
|
||||
props: {
|
||||
data: Object,
|
||||
},
|
||||
methods: {
|
||||
clear() {
|
||||
// clear odrive errors
|
||||
// set them to 0?
|
||||
console.log(this.data);
|
||||
let paths = [
|
||||
this.data.axis + ".error",
|
||||
this.data.axis + ".motor.error",
|
||||
this.data.axis + ".encoder.error",
|
||||
this.data.axis + ".controller.error",
|
||||
];
|
||||
for (const path of paths) {
|
||||
putVal(path, 0);
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.measure-button:active {
|
||||
background-color: var(--bg-color);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,93 @@
|
||||
<template>
|
||||
<div class="card">
|
||||
<button class="close-button" @click=deleteCtrl>X</button>
|
||||
<span class="ctrlName">{{name}}:</span>
|
||||
<div class="right">
|
||||
<span class="ctrlVal" v-if="!writeAccess">{{value}}</span>
|
||||
<span class="ctrlVal" v-if="writeAccess">{{fakeValue}}</span>
|
||||
<input
|
||||
class="ctrlInput"
|
||||
v-if="writeAccess"
|
||||
type="checkbox"
|
||||
:value="value"
|
||||
@click="putVal"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getVal, getReadonly, putVal, fetchParam } from "../../lib/odrive_utils.js";
|
||||
|
||||
export default {
|
||||
name: "CtrlBoolean",
|
||||
//type checking here for properties
|
||||
props: {
|
||||
path: String,
|
||||
odrives: Object,
|
||||
dashID: String,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
fakeValue: undefined,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
value: function () {
|
||||
let keys = this.path.split(".");
|
||||
keys.shift(); // don't need first key here
|
||||
return getVal(keys.join('.'));
|
||||
},
|
||||
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
|
||||
return getReadonly(keys.join('.')) == false;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
putVal: function (e) {
|
||||
let keys = this.path.split('.');
|
||||
keys.shift();
|
||||
putVal(keys.join('.'), e.target.checked);
|
||||
fetchParam(keys.join('.'));
|
||||
this.fakeValue = e.target.checked;
|
||||
},
|
||||
deleteCtrl: function() {
|
||||
// commit a mutation in the store with the relevant information
|
||||
this.$store.commit("removeCtrlFromDash", {dashID: this.dashID, path: this.path});
|
||||
}
|
||||
},
|
||||
created() {
|
||||
// update parameter value on component creation
|
||||
let keys = this.path.split('.');
|
||||
keys.shift();
|
||||
fetchParam(keys.join('.'));
|
||||
this.fakeValue = getVal(keys.join('.'));
|
||||
}
|
||||
};
|
||||
</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,107 @@
|
||||
<template>
|
||||
<div class="card">
|
||||
<button class="close-button" @click=deleteCtrl>X</button>
|
||||
<span class="ctrlName">{{name}}:</span>
|
||||
<div class="right">
|
||||
<select class="ctrl-enum" v-model="selected" @change="putVal">
|
||||
<option v-for="option in options" :key="option.value" :value="option.value">
|
||||
{{ option.text }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getVal, getReadonly, putVal, fetchParam } from "../../lib/odrive_utils.js";
|
||||
|
||||
export default {
|
||||
name: "CtrlNumeric",
|
||||
//type checking here for properties
|
||||
props: {
|
||||
path: String,
|
||||
odrives: Object,
|
||||
dashID: String,
|
||||
options: Array,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
selected: undefined,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
value: function () {
|
||||
let keys = this.path.split('.');
|
||||
keys.shift();
|
||||
return parseFloat(getVal(keys.join('.')));
|
||||
},
|
||||
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
|
||||
return getReadonly(keys.join('.')) === false;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
value: function(newVal) {
|
||||
this.selected = newVal;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
putVal: function (e) {
|
||||
let keys = this.path.split('.');
|
||||
keys.shift();
|
||||
console.log("Calling putVal from CtrlEnum");
|
||||
putVal(keys.join('.'), parseFloat(e.target.value));
|
||||
},
|
||||
deleteCtrl: function() {
|
||||
// commit a mutation in the store with the relevant information
|
||||
this.$store.commit("removeCtrlFromDash", {dashID: this.dashID, path: this.path});
|
||||
},
|
||||
},
|
||||
created() {
|
||||
// update parameter value on component creation
|
||||
let keys = this.path.split('.');
|
||||
keys.shift();
|
||||
fetchParam(keys.join('.'));
|
||||
|
||||
this.selected = this.value;
|
||||
console.log(this.value);
|
||||
}
|
||||
};
|
||||
</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;
|
||||
}
|
||||
|
||||
.ctrl-enum {
|
||||
border: none;
|
||||
border-bottom: 1px solid grey;
|
||||
background-color: var(--fg-color);
|
||||
text-align-last: right;
|
||||
}
|
||||
</style>
|
||||