This commit is contained in:
2025-05-13 01:34:53 +03:00
parent 427735e23d
commit 83f3f1c7d4
945 changed files with 633484 additions and 0 deletions
+571
View File
@@ -0,0 +1,571 @@
<template>
<div id="app">
<!-- HEADER -->
<div class="header" id="header">
<button class="dash-button menu-button" @click="toggleMenu">Menu</button>
<div class="card menu" id="menu" :style="{top: menuTop}" v-show="showMenu">
<button class="dash-button menu-item" @click="exportDash">Export dash</button>
<button class="dash-button menu-item" @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 class="dash-button menu-item" id="importButton" @click="calcImportLeft();calcImportTop();toggleImport()">Import ODrive config</button>
<button class="dash-button menu-item" id="exportButton" @click="calcExportLeft();calcExportTop();toggleExport()">Export ODrive config</button>
</div>
<div class="card import-menu" :style="{top: importTop, left: importLeft}" v-show="showImport">
<button v-for="odrive in Object.keys(odrives)" :key="odrive" class="dash-button" @click="importConfigWrapper">
{{odrive}}
<input
type="file"
id="inputConfig"
ref="fileInputConfig"
@change="
importConfigFile($event.target, odrive);
"
onclick="this.value=null"
value
style="display: none"
/>
</button>
</div>
<div class="card export-menu" :style="{top: exportTop, left: exportLeft}" v-show="showExport">
<button v-for="odrive in Object.keys(odrives)" :key="odrive" class="dash-button" @click="exportConfig(odrive)">{{odrive}}</button>
</div>
<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>
</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>
</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";
import { pathsFromTree } from "./lib/utils.js";
import { getVal, putVal } from "./lib/odrive_utils.js";
export default {
name: "App",
components: {
Start,
Dashboard,
Axis,
Wizard,
},
data: function () {
return {
//currentDash: "Start",
showMenu: false,
showImport: false,
showExport: false,
menuTop: '0px',
importTop: '0px',
importLeft: '0px',
exportTop: '0px',
exportLeft: '0px',
};
},
mounted() {
var elem = document.getElementById('header');
this.menuTop = elem.offsetHeight + 'px';
},
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;
},
samplingText: function () {
let ret;
if (this.$store.state.sampling) {
ret = "stop sampling";
} else {
ret = "start sampling";
}
return ret;
},
},
methods: {
toggleMenu() {
this.showMenu = !this.showMenu;
if (!this.showMenu) {
this.showImport = false;
this.showExport = false;
}
},
toggleImport() {
this.showImport = !this.showImport;
this.showExport = false;
},
toggleExport() {
this.showExport = !this.showExport;
this.showImport= false;
},
importConfigWrapper() {
const inputElem = document.getElementById("inputConfig");
if (inputElem) {
console.log("importing config");
inputElem.click();
}
},
importConfigFile(event, odrive) {
console.log("Importing config to odrive " + odrive);
let file = event.files[0];
const reader = new FileReader();
// this is ugly, but it gets around scoping problems the "load" callback
let importConfig = (config) => {
let paths = pathsFromTree(config);
for (const path of paths) {
let val = config;
for(const key of path.split('.')) {
val = val[key];
}
putVal(odrive + '.' + path, val);
}
console.log("Config imported to odrive " + odrive);
};
reader.addEventListener("load", function (e) {
let importString = e.target.result;
importString = importString.replace(/ Infinity/g, '" Infinity"');
importString = importString.replace(/ -Infinity/g, '" -Infinity"');
importConfig(JSON.parse(importString));
});
reader.readAsText(file);
},
exportConfig(odrive) {
console.log("Exporting config from odrive " + odrive);
let configPaths = [];
let exportConfig = {};
let paramTree = this.$store.state.odriveConfigs.params[odrive];
for (const path of pathsFromTree(paramTree)) {
if (path.includes('config.') && !path.includes('mapping')) {
configPaths.push(path);
}
}
for (const path of configPaths) {
let keys = path.split('.');
let val = String(getVal(odrive + '.' + path));
// super ugly! Need a better way to do this and handle tuples in odrive_utils
if (val.includes('True')) {
val = true;
}
else if (val.includes('False')) {
val = false;
}
else if (val.includes('.')) {
val = parseFloat(val);
}
else if (!val.includes(',') && !val.includes('Infinity')){
val = parseInt(val);
}
let obj = exportConfig;
for (const key of keys.slice(0,-1)) {
if (!Object.keys(obj).includes(key)) {
obj[key] = {}
}
obj = obj[key];
}
obj[keys.pop()] = val;
}
let exportString = JSON.stringify(exportConfig, null, 2);
if (exportString.includes('"Infinity"')) {
console.log("found infinity!");
}
console.log(typeof exportString);
exportString = exportString.replace(/"Infinity"/g, 'Infinity');
exportString = exportString.replace(/"-Infinity"/g, '-Infinity');
const blob = new Blob([exportString], {
type: "application/json",
});
saveAs(blob, odrive + '-config.json');
},
calcImportLeft() {
let elem = document.getElementById('menu');
this.importLeft = elem.offsetWidth + 'px';
},
calcImportTop() {
this.importTop = document.getElementById('importButton').getBoundingClientRect().top + 'px';
},
calcExportLeft() {
let elem = document.getElementById('menu')
this.exportLeft = elem.offsetWidth + 'px';
},
calcExportTop() {
this.exportTop = document.getElementById('exportButton').getBoundingClientRect().top + 'px';
},
changeDash(dashName) {
console.log(dashName);
this.$store.commit("setDash", dashName);
},
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);
}
.menu-item {
text-align: left;
}
.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;
}
.menu-button {
border-right: 1px solid grey;
}
.menu {
position: absolute;
margin: 0;
padding: 0;
left: 0;
display: flex;
flex-direction: column;
}
.import-menu {
position: absolute;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
}
.export-menu {
position: absolute;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
}
</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": "Startup 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": "#1f77b4"
},
{
"path": "odrives.odrive0.axis0.encoder.pos_estimate",
"color": "#ff7f0e"
}
]
},
{
"name": "d9ee2474-3e53-408f-9eab-1656342eb531",
"vars": [
{
"path": "odrives.odrive0.axis0.controller.vel_setpoint",
"color": "#1f77b4"
},
{
"path": "odrives.odrive0.axis0.encoder.vel_estimate",
"color": "#ff7f0e"
}
]
}
]
}
@@ -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": "Startup 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": "#1f77b4"
},
{
"path": "odrives.odrive0.axis0.encoder.pos_estimate",
"color": "#ff7f0e"
}
]
},
{
"name": "d9ee2474-3e53-408f-9eab-1656342eb531",
"vars": [
{
"path": "odrives.odrive0.axis0.controller.vel_setpoint",
"color": "#1f77b4"
},
{
"path": "odrives.odrive0.axis0.encoder.vel_estimate",
"color": "#ff7f0e"
}
]
}
]
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 169 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 171 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 165 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

@@ -0,0 +1,700 @@
---
version: 0.0.1
ns: com.odriverobotics
summary: ODrive Interface Definitions
dictionary: [ODrive] # Prevent the word 'ODrive' from being detected as two words 'O' and 'Drive'
interfaces:
ODrive:
c_is_class: True
attributes:
vbus_voltage: readonly float32
ibus: readonly float32
serial_number: readonly uint64
hw_version_major: readonly uint8
hw_version_minor: readonly uint8
hw_version_variant: readonly uint8
fw_version_major: readonly uint8
fw_version_minor: readonly uint8
fw_version_revision: readonly uint8
fw_version_unreleased:
type: readonly uint8
doc: 0 for official releases, 1 otherwise
brake_resistor_armed: readonly bool
brake_resistor_saturated: bool
system_stats:
c_is_class: False
attributes:
uptime: readonly uint32
min_heap_space: readonly uint32
min_stack_space_axis0: readonly uint32
min_stack_space_axis1: readonly uint32
min_stack_space_comms: readonly uint32
min_stack_space_usb: readonly uint32
min_stack_space_uart: readonly uint32
min_stack_space_can: readonly uint32
min_stack_space_usb_irq: readonly uint32
min_stack_space_startup: readonly uint32
stack_usage_axis0: readonly uint32
stack_usage_axis1: readonly uint32
stack_usage_comms: readonly uint32
stack_usage_usb: readonly uint32
stack_usage_uart: readonly uint32
stack_usage_usb_irq: readonly uint32
stack_usage_startup: readonly uint32
stack_usage_can: readonly uint32
usb:
c_is_class: False
attributes:
rx_cnt: readonly uint32
tx_cnt: readonly uint32
tx_overrun_cnt: readonly uint32
i2c:
c_is_class: False
attributes:
addr: readonly uint8
addr_match_cnt: readonly uint32
rx_cnt: readonly uint32
error_cnt: readonly uint32
config:
c_is_class: False
attributes:
enable_uart:
type: bool
doc: 'TODO: changing this currently requires a reboot - fix this'
uart_baudrate:
type: uint32
doc: "Defines the baudrate used on the UART interface.
Some baudrates will have a small timing error due to hardware limitations.
Here's an (incomplete) list of baudrates for ODrive v3.x:
Configured | Actual | Error [%]
-------------|---------------|-----------
1.2 KBps | 1.2 KBps | 0
2.4 KBps | 2.4 KBps | 0
9.6 KBps | 9.6 KBps | 0
19.2 KBps | 19.195 KBps | 0.02
38.4 KBps | 38.391 KBps | 0.02
57.6 KBps | 57.613 KBps | 0.02
115.2 KBps | 115.068 KBps | 0.11
230.4 KBps | 230.769 KBps | 0.16
460.8 KBps | 461.538 KBps | 0.16
921.6 KBps | 913.043 KBps | 0.93
1.792 MBps | 1.826 MBps | 1.9
1.8432 MBps | 1.826 MBps | 0.93
For more information refer to Section 30.3.4 and Table 142 (the column with f_PCLK = 42 MHz) in the STM datasheet:
https://www.st.com/content/ccc/resource/technical/document/reference_manual/3d/6d/5a/66/b4/99/40/d4/DM00031020.pdf/files/DM00031020.pdf/jcr:content/translations/en.DM00031020.pdf"
enable_i2c_instead_of_can:
type: bool
doc: 'Changing this requires a reboot'
enable_ascii_protocol_on_usb: bool
max_regen_current: float32
brake_resistance:
type: float32
unit: Ohm
doc: Value of the brake resistor connected to the ODrive. Set to 0 to disable.
dc_bus_undervoltage_trip_level:
type: float32
unit: V
doc: Minimum voltage below which the motor stops operating.
dc_bus_overvoltage_trip_level:
type: float32
unit: V
doc: Maximum voltage above which the motor stops operating.
This protects against cases in which the power supply fails to dissipate
the brake power if the brake resistor is disabled.
The default is 26V for the 24V board version and 52V for the 48V board version.
enable_dc_bus_overvoltage_ramp:
type: bool
doc: 'If enabled, if the measured DC voltage exceeds `dc_bus_overvoltage_ramp_start`,
the ODrive will sink more power than usual into the the brake resistor
in an attempt to bring the voltage down again.
The brake duty cycle is increased by the following amount:
vbus_voltage == dc_bus_overvoltage_ramp_start => brake_duty_cycle += 0%
vbus_voltage == dc_bus_overvoltage_ramp_end => brake_duty_cycle += 100%
Remarks:
- This feature is active even when all motors are disarmed.
- This feature is disabled if `brake_resistance` is non-positive.'
dc_bus_overvoltage_ramp_start:
type: float32
doc: See `enable_dc_bus_overvoltage_ramp`.
Do not set this lower than your usual vbus_voltage,
unless you like fried brake resistors.
dc_bus_overvoltage_ramp_end:
type: float32
doc: See `enable_dc_bus_overvoltage_ramp`.
Must be larger than `dc_bus_overvoltage_ramp_start`,
otherwise the ramp feature is disabled.
dc_max_positive_current:
type: float32
unit: A
doc: Max current the power supply can source.
dc_max_negative_current:
type: float32
unit: A
doc: Max current the power supply can sink. You most likely want a non-positive value here. Set to -INFINITY to disable.
gpio1_pwm_mapping: {type: Endpoint, c_name: 'pwm_mappings[0]'} # TODO: disable for ODrive v3.2 and older
gpio2_pwm_mapping: {type: Endpoint, c_name: 'pwm_mappings[1]'} # TODO: disable for ODrive v3.2 and older
gpio3_pwm_mapping: {type: Endpoint, c_name: 'pwm_mappings[2]'} # TODO: disable for ODrive v3.2 and older
gpio4_pwm_mapping: {type: Endpoint, c_name: 'pwm_mappings[3]'}
gpio3_analog_mapping: {type: Endpoint, c_name: 'analog_mappings[2]'}
gpio4_analog_mapping: {type: Endpoint, c_name: 'analog_mappings[3]'}
user_config_loaded: readonly bool
axis0: {type: Axis, c_name: get_axis(0)}
axis1: {type: Axis, c_name: get_axis(1)}
can: {type: Can, c_name: get_can()}
test_property: uint32
functions:
test_function: {in: {delta: int32}, out: {cnt: int32}}
get_oscilloscope_val: {in: {index: uint32}, out: {val: float32}}
get_adc_voltage: {in: {gpio: uint32}, out: {voltage: float32}}
save_configuration:
erase_configuration:
reboot:
enter_dfu_mode:
ODrive.Can:
c_is_class: True
attributes:
error:
nullflag: None
flags: {DuplicateCanIds: }
config:
c_is_class: False
attributes:
baud_rate: readonly uint32
protocol: Protocol
functions:
set_baud_rate: {in: {baudRate: uint32}}
ODrive.Endpoint:
c_is_class: False
attributes:
endpoint: endpoint_ref
min: float32
max: float32
ODrive.Axis:
c_is_class: True
attributes:
error:
nullflag: 'None'
flags:
InvalidState:
doc: An invalid state was requested.
DcBusUnderVoltage:
DcBusOverVoltage:
CurrentMeasurementTimeout:
BrakeResistorDisarmed:
doc: The brake resistor was unexpectedly disarmed.
MotorDisarmed:
doc: The motor was unexpectedly disarmed.
MotorFailed:
doc: Check `motor.error` for more information.
SensorlessEstimatorFailed:
EncoderFailed:
doc: Check `encoder.error` for more information.
ControllerFailed:
PosCtrlDuringSensorless:
doc: DEPRECATED
WatchdogTimerExpired:
MinEndstopPressed:
MaxEndstopPressed:
EstopRequested:
HomingWithoutEndstop:
bit: 17
doc: the min endstop was not enabled during homing
step_dir_active: readonly bool
current_state: readonly AxisState
requested_state: AxisState
loop_counter: readonly uint32
lockin_state:
typeargs: {fibre.Property.mode: readonly}
values:
Inactive:
Ramp:
Accelerate:
ConstVel:
is_homed: {type: bool, c_name: homing_.is_homed}
config:
c_is_class: False
attributes:
startup_motor_calibration:
type: bool
doc: run motor calibration at startup, skip otherwise
startup_encoder_index_search:
type: bool
doc: run encoder index search after startup, skip otherwise this only has an effect if encoder.config.use_index is also true
startup_encoder_offset_calibration:
type: bool
doc: run encoder offset calibration after startup, skip otherwise
startup_closed_loop_control:
type: bool
doc: enable closed loop control after calibration/startup
startup_sensorless_control:
type: bool
doc: enable sensorless control after calibration/startup
startup_homing:
type: bool
doc: enable homing after calibration/startup
enable_step_dir:
type: bool
doc: Enable step/dir input after calibration.
For M0 this has no effect if `enable_uart` is true.
step_dir_always_on:
type: bool
doc: Keep step/dir enabled while the motor is disabled.
This is ignored if enable_step_dir is false.
This setting only takes effect on a state transition
into idle or out of closed loop control.
counts_per_step: float32
watchdog_timeout:
type: float32
unit: s
doc: 0 disables watchdog
enable_watchdog: bool
step_gpio_pin: {type: uint16, c_setter: 'set_step_gpio_pin'}
dir_gpio_pin: {type: uint16, c_setter: 'set_dir_gpio_pin'}
calibration_lockin: # TODO: this is a subset of lockin state
c_is_class: False
attributes:
current: float32
ramp_time: float32
ramp_distance: float32
accel: float32
vel: float32
sensorless_ramp: LockinConfig
general_lockin: LockinConfig
can_node_id:
type: uint32
doc: Both axes will have the same id to start
can_node_id_extended: bool
can_heartbeat_rate_ms: uint32
motor: Motor
controller: Controller
encoder: Encoder
sensorless_estimator: SensorlessEstimator
trap_traj: TrapezoidalTrajectory
min_endstop: Endstop
max_endstop: Endstop
functions:
watchdog_feed:
doc: Feed the watchdog to prevent watchdog timeouts.
clear_errors:
doc: Check the watchdog timer for expiration. Also sets the watchdog error bit if expired.
ODrive.Axis.LockinConfig:
c_is_class: False
attributes:
current:
type: float32
unit: A
ramp_time:
type: float32
unit: s
ramp_distance:
type: float32
unit: rad
accel:
type: float32
unit: rad/s^2
vel:
type: float32
unit: rad/s
finish_distance:
type: float32
unit: rad
finish_on_vel: bool
finish_on_distance: bool
finish_on_enc_idx: bool
ODrive.Motor:
c_is_class: True
attributes:
error:
nullflag: None
flags:
PhaseResistanceOutOfRange:
PhaseInductanceOutOfRange:
AdcFailed:
DrvFault:
ControlDeadlineMissed:
NotImplementedMotorType:
BrakeCurrentOutOfRange:
ModulationMagnitude:
BrakeDeadtimeViolation:
UnexpectedTimerCallback:
CurrentSenseSaturation:
InverterOverTemp:
CurrentLimitViolation:
BrakeDutyCycleNan:
DcBusOverRegenCurrent: {doc: too much current pushed into the power supply}
DcBusOverCurrent: {doc: too much current pulled out of the power supply}
armed_state:
typeargs: {fibre.Property.mode: readonly}
values:
Disarmed:
WaitingForTimings:
WaitingForUpdate:
Armed:
is_calibrated: readonly bool
current_meas_phB: {type: readonly float32, c_name: current_meas_.phB}
current_meas_phC: {type: readonly float32, c_name: current_meas_.phC}
DC_calib_phB: {type: float32, c_name: DC_calib_.phB}
DC_calib_phC: {type: float32, c_name: DC_calib_.phC}
phase_current_rev_gain: float32
thermal_current_lim: readonly float32
inverter_temp:
type: readonly float32
unit: °C
doc: NaN while the ODrive is initializing.
current_control:
c_is_class: False
attributes:
p_gain: float32
i_gain: float32
v_current_control_integral_d: float32
v_current_control_integral_q: float32
Ibus: float32
final_v_alpha: float32
final_v_beta: float32
Id_setpoint: float32
Iq_setpoint: readonly float32
Iq_measured: float32
Id_measured: float32
I_measured_report_filter_k: float32
max_allowed_current: readonly float32
overcurrent_trip_level: readonly float32
acim_rotor_flux: float32
async_phase_vel: readonly float32
async_phase_offset: float32
gate_driver:
c_name: gate_driver_exported_
c_is_class: False
attributes:
drv_fault:
typeargs: {fibre.Property.mode: readonly}
nullflag: NoFault
flags:
FetLowCOvercurrent: {bit: 0, doc: FET Low side, Phase C Over Current fault}
FetHighCOvercurrent: {bit: 1, doc: FET High side, Phase C Over Current fault}
FetLowBOvercurrent: {bit: 2, doc: FET Low side, Phase B Over Current fault}
FetHighBOvercurrent: {bit: 3, doc: FET High side, Phase B Over Current fault}
FetLowAOvercurrent: {bit: 4, doc: FET Low side, Phase A Over Current fault}
FetHighAOvercurrent: {bit: 5, doc: FET High side, Phase A Over Current fault}
OvertemperatureWarning: {bit: 6, doc: Over Temperature Warning fault}
OvertemperatureShutdown: {bit: 7, doc: Over Temperature Shut Down fault}
PVddUndervoltage: {bit: 8, doc: Power supply Vdd Under Voltage fault}
GVddUndervoltage: {bit: 9, doc: DRV8301 Vdd Under Voltage fault}
GVddOvervoltage: {bit: 10, doc: DRV8301 Vdd Over Voltage fault}
# status_reg_1: readonly uint32
# status_reg_2: readonly uint32
# ctrl_reg_1: readonly uint32
# ctrl_reg_2: readonly uint32
timing_log:
c_is_class: False
attributes:
general: {type: readonly uint16, c_name: 'get(TIMING_LOG_GENERAL)'}
adc_cb_i: {type: readonly uint16, c_name: 'get(TIMING_LOG_ADC_CB_I)'}
adc_cb_dc: {type: readonly uint16, c_name: 'get(TIMING_LOG_ADC_CB_DC)'}
meas_r: {type: readonly uint16, c_name: 'get(TIMING_LOG_MEAS_R)'}
meas_l: {type: readonly uint16, c_name: 'get(TIMING_LOG_MEAS_L)'}
enc_calib: {type: readonly uint16, c_name: 'get(TIMING_LOG_ENC_CALIB)'}
idx_search: {type: readonly uint16, c_name: 'get(TIMING_LOG_IDX_SEARCH)'}
foc_voltage: {type: readonly uint16, c_name: 'get(TIMING_LOG_FOC_VOLTAGE)'}
foc_current: {type: readonly uint16, c_name: 'get(TIMING_LOG_FOC_CURRENT)'}
spi_start: {type: readonly uint16, c_name: 'get(TIMING_LOG_SPI_START)'}
sample_now: {type: readonly uint16, c_name: 'get(TIMING_LOG_SAMPLE_NOW)'}
spi_end: {type: readonly uint16, c_name: 'get(TIMING_LOG_SPI_END)'}
config:
c_is_class: False
attributes:
pre_calibrated: {type: bool, c_setter: set_pre_calibrated}
pole_pairs: int32
calibration_current: float32
resistance_calib_max_voltage: float32
phase_inductance: {type: float32, c_setter: set_phase_inductance}
phase_resistance: {type: float32, c_setter: set_phase_resistance}
direction: int32
motor_type: MotorType
current_lim: float32
current_lim_margin: float32
inverter_temp_limit_lower: float32
inverter_temp_limit_upper: float32
requested_current_range: float32
current_control_bandwidth: {type: float32, c_setter: set_current_control_bandwidth}
acim_slip_velocity: float32
acim_gain_min_flux: float32
acim_autoflux_min_Id: float32
acim_autoflux_enable: bool
acim_autoflux_attack_gain: float32
acim_autoflux_decay_gain: float32
ODrive.Controller:
c_is_class: True
attributes:
error:
nullflag: None
flags:
Overspeed:
InvalidInputMode:
UnstableGain:
InvalidMirrorAxis:
InvalidLoadEncoder:
InvalidEstimate:
input_pos: {type: float32, c_setter: set_input_pos}
input_vel: float32
input_current: float32
pos_setpoint: readonly float32
vel_setpoint: readonly float32
current_setpoint: readonly float32
trajectory_done: readonly bool
vel_integrator_current: float32
anticogging_valid: bool
config:
c_is_class: False
attributes:
gain_scheduling_width: float32
enable_vel_limit: bool
enable_current_mode_vel_limit:
type: bool
doc: Enable velocity limit in current control mode (requires a valid velocity estimator).
enable_gain_scheduling: bool
enable_overspeed_error: bool
control_mode: ControlMode
input_mode: InputMode
pos_gain:
type: float32
unit: (counts/s) / counts
vel_gain:
type: float32
unit: 'A/(counts/s) (or A/(rad/s) in sensorless mode'
vel_integrator_gain:
type: float32
unit: A/(counts/s * s)
vel_limit:
type: float32
unit: counts/s
doc: Infinity to disable.
vel_limit_tolerance:
type: float32
doc: Ratio to `vel_limit`. Infinity to disable.
vel_ramp_rate: float32
current_ramp_rate:
type: float32
unit: A / sec
homing_speed:
type: float32
unit: counts/s
inertia:
type: float32
unit: A/(count/s^2)
axis_to_mirror: uint8
mirror_ratio: float32
load_encoder_axis:
type: uint8
# TODO: this is meaningless for a user. Should there be a separate developer note?
doc: Default depends on Axis number and is set in load_configuration()
input_filter_bandwidth:
type: float32
unit: 1/s
c_setter: set_input_filter_bandwidth
anticogging:
c_is_class: False
attributes:
index: readonly uint32
pre_calibrated: bool
calib_anticogging: readonly bool
calib_pos_threshold: float32
calib_vel_threshold: float32
cogging_ratio: readonly float32
anticogging_enabled: bool
functions:
move_incremental: {in: {displacement: float32, from_input_pos: bool}}
start_anticogging_calibration:
ODrive.Encoder:
c_is_class: True
attributes:
error:
nullflag: None
flags:
UnstableGain:
CprPolepairsMismatch:
NoResponse:
UnsupportedEncoderMode:
IllegalHallState:
IndexNotFoundYet:
AbsSpiTimeout:
AbsSpiComFail:
AbsSpiNotReady:
is_ready: readonly bool
index_found: readonly bool
shadow_count: readonly int32
count_in_cpr: readonly int32
interpolation: readonly float32
phase: readonly float32
pos_estimate: readonly float32
pos_cpr: readonly float32
hall_state: readonly uint8
vel_estimate: readonly float32
calib_scan_response: readonly float32
pos_abs: int32
spi_error_rate: readonly float32
config:
c_is_class: False
attributes:
mode: Mode
use_index: {type: bool, c_setter: set_use_index}
find_idx_on_lockin_only: {type: bool, c_setter: set_find_idx_on_lockin_only}
abs_spi_cs_gpio_pin: {type: uint16, c_setter: set_abs_spi_cs_gpio_pin}
zero_count_on_find_idx: bool
cpr: int32
offset: int32
pre_calibrated: {type: bool, c_setter: set_pre_calibrated}
offset_float: float32
enable_phase_interpolation: bool
bandwidth: {type: float32, c_setter: set_bandwidth}
calib_range: float32
calib_scan_distance: float32
calib_scan_omega: float32
idx_search_unidirectional: bool
ignore_illegal_hall_state: bool
sincos_gpio_pin_sin: uint16
sincos_gpio_pin_cos: uint16
functions:
set_linear_count: {in: {count: int32}}
ODrive.SensorlessEstimator:
c_is_class: True
attributes:
error:
nullflag: None
flags:
UnstableGain:
phase: float32
pll_pos: float32
vel_estimate: float32
# pll_kp: float32
# pll_ki: float32
config:
c_is_class: False
attributes:
observer_gain: float32
pll_bandwidth: float32
pm_flux_linkage: float32
ODrive.TrapezoidalTrajectory:
c_is_class: True
attributes:
config:
c_is_class: False
attributes:
vel_limit: float32
accel_limit: float32
decel_limit: float32
ODrive.Endstop:
c_is_class: True
attributes:
endstop_state: readonly bool
config:
c_is_class: False
attributes:
gpio_num: {type: uint16, c_setter: set_gpio_num}
enabled: {type: bool, c_setter: set_enabled}
offset: float32
is_active_high: bool
pullup: bool
debounce_ms: {type: uint32, c_setter: set_debounce_ms}
valuetypes:
ODrive.Can.Protocol:
values: {Simple: }
ODrive.Axis.AxisState: # TODO: remove redundant "Axis" in name
values:
Undefined:
doc: will fall through to idle
Idle:
doc: disable PWM and do nothing
StartupSequence:
doc: the actual sequence is defined by the config.startup... flags
FullCalibrationSequence:
doc: run all calibration procedures, then idle
MotorCalibration:
doc: run motor calibration
SensorlessControl:
doc: run sensorless control
EncoderIndexSearch:
doc: run encoder index search
EncoderOffsetCalibration:
doc: run encoder offset calibration
ClosedLoopControl:
doc: run closed loop control
LockinSpin:
doc: run lockin spin
EncoderDirFind:
Homing:
doc: run axis homing function
ODrive.Encoder.Mode:
values:
Incremental:
Hall:
Sincos:
SpiAbsCui:
value: 0x100
doc: compatible with CUI AMT23xx
SpiAbsAms:
value: 0x101
doc: compatible with AMS AS5047P, AS5048A/AS5048B (no daisy chain support)
SpiAbsAeat:
value: 0x102
doc: not yet implemented
ODrive.Controller.ControlMode:
values:
# Note: these should be sorted from lowest level of control to
# highest level of control, to allow "<" style comparisons.
VoltageControl:
CurrentControl:
VelocityControl:
PositionControl:
ODrive.Controller.InputMode:
values:
Inactive:
Passthrough:
VelRamp:
PosFilter:
MixChannels:
TrapTraj:
CurrentRamp:
Mirror:
ODrive.Motor.MotorType:
values:
HighCurrent:
#LowCurrent: # not implemented
Gimbal: {value: 2}
Acim:
@@ -0,0 +1,131 @@
{
"GPIO_MODE_DIGITAL": 0,
"GPIO_MODE_DIGITAL_PULL_UP": 1,
"GPIO_MODE_DIGITAL_PULL_DOWN": 2,
"GPIO_MODE_ANALOG_IN": 3,
"GPIO_MODE_UART_A": 4,
"GPIO_MODE_UART_B": 5,
"GPIO_MODE_UART_C": 6,
"GPIO_MODE_CAN_A": 7,
"GPIO_MODE_I2C_A": 8,
"GPIO_MODE_SPI_A": 9,
"GPIO_MODE_PWM": 10,
"GPIO_MODE_ENC0": 11,
"GPIO_MODE_ENC1": 12,
"GPIO_MODE_ENC2": 13,
"GPIO_MODE_MECH_BRAKE": 14,
"GPIO_MODE_STATUS": 15,
"STREAM_PROTOCOL_TYPE_FIBRE": 0,
"STREAM_PROTOCOL_TYPE_ASCII": 1,
"STREAM_PROTOCOL_TYPE_STDOUT": 2,
"STREAM_PROTOCOL_TYPE_ASCII_AND_STDOUT": 3,
"PROTOCOL_SIMPLE": 1,
"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_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,
"AXIS_STATE_ENCODER_HALL_POLARITY_CALIBRATION": 12,
"AXIS_STATE_ENCODER_HALL_PHASE_CALIBRATION": 13,
"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,
"ENCODER_MODE_SPI_ABS_MA732": 260,
"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,
"INPUT_MODE_TUNING": 8,
"MOTOR_TYPE_HIGH_CURRENT": 0,
"MOTOR_TYPE_GIMBAL": 2,
"MOTOR_TYPE_ACIM": 3,
"ODRIVE_ERROR_NONE": 0,
"ODRIVE_ERROR_CONTROL_ITERATION_MISSED": 1,
"ODRIVE_ERROR_DC_BUS_UNDER_VOLTAGE": 2,
"ODRIVE_ERROR_DC_BUS_OVER_VOLTAGE": 4,
"ODRIVE_ERROR_DC_BUS_OVER_REGEN_CURRENT": 8,
"ODRIVE_ERROR_DC_BUS_OVER_CURRENT": 16,
"ODRIVE_ERROR_BRAKE_DEADTIME_VIOLATION": 32,
"ODRIVE_ERROR_BRAKE_DUTY_CYCLE_NAN": 64,
"ODRIVE_ERROR_INVALID_BRAKE_RESISTANCE": 128,
"CAN_ERROR_NONE": 0,
"CAN_ERROR_DUPLICATE_CAN_IDS": 1,
"AXIS_ERROR_NONE": 0,
"AXIS_ERROR_INVALID_STATE": 1,
"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,
"AXIS_ERROR_UNKNOWN_POSITION": 524288,
"MOTOR_ERROR_NONE": 0,
"MOTOR_ERROR_PHASE_RESISTANCE_OUT_OF_RANGE": 1,
"MOTOR_ERROR_PHASE_INDUCTANCE_OUT_OF_RANGE": 2,
"MOTOR_ERROR_DRV_FAULT": 8,
"MOTOR_ERROR_CONTROL_DEADLINE_MISSED": 16,
"MOTOR_ERROR_MODULATION_MAGNITUDE": 128,
"MOTOR_ERROR_CURRENT_SENSE_SATURATION": 1024,
"MOTOR_ERROR_CURRENT_LIMIT_VIOLATION": 4096,
"MOTOR_ERROR_MODULATION_IS_NAN": 65536,
"MOTOR_ERROR_MOTOR_THERMISTOR_OVER_TEMP": 131072,
"MOTOR_ERROR_FET_THERMISTOR_OVER_TEMP": 262144,
"MOTOR_ERROR_TIMER_UPDATE_MISSED": 524288,
"MOTOR_ERROR_CURRENT_MEASUREMENT_UNAVAILABLE": 1048576,
"MOTOR_ERROR_CONTROLLER_FAILED": 2097152,
"MOTOR_ERROR_I_BUS_OUT_OF_RANGE": 4194304,
"MOTOR_ERROR_BRAKE_RESISTOR_DISARMED": 8388608,
"MOTOR_ERROR_SYSTEM_LEVEL": 16777216,
"MOTOR_ERROR_BAD_TIMING": 33554432,
"MOTOR_ERROR_UNKNOWN_PHASE_ESTIMATE": 67108864,
"MOTOR_ERROR_UNKNOWN_PHASE_VEL": 134217728,
"MOTOR_ERROR_UNKNOWN_TORQUE": 268435456,
"MOTOR_ERROR_UNKNOWN_CURRENT_COMMAND": 536870912,
"MOTOR_ERROR_UNKNOWN_CURRENT_MEASUREMENT": 1073741824,
"MOTOR_ERROR_UNKNOWN_VBUS_VOLTAGE": 2147483648,
"MOTOR_ERROR_UNKNOWN_VOLTAGE_COMMAND": 4294967296,
"MOTOR_ERROR_UNKNOWN_GAINS": 8589934592,
"MOTOR_ERROR_CONTROLLER_INITIALIZING": 17179869184,
"MOTOR_ERROR_UNBALANCED_PHASES": 34359738368,
"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,
"CONTROLLER_ERROR_INVALID_CIRCULAR_RANGE": 64,
"CONTROLLER_ERROR_SPINOUT_DETECTED": 128,
"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,
"ENCODER_ERROR_HALL_NOT_CALIBRATED_YET": 512,
"SENSORLESS_ESTIMATOR_ERROR_NONE": 0,
"SENSORLESS_ESTIMATOR_ERROR_UNSTABLE_GAIN": 1,
"SENSORLESS_ESTIMATOR_ERROR_UNKNOWN_CURRENT_MEASUREMENT": 2
}
Binary file not shown.

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,77 @@
{
"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,
"calib_scan_distance": null,
"abs_spi_cs_gpio_pin": 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,
"calib_scan_distance": null,
"abs_spi_cs_gpio_pin": 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,929 @@
import odriveEnums from "../odriveEnums.json";
// wizard is an object of pages.
// each page has a number, title, vue component name, next destination, back destination, and array of choices
// certain choices require a hook - for example, hall effect encoder cpr is 6 * motor.config.pole_pairs
// each hook takes a config object, modifies it, and returns it after running appropriate calculations.
// see choiceHandler() in Wizard.vue for how this happens
// Pages can also have custom components displayed below the choice tiles
// Example - on motor pages, there are buttons to run motor calibration and clear errors.
export let pages = {
ODrive: {
title: "Which ODrive do you have?",
link: "ODrive Version",
component: "wizardPage",
next: "Brake",
back: "ODrive",
nextTooltip: "Make a choice!",
choiceMade: false,
choices: [
{
component: "wizardChoice",
data: {
imageURL: require("../images/24v_300x300.png"),
},
title: "ODrive v3.6 24V",
requirements: [],
hooks: [],
tooltip: null,
altTooltip: null,
},
{
component: "wizardChoice",
data: {
imageURL: require("../images/56v_300x300.png"),
},
title: "ODrive v3.6 56V",
requirements: [],
hooks: [],
tooltip: null,
altTooltip: null,
},
],
customComponents: [],
pageComponents: [],
},
Brake: {
title: "Brake Resistor",
link: "Brake Resistor",
component: "wizardPage",
next: "Motor_0",
back: "ODrive",
nextTooltip: "Make a choice!",
choiceMade: false,
choices: [
{
component: "wizardBrake",
data: {},
title: "Brake Resistor",
requirements: [],
hooks: [],
tooltip: "When you slow down a motor, the energy has to go somewhere.\n\r \
It either goes to the brake resistor or back to your power supply.",
altTooltip: null,
},
{
component: "wizardChoice",
data: {
imageURL: null,
configStub: {
config: {
brake_resistance: 0,
}
},
},
title: "I don't have a brake resistor",
hooks: [],
requirements: [],
tooltip: "Only operate without a brake resistor if your power source can handle reverse current, like a battery."
}
],
},
Motor_0: {
title: "Which motor are you using on Axis 0?",
link: "Motor 0",
component: "wizardPage",
next: "Encoder_0",
back: "Brake",
nextTooltip: "ODrive needs to know the inductance and resistance of your motor in order to control it.\
Click the Calibrate Motor button to start the measurement process. The motor will beep\
during calibration.",
choiceMade: false,
choices: [
{
component: "wizardChoice",
data: {
imageURL: require("../images/D5065_300x300.png"),
configStub: {
axis0: {
motor: {
config: {
pole_pairs: 7,
torque_constant: 8.27 / 270,
// R and L come from "Calibrate Motor" button on page
//phase_resistance: 0.039, // from test rig
//phase_inductance: 1.57e-5,
motor_type: odriveEnums.MOTOR_TYPE_HIGH_CURRENT, // MOTOR_TYPE_HIGH_CURRENT
},
},
},
},
},
title: "ODrive D5065",
requirements: [],
hooks: [],
tooltip: "D5065 motor from the ODrive shop",
altTooltip: null,
},
{
component: "wizardChoice",
data: {
imageURL: require("../images/D6374_300x300.png"),
configStub: {
axis0: {
motor: {
config: {
pole_pairs: 7,
torque_constant: 8.27 / 150,
// R and L come from "Calibrate Motor" button on page
//phase_resistance: 0.041, // measurement from PJ
//phase_inductance: 2.23e-5,
motor_type: odriveEnums.MOTOR_TYPE_HIGH_CURRENT, // MOTOR_TYPE_HIGH_CURRENT,
},
},
},
},
},
title: "ODrive D6374",
requirements: [],
hooks: [],
tooltip: "D6374 motor from the ODrive shop",
altTooltip: null,
},
{
component: "wizardMotor",
data: {
axis: "axis0",
},
title: "Other Motor",
requirements: [],
hooks: [],
tooltip: "Bring your own motor!",
altTooltip: null,
},
],
pageComponents: [
{
component: "wizardMotorCal",
id: 0,
data: {
axis: "axis0",
}
},
{
component: "wizardCalStatus",
id: 1,
data: {
success: "Calibration succeeded!",
failure: "Calibration failed",
tooltip: "Double check your motor connections. If the connections are good, it is \
possible that your motor has a higher resistance or lower inductance than ODrive expects."
}
}
]
},
Encoder_0: {
title: "Which encoder are you using for Axis 0?",
link: "Encoder 0",
component: "wizardPage",
next: "Control_0",
back: "Motor_0",
nextTooltip: "ODrive needs to know the relation between the encoder position and motor position in order to function. \
Click the Calibrate Encoder button to start the measurement process. The motor will slowly rotate back \
and forth during calibration.",
choiceMade: false,
choices: [
{
component: "wizardChoice",
data: {
imageURL: require("../images/amt102-v_300x300.png"),
configStub: {
axis0: {
encoder: {
config: {
cpr: 8192,
use_index: true,
mode: odriveEnums.ENCODER_MODE_INCREMENTAL, // ENCODER_MODE_INCREMENTAL
}
}
}
},
},
title: "CUI AMT102V",
requirements: [],
hooks: [],
tooltip: "Incremental 8192cpr encoder from the ODrive shop",
altTooltip: null,
},
{
component: "wizardChoice",
data: {
imageURL: require("../images/hall_effect_300x300.png"),
configStub: {
axis0: {
encoder: {
config: {
use_index: false,
mode: odriveEnums.ENCODER_MODE_HALL,
}
}
}
},
},
title: "Hall Effect",
requirements: [],
hooks: [
// hook takes a config object (normally this.wizardConfig in Wizard.vue), creates and returns a copy
(configObj) => {
let newConfig = configObj;
newConfig.axis0.encoder.config.cpr = 6 * newConfig.axis0.motor.config.pole_pairs;
return newConfig;
},
],
tooltip: "If your motor is 'sensored' and you don't have another encoder, select this option",
altTooltip: null,
},
{
component: "wizardEncoderIncremental",
data: {
axis: "axis0",
},
title: "Incremental Without Index",
requirements: [],
hooks: [],
tooltip: "Generic incremental encoder without index. Set cpr to 4 * PPR (pulses per revolution).",
altTooltip: null,
},
{
component: "wizardEncoderIncrementalIndex",
data: {
axis: "axis0",
},
title: "Incremental With Index",
requirements: [],
hooks: [],
tooltip: "Generic incremental encoder with index. Set cpr to 4 * PPR (pulses per revolution).",
altTooltip: null,
},
],
pageComponents: [
{
component: "wizardEncoderCal",
id: 0,
data: {
axis: "axis0",
}
},
{
component: "wizardCalStatus",
id: 1,
data: {
success: "Calibration succeeded!",
failure: "Calibration failed",
tooltip: "Double check your encoder connections. If the connections are good, you \
have a wrong setting for motor pole pairs or encoder cpr."
}
}
]
},
Control_0: {
title: "Control mode for Axis 0",
link: "Control Modes 0",
component: "wizardPage",
next: "Misc_0",
back: "Encoder_0",
nextTooltip: "Make a choice!",
choiceMade: false,
choices: [
{
component: "wizardChoice",
data: {
imageURL: require("../images/temp.png"),
configStub: {
axis0: {
controller: {
config: {
control_mode: odriveEnums.CONTROL_MODE_POSITION_CONTROL,
},
},
},
},
},
title: "Position Control",
requirements: [],
hooks: [],
tooltip: "Use this mode to control the position of the motor",
altTooltip: null,
},
{
component: "wizardChoice",
data: {
imageURL: require("../images/temp.png"),
configStub: {
axis0: {
controller: {
config: {
control_mode: odriveEnums.CONTROL_MODE_VELOCITY_CONTROL,
},
},
},
},
},
title: "Velocity Control",
requirements: [],
hooks: [],
tooltip: "Use this mode to control the velocity of the motor",
altTooltip: null,
},
{
component: "wizardChoice",
data: {
imageURL: require("../images/temp.png"),
configStub: {
axis0: {
controller: {
config: {
control_mode: odriveEnums.CONTROL_MODE_TORQUE_CONTROL,
},
},
},
},
},
title: "Torque Control",
requirements: [],
hooks: [],
tooltip: "Use this mode to control the torque output of the motor",
altTooltip: null,
},
{
component: "wizardChoice",
data: {
imageURL: require("../images/temp.png"),
configStub: {
axis0: {
controller: {
config: {
control_mode: odriveEnums.CONTROL_MODE_VOLTAGE_CONTROL,
},
},
},
},
},
title: "Voltage Control",
requirements: [],
hooks: [],
tooltip: "This mode is used for gimbal motors",
altTooltip: null,
},
],
customComponents: [],
pageComponents: [],
},
/*
Input_0: {
title: "Input mode selection for Axis 0",
link: "Input Modes 0",
component: "wizardPage",
next: "Misc_0",
back: "Control_0",
nextTooltip: "Make a choice!",
choiceMade: false,
choices: [
{
component: "wizardChoice",
data: {
imageURL: require("../images/temp.png"),
configStub: {
axis0: {
controller: {
config: {
input_mode: odriveEnums.INPUT_MODE_PASSTHROUGH,
},
},
},
},
},
title: "Input Passthrough",
requirements: [],
hooks: [],
tooltip: "This is the default input mode. Input commands are passed to the controller without modification.",
altTooltip: null,
},
{
component: "wizardInputFiltered",
data: {
axis: "axis0",
},
title: "Position Input Filtering",
requirements: [
(configObj) => {
return configObj.axis0.controller.config.control_mode == odriveEnums.CONTROL_MODE_POSITION_CONTROL;
},
],
hooks: [],
tooltip: "Filtered position mode. This provides smoother motion than the passthrough mode. \
Input position commands are sent through a 2nd order filter. A higher bandwidth will cause quicker position changes.",
altTooltip: "Control mode must be Position Control for this input mode",
},
{
component: "wizardInputVelRamp",
data: {
axis: "axis0",
},
title: "Velocity Input Ramping",
requirements: [
(configObj) => {
return configObj.axis0.controller.config.control_mode == odriveEnums.CONTROL_MODE_VELOCITY_CONTROL;
},
],
hooks: [],
tooltip: "For velocity control mode, you can choose to have the input velocities ramped between values. vel_ramp_rate is the \
acceleration between velocities.",
altTooltip: "Control mode must be Velocity Control for this input mode",
},
{
component: "wizardInputTrajectory",
data: {
axis: "axis0",
},
title: "Trajectory Planning",
requirements: [
(configObj) => {
return configObj.axis0.controller.config.control_mode == odriveEnums.CONTROL_MODE_POSITION_CONTROL;
},
],
hooks: [],
tooltip: "This mode lets you smoothly accelerate, coast, and decelerate from one position to another. vel_limit is your coasting speed, \
accel_limit is the maximum acceleration in turns/s^2, decel_limit is the maximum decelaration in turns/s^2 and inertia is an \
optional value to correlate acceleration with motor torque.",
altTooltip: "Control mode must be Position Control for this input mode",
}
],
pageComponents: [],
},
*/
Misc_0: {
title: "Finishing touches for Axis 0",
link: "Misc 0",
component: "wizardPage",
next: "Motor_1",
back: "Control_0",
nextTooltip: "Make a choice!",
choiceMade: false,
choices: [
{
component: "wizardMisc",
data: {
axis: "axis0",
},
title: "Miscellaneous Parameters",
requirements: [],
hooks: [],
tooltip: null,
altTooltip: null,
},
],
pageComponents: [],
},
Motor_1: {
title: "Which motor are you using for Axis 1?",
link: "Motor 1",
component: "wizardPage",
next: "Encoder_1",
back: "Misc_0",
nextTooltip: "ODrive needs to know the inductance and resistance of your motor in order to control it.\
Click the Calibrate Motor button to start the measurement process. The motor will beep\
during calibration.",
choiceMade: false,
choices: [
{
component: "wizardChoice",
data: {
imageURL: require("../images/D5065_300x300.png"),
configStub: {
axis1: {
motor: {
config: {
pole_pairs: 7,
torque_constant: 8.27 / 270,
// R and L come from "Calibrate Motor" button on page
//phase_resistance: 0.039, // from test rig
//phase_inductance: 1.57e-5,
motor_type: odriveEnums.MOTOR_TYPE_HIGH_CURRENT, // MOTOR_TYPE_HIGH_CURRENT
},
},
},
},
},
title: "ODrive D5065",
requirements: [],
hooks: [],
tooltip: "D5065 motor from the ODrive shop",
altTooltip: null,
},
{
component: "wizardChoice",
data: {
imageURL: require("../images/D6374_300x300.png"),
configStub: {
axis1: {
motor: {
config: {
pole_pairs: 7,
torque_constant: 8.27 / 150,
// R and L come from "Calibrate Motor" button on page
//phase_resistance: 0.041, // measurement from PJ
//phase_inductance: 2.23e-5,
motor_type: odriveEnums.MOTOR_TYPE_HIGH_CURRENT, // MOTOR_TYPE_HIGH_CURRENT,
},
},
},
},
},
title: "ODrive D6374",
requirements: [],
hooks: [],
tooltip: "D6374 motor from the ODrive shop",
altTooltip: null,
},
{
component: "wizardMotor",
data: {
axis: "axis1",
},
title: "Other Motor",
requirements: [],
hooks: [],
tooltip: "Bring your own motor!",
altTooltip: null,
},
],
pageComponents: [
{
component: "wizardMotorCal",
id: 0,
data: {
axis: "axis1",
}
},
{
component: "wizardCalStatus",
id: 1,
data: {
success: "Calibration succeeded!",
failure: "Calibration failed",
tooltip: "Double check your motor connections. If the connections are good, it is \
possible that your motor has a higher resistance or lower inductance than ODrive expects."
}
}
]
},
Encoder_1: {
title: "Which encoder are you using for Axis 1?",
link: "Encoder 1",
component: "wizardPage",
next: "Control_1",
back: "Motor_1",
nextTooltip: "ODrive needs to know the relation between the encoder position and motor position in order to function. \
Click the Calibrate Encoder button to start the measurement process. The motor will slowly rotate back \
and forth during calibration.",
choiceMade: false,
choices: [
{
component: "wizardChoice",
data: {
imageURL: require("../images/amt102-v_300x300.png"),
configStub: {
axis1: {
encoder: {
config: {
cpr: 8192,
use_index: true,
mode: odriveEnums.ENCODER_MODE_INCREMENTAL, // ENCODER_MODE_INCREMENTAL
}
}
}
},
},
title: "CUI AMT102V",
requirements: [],
hooks: [],
tooltip: "Incremental 8192cpr encoder from the ODrive shop",
altTooltip: null,
},
{
component: "wizardChoice",
data: {
imageURL: require("../images/hall_effect_300x300.png"),
configStub: {
axis1: {
encoder: {
config: {
use_index: false,
mode: odriveEnums.ENCODER_MODE_HALL,
}
}
}
},
},
title: "Hall Effect",
requirements: [],
hooks: [
// hook takes a config object (normally this.wizardConfig in Wizard.vue), creates and returns a copy
(configObj) => {
let newConfig = configObj;
newConfig.axis1.encoder.config.cpr = 6 * newConfig.axis1.motor.config.pole_pairs;
return newConfig;
},
],
tooltip: "If your motor is 'sensored' and you don't have another encoder, select this option",
altTooltip: null,
},
{
component: "wizardEncoderIncremental",
data: {
axis: "axis1",
},
title: "Incremental Without Index",
requirements: [],
hooks: [],
tooltip: "Generic incremental encoder without index. Set cpr to 4 * PPR (pulses per revolution).",
altTooltip: null,
},
{
component: "wizardEncoderIncrementalIndex",
data: {
axis: "axis1",
},
title: "Incremental With Index",
requirements: [],
hooks: [],
tooltip: "Generic incremental encoder with index. Set cpr to 4 * PPR (pulses per revolution).",
altTooltip: null,
},
],
pageComponents: [
{
component: "wizardEncoderCal",
id: 0,
data: {
axis: "axis1",
}
},
{
component: "wizardCalStatus",
id: 1,
data: {
success: "Calibration succeeded!",
failure: "Calibration failed",
tooltip: "Double check your encoder connections. If the connections are good, you either \
have a wrong setting for motor pole pairs or encoder cpr."
}
}
]
},
Control_1: {
title: "Control mode for Axis 1",
link: "Control Modes 1",
component: "wizardPage",
next: "Misc_1",
back: "Encoder_1",
nextTooltip: "Make a choice!",
choiceMade: false,
choices: [
{
component: "wizardChoice",
data: {
imageURL: require("../images/temp.png"),
configStub: {
axis1: {
controller: {
config: {
control_mode: odriveEnums.CONTROL_MODE_POSITION_CONTROL,
},
},
},
},
},
title: "Position Control",
requirements: [],
hooks: [],
tooltip: "Use this mode to control the position of the motor",
altTooltip: null,
},
{
component: "wizardChoice",
data: {
imageURL: require("../images/temp.png"),
configStub: {
axis1: {
controller: {
config: {
control_mode: odriveEnums.CONTROL_MODE_VELOCITY_CONTROL,
},
},
},
},
},
title: "Velocity Control",
requirements: [],
hooks: [],
tooltip: "Use this mode to control the velocity of the motor",
altTooltip: null,
},
{
component: "wizardChoice",
data: {
imageURL: require("../images/temp.png"),
configStub: {
axis1: {
controller: {
config: {
control_mode: odriveEnums.CONTROL_MODE_TORQUE_CONTROL,
},
},
},
},
},
title: "Torque Control",
requirements: [],
hooks: [],
tooltip: "Use this mode to control the torque output of the motor",
altTooltip: null,
},
{
component: "wizardChoice",
data: {
imageURL: require("../images/temp.png"),
configStub: {
axis1: {
controller: {
config: {
control_mode: odriveEnums.CONTROL_MODE_VOLTAGE_CONTROL,
},
},
},
},
},
title: "Voltage Control",
requirements: [],
hooks: [],
tooltip: "This mode is used for gimbal motors",
altTooltip: null,
},
],
customComponents: [],
pageComponents: [],
},
/*
Input_1: {
title: "Input mode selection for Axis 1",
link: "Input Modes 1",
component: "wizardPage",
next: "Misc_1",
back: "Control_1",
nextTooltip: "Make a choice!",
choiceMade: false,
choices: [
{
component: "wizardChoice",
data: {
imageURL: require("../images/temp.png"),
configStub: {
axis1: {
controller: {
config: {
input_mode: odriveEnums.INPUT_MODE_PASSTHROUGH,
},
},
},
},
},
title: "Input Passthrough",
requirements: [],
hooks: [],
tooltip: "This is the default input mode. Input commands are passed to the controller without modification.",
altTooltip: null,
},
{
component: "wizardInputFiltered",
data: {
axis: "axis1",
},
title: "Position Input Filtering",
requirements: [
(configObj) => {
return configObj.axis1.controller.config.control_mode == odriveEnums.CONTROL_MODE_POSITION_CONTROL;
},
],
hooks: [],
tooltip: "Filtered position mode. This provides smoother motion than the passthrough mode. \
Input position commands are sent through a 2nd order filter. A higher bandwidth will cause quicker position changes.",
altTooltip: "Control mode must be Position Control for this input mode",
},
{
component: "wizardInputVelRamp",
data: {
axis: "axis1",
},
title: "Velocity Input Ramping",
requirements: [
(configObj) => {
return configObj.axis1.controller.config.control_mode == odriveEnums.CONTROL_MODE_VELOCITY_CONTROL;
},
],
hooks: [],
tooltip: "For velocity control mode, you can choose to have the input velocities ramped between values. vel_ramp_rate is the \
acceleration between velocities.",
altTooltip: "Control mode must be Velocity Control for this input mode",
},
{
component: "wizardInputTrajectory",
data: {
axis: "axis1",
},
title: "Trajectory Planning",
requirements: [
(configObj) => {
return configObj.axis1.controller.config.control_mode == odriveEnums.CONTROL_MODE_POSITION_CONTROL;
},
],
hooks: [],
tooltip: "This mode lets you smoothly accelerate, coast, and decelerate from one position to another. vel_limit is your coasting speed, \
accel_limit is the maximum acceleration in turns/s^2, decel_limit is the maximum decelaration in turns/s^2 and inertia is an \
optional value to correlate acceleration with motor torque.",
altTooltip: "Control most must be Position Control for this input mode",
}
],
pageComponents: [],
},
*/
Misc_1: {
title: "Finishing touches for Axis 1",
link: "Misc 1",
component: "wizardPage",
next: "End",
back: "Control_1",
nextTooltip: "Make a choice!",
choiceMade: false,
choices: [
{
component: "wizardMisc",
data: {
axis: "axis1",
},
title: "Miscellaneous Parameters",
requirements: [],
hooks: [],
tooltip: null,
altTooltip: null,
},
],
pageComponents: [],
},
End: {
title: "Review choices",
link: "Review and Apply",
component: "wizardPage",
next: "End",
back: "Misc_1",
choiceMade: false,
choices: [
{
component: "wizardEnd",
data: {},
title: null,
requirements: [],
hooks: [],
tooltip: "Old config values are on the left, new config values are on the right in green",
altTooltip: null,
}
],
pageComponents: [],
}
}
export let enumVars = {
// for wizardEnd.vue
motor_type: {
0: "MOTOR_TYPE_HIGH_CURRENT",
2: "MOTOR_TYPE_GIMBAL",
3: "MOTOR_TYPE_ACIM",
},
mode: {
0: "ENCODER_MODE_INCREMENTAL",
1: "ENCODER_MODE_HALL",
2: "ENCODER_MODE_SINCOS",
256: "ENCODER_MODE_SPI_ABS_CUI",
257: "ENCODER_MODE_SPI_ABS_AMS",
258: "ENCODER_MODE_SPI_ABS_AEAT",
259: "ENCODER_MODE_SPI_ABS_RLS",
},
input_mode: {
0: "INPUT_MODE_INACTIVE",
1: "INPUT_MODE_PASSTHROUGH",
2: "INPUT_MODE_VEL_RAMP",
3: "INPUT_MODE_POS_FILTER",
4: "INPUT_MODE_MIX_CHANNELS",
5: "INPUT_MODE_TRAP_TRAJ",
6: "INPUT_MODE_TORQUE_RAMP",
7: "INPUT_MODE_MIRROR",
},
control_mode: {
0: "CONTROL_MODE_VOLTAGE_CONTROL",
1: "CONTROL_MODE_TORQUE_CONTROL",
2: "CONTROL_MODE_VELOCITY_CONTROL",
3: "CONTROL_MODE_POSITION_CONTROL",
},
}
+186
View File
@@ -0,0 +1,186 @@
'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() {
// call both 'python' and 'python3' to figure out the correct command
let spawnRet = spawnSync('python', ['-V']);
let success = spawnRet.status != null;
let outputString;
let cmd = '';
if (success) {
if (spawnRet.stdout.toString().length > 1) {
outputString = spawnRet.stdout.toString();
}
else {
outputString = spawnRet.stderr.toString();
}
if (outputString.includes("Python 3")) {
cmd = 'python';
}
}
if (cmd == '') {
spawnRet = spawnSync('python3', ['-V']);
success = spawnRet.status != null;
if (success) {
if (spawnRet.stdout.toString().length > 1) {
outputString = spawnRet.stdout.toString();
}
else {
outputString = spawnRet.stderr.toString();
}
if (outputString.includes("Python 3")) {
cmd = 'python3';
}
}
}
return cmd;
}
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) {
try {
console.log(data.toString('utf8'));
} catch (error) {
console.log(error);
}
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,202 @@
<template>
<div
class="axis"
@click.self="showError = !showError"
:class="{ inactive: !connected, 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";
export default {
name: "Axis",
components: {
clearErrors,
},
props: ["axis", "odrives"],
data() {
return {
showError: false,
axisErr: undefined,
motorErr: undefined,
controllerErr: undefined,
encoderErr: undefined,
};
},
methods: {
getErrorString(errCode, errType) {
let retMsg = "none";
if (errCode != 0) {
// we got an error!
// mask errors depending on which page is active
let errs = [];
for (const [name, value] of Object.entries(odriveEnums)) {
if (name.includes(errType) && errCode & value) {
errs.push(name);
}
}
retMsg = errs.join(", ");
}
return retMsg;
},
},
computed: {
connected() {
return this.$store.state.ODrivesConnected[this.axis.split(".")[0]];
},
axisErrorMsg() {
let errCode = this.axisErr;
if (this.$store.state.currentDash == "Wizard") {
errCode &= ~odriveEnums.AXIS_ERROR_MOTOR_FAILED;
errCode &= ~odriveEnums.AXIS_ERROR_ENCODER_FAILED;
}
return this.getErrorString(errCode, 'AXIS_ERROR');
},
motorErrorMsg() {
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 this.getErrorString(errCode, 'MOTOR_ERROR');
},
encoderErrorMsg() {
let errCode = this.encoderErr;
if (this.$store.state.currentDash == "Wizard") {
errCode &= ~odriveEnums.ENCODER_ERROR_CPR_POLEPAIRS_MISMATCH;
errCode &= ~odriveEnums.ENCODER_ERROR_NO_RESPONSE;
}
return this.getErrorString(errCode, 'ENCODER_ERROR');
},
controllerErrorMsg() {
let errCode = this.controllerErr;
return this.getErrorString(errCode, 'CONTROLLER_ERROR');
},
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 = () => {
// Do we have an active connection to the ODrive that contains this axis?
if (this.$store.state.ODrivesConnected[this.axis.split(".")[0]]) {
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");
}
};
this.cyclicUpdate = setInterval(update, 1000);
},
beforeDestroy() {
clearInterval(this.cyclicUpdate);
}
};
</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;
}
.inactive {
color: grey;
}
</style>
@@ -0,0 +1,101 @@
<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" :value="valueDisplay" spellcheck="false"/>
<button class="action-button close-button" @click="putVal">Send</button>
</div>
</div>
</template>
<script>
import { putVal, parseMath } from "../../lib/odrive_utils.js";
import { numberDisplay } from "../../lib/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('.');
},
valueDisplay() {
return numberDisplay(this.value);
}
},
methods: {
newVal: function (e) {
let val = parseMath(e.target.value);
if (val !== false) {
this.value = val;
console.log("input = " + e.target.value + ", 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,90 @@
<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"
:value="value"
:checked="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 {
}
},
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('.'));
},
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('.'));
}
};
</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>
@@ -0,0 +1,57 @@
<template>
<div class="card" @click.self="executeFunction">
<button class="close-button" @click=deleteCtrl>X</button>
<button class="execute" @click="executeFunction">{{name}}()</button>
</div>
</template>
<script>
import { callFcn } from "../../lib/odrive_utils.js";
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: {
executeFunction: function() {
//execute this function on the odrive
let keys = this.path.split(".");
keys.shift();
callFcn(keys.join('.'));
},
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,109 @@
<template>
<div class="card">
<button class="close-button" @click=deleteCtrl>X</button>
<span class="ctrlName">{{name}}:</span>
<div class="right">
<span v-if="!writeAccess" class="ctrlVal">{{value}}</span>
<input v-if="writeAccess" :placeholder="value" v-on:change="putVal" :value="value" spellcheck="false"/>
<!-- <span class="unit">[{{unit}}]</span> -->
</div>
</div>
</template>
<script>
import { getVal, getReadonly, putVal, fetchParam, getUnit, parseMath } from "../../lib/odrive_utils.js";
import { numberDisplay } from "../../lib/utils.js";
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();
let val = getVal(keys.join('.'));
return numberDisplay(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
return getReadonly(keys.join('.')) === false;
},
unit() {
let keys = this.path.split(".");
keys.shift();
return getUnit(this.$store.state.odrives.odrive0,keys.join('.'));
},
},
methods: {
putVal: function (e) {
let keys = this.path.split('.');
keys.shift();
console.log("input recieved: " + e.target.value);
let val = parseMath(e.target.value);
if (val !== false) {
putVal(keys.join('.'), val);
}
},
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('.'));
},
};
</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 */
}
.unit {
font-family: "Roboto Mono", monospace;
}
</style>
@@ -0,0 +1,151 @@
<template>
<div class="card">
<div>
<button class="close-button" @click=deleteCtrl>X</button>
<span class="ctrlName">{{name}}</span>
</div>
<div class="slider-container">
<input :value="displayMin" :placeholder="displayMin" v-on:change="setMin"/>
<!-- <vue-slider v-model="value" :min="min" :max="max" :interval="interval" /> -->
<vue-slider v-model="value" :data="data" :adsorb="true" @change="putVal"/>
<input :value="displayMax" :placeholder="displayMax" v-on:change="setMax" />
</div>
</div>
</template>
<script>
import VueSlider from "vue-slider-component";
import "vue-slider-component/theme/default.css";
import { getVal, putVal, parseMath } from "../../lib/odrive_utils.js";
import { numberDisplay } from "../../lib/utils.js";
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(".");
},
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);
},
displayMin() {
return numberDisplay(this.min);
},
displayMax() {
return numberDisplay(this.max);
}
},
methods: {
putVal: function (value) {
console.log(value);
let keys = this.path.split(".");
keys.shift();
putVal(keys.join('.'), value);
},
setMin: function (e) {
let min = parseMath(e.target.value);
if (min < this.max) {
this.min = min;
this.data = Array.from(Array(101), (_, i) => this.min + (this.max-this.min) / 100 * i);
this.value = this.findNearest(this.data, this.value);
}
},
setMax: function (e) {
let max = parseMath(e.target.value);
if (max > this.min) {
this.max = max;
this.data = Array.from(Array(101), (_, i) => this.min + (this.max-this.min) / 100 * i);
this.value = this.findNearest(this.data, this.value);
}
},
deleteCtrl: function() {
// commit a mutation in the store with the relevant information
this.$store.commit("removeCtrlFromDash", {dashID: this.dashID, path: this.path});
},
findNearest(data, value) {
// find item in data closest to value
let diff = Number.POSITIVE_INFINITY;
let retVal = value;
for (const val of data) {
if (Math.abs(val - value) < diff) {
diff = Math.abs(val - value);
retVal = val;
}
}
return retVal;
}
},
mounted() {
let initVal = () => {
let keys = this.path.split(".");
keys.shift(); // don't need first key here
return parseFloat(getVal(keys.join('.')));
};
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,167 @@
<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
},
events: [],
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.cyclicUpdate = setInterval(() => this.fillData(), 50);
},
beforeDestroy() {
clearInterval(this.cyclicUpdate);
},
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],
},
],
};
},
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,91 @@
<template>
<div class="card wizard-choice" :class="{ 'choice-inactive': !allowed }">
<div class="left">
<span>Brake resistor value = </span>
<input v-on:change="setBR" placeholder="Change Me!" :value="brake_resistance"/>
<span class="unit"> [{{ unit }}] </span>
</div>
</div>
</template>
<script>
import { getUnit, parseMath, getVal } from "../../../lib/odrive_utils.js";
export default {
name: "wizardBrake",
props: {
data: Object,
allowed: Boolean,
},
data: function () {
return {
brake_resistance: undefined,
};
},
computed: {
unit() {
// goal is to return "Ohms" from odriveUnits
let path = "odrive0.config.brake_resistance";
return getUnit(this.$store.state.odrives.odrive0, path);
},
value() {
let path = "odrive0.config.brake_resistance";
return getVal(path);
}
},
methods: {
setBR(e) {
console.log("from setBR " + e.target.value);
let val = parseMath(e.target.value);
if (val !== false) {
this.brake_resistance = val;
let configStub = undefined;
configStub = {
config: {
brake_resistance: this.brake_resistance,
},
};
this.$emit("choice", {
choice: "Brake Resistor",
configStub: configStub,
hooks: [],
});
}
},
},
};
</script>
<style scoped>
.wizard-choice {
display: flex;
flex-direction: column;
margin: 2rem;
}
.left {
margin-left: auto;
}
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 */
}
.unit {
font-family: "Roboto Mono", monospace;
}
</style>
@@ -0,0 +1,56 @@
<template>
<!-- single choice for a wizard page -->
<div class="wizard-choice card" :class="{'choice-inactive': !allowed}">
<img v-if="data.imageURL != null" :alt="title" :src="data.imageURL" />
{{ title }}
</div>
</template>
<script>
export default {
name: "wizardChoice",
props: {
data: Object,
selected: Boolean,
title: String,
hooks: Array,
allowed: Boolean,
},
data() {
return {
oldSelected: undefined,
}
},
watch: {
selected(newVal) {
if (newVal == true) {
// emit choice event
this.$emit('choice', {choice: this.title, configStub: this.data.configStub, hooks: this.hooks});
}
if (this.oldSelected == true && newVal == false){
console.log("emitting undo-choice");
this.$emit('undo-choice', {choice: this.title, configStub: this.data.configStub, hooks: this.hooks})
}
this.oldSelected = newVal;
}
}
};
</script>
<style scoped>
.wizard-choice {
text-align: center;
display: flex;
flex-direction: column;
margin: 2rem;
}
.inactive {
background-color: var(--bg-color);
}
img {
padding: 0px;
margin: auto;
}
</style>
@@ -0,0 +1,100 @@
<template>
<div class="card wizard-choice" :class="{ 'choice-inactive': !allowed }">
<div class="left">
<span>counts per revolution = </span>
<input v-on:change="setCPR" placeholder="Change Me!" :value="cpr" />
</div>
<span class="name">Incremental Encoder without Index</span>
</div>
</template>
<script>
import odriveEnums from "../../../assets/odriveEnums.json";
import { parseMath } from "../../../lib/odrive_utils.js";
export default {
name: "wizardEncoderIncremental",
props: {
data: Object,
allowed: Boolean,
},
data: function () {
return {
cpr_set: false,
cpr: undefined,
};
},
methods: {
setCPR(e) {
let val = parseMath(e.target.value);
if (val !== false) {
this.cpr = val;
let configStub = undefined;
if (this.data.axis == "axis0") {
configStub = {
axis0: {
encoder: {
config: {
cpr: this.cpr,
use_index: false,
mode: odriveEnums.ENCODER_MODE_INCREMENTAL,
},
},
},
};
} else if (this.data.axis == "axis1") {
configStub = {
axis1: {
encoder: {
config: {
cpr: this.cpr,
use_index: false,
mode: odriveEnums.ENCODER_MODE_INCREMENTAL,
},
},
},
};
}
this.$emit("choice", {
choice: "Incremental",
configStub: configStub,
hooks: [],
});
}
},
},
};
</script>
<style scoped>
.wizard-choice {
display: flex;
flex-direction: column;
margin: 2rem;
}
.left {
margin-left: auto;
}
.name {
margin-top: auto;
}
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,101 @@
<template>
<div class="card wizard-choice" :class="{ 'choice-inactive': !allowed }">
<div class="left">
<span>counts per revolution = </span>
<input v-on:change="setCPR" placeholder="Change Me!" :value="cpr" />
</div>
<span class="name">Incremental Encoder with Index</span>
</div>
</template>
<script>
import odriveEnums from "../../../assets/odriveEnums.json";
import { parseMath } from "../../../lib/odrive_utils.js";
export default {
name: "wizardEncoderIncremental",
props: {
data: Object,
allowed: Boolean,
},
data: function () {
return {
cpr_set: false,
cpr: undefined,
};
},
methods: {
setCPR(e) {
let val = parseMath(e.target.value);
if (val !== false) {
this.cpr = val;
let configStub = undefined;
if (this.data.axis == "axis0") {
configStub = {
axis0: {
encoder: {
config: {
cpr: this.cpr,
use_index: true,
mode: odriveEnums.ENCODER_MODE_INCREMENTAL,
},
},
},
};
} else if (this.data.axis == "axis1") {
configStub = {
axis1: {
encoder: {
config: {
cpr: this.cpr,
use_index: true,
mode: odriveEnums.ENCODER_MODE_INCREMENTAL,
},
},
},
};
}
this.$emit("choice", {
choice: "IncrementalIndex",
configStub: configStub,
hooks: [],
});
}
},
},
};
</script>
<style scoped>
.wizard-choice {
display: flex;
flex-direction: column;
margin: 2rem;
}
.left {
margin-left: auto;
}
.name {
margin-top: auto;
}
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,127 @@
<template>
<div class="card" :class="{'choice-inactive': !allowed}">
<div class="diff" v-for="diff in configDiffs" :key="diff.path">
<span class="diff-path">{{diff.path}}: </span>
<span class="diff-old">{{diffRepresentation(diff.path,diff.oldVal)}}</span>
<span class="diff-seperator"> ⮕ </span>
<span :class="{'diff-new': diff.oldVal != diff.newVal, 'diff-same': diff.oldVal == diff.newVal}">{{diffRepresentation(diff.path,diff.newVal)}}</span>
</div>
<button class="wizard-button card" @click="applyConfig">Apply</button>
</div>
</template>
<script>
import { enumVars } from "../../../assets/wizard/wizard.js";
import { putVal, fetchParam } from "../../../lib/odrive_utils.js"
export default {
name: "wizardEnd",
props: {
config: Object,
axis: String,
allowed: Boolean,
// actual odrive config is in this.$store (Vuex store)
},
data: function() {
return {
configDiffs: [], // array of objects {path, oldval, newval}
flatpaths: [],
path: [],
}
},
created() {
// flatten config tree into array of full variable paths
this.pathFromTree(this.config);
//for (const path of this.flatpaths){
// console.log(path);
//}
for (const path of this.flatpaths) {
let odrvObj = this.$store.state.odrives.odrive0;
let configObj = this.config;
for (const key of path.split('.')){
odrvObj = odrvObj[key];
configObj = configObj[key];
}
if (configObj != null) {
console.log("oldVal is " + odrvObj["val"] + " path is " + path);
if (Number.isInteger(parseFloat(odrvObj["val"]))){
this.configDiffs.push({path: path, oldVal: parseFloat(odrvObj["val"]), newVal: configObj});
}
else if (typeof configObj == 'boolean'){
this.configDiffs.push({path: path, oldVal: odrvObj["val"] == "True" || odrvObj["val"] == true, newVal: configObj});
}
else {
this.configDiffs.push({path: path, oldVal: parseFloat(odrvObj["val"]), newVal: configObj});
}
}
}
},
methods: {
pathFromTree(tree){
// array path
for (const key of Object.keys(tree)) {
this.path.push(key);
if (tree[key] != null && typeof tree[key] == "object") {
this.pathFromTree(tree[key]);
}
else {
this.flatpaths.push(this.path.join('.'));
}
this.path.pop();
}
},
applyConfig(){
// flatpaths will already be populated at this point
for (const diff of this.configDiffs) {
putVal("odrive0." + diff.path, diff.newVal);
fetchParam("odrive0." + diff.path);
console.log("applying " + diff.newVal + " to " + diff.path);
}
},
diffRepresentation(path, val) {
// for the diff view, check if the path indicates that a value is an Enum and return enum string
// otherwise, just the numeric value
let keys = path.split('.');
let retval;
if (Object.keys(enumVars).includes(keys[keys.length - 1])) {
retval = enumVars[keys[keys.length-1]][val];
}
else if (!Number.isInteger(val) && typeof val != 'boolean') {
retval = parseFloat(val).toExponential(3);
}
else {
retval = val;
}
return retval;
}
}
}
</script>
<style scoped>
.diff {
text-align: left;
}
.diff-path {
font-family: "Roboto Mono", monospace;
}
.diff-old {
font-weight: bold;
}
.diff-new {
font-weight: bold;
color: green;
}
.diff-same {
font-weight: bold;
color: blue;
}
.wizard-button:active {
background-color: var(--bg-color);
}
</style>
@@ -0,0 +1,117 @@
<template>
<div class="card wizard-motor-custom wizard-choice" :class="{'choice-inactive': !allowed}">
<div class="left">
<span>Input filter bandwidth =</span>
<input type="number" v-on:change="setBandwidth" :placeholder="bandwidth" />
<span>Hz</span>
</div>
<span class="name">Filtered Position Input Mode</span>
</div>
</template>
<script>
import odriveEnums from "../../../assets/odriveEnums.json";
import { getVal } from "../../../lib/odrive_utils.js"
export default {
name: "wizardInputFiltered",
props: {
data: Object,
allowed: Boolean,
},
data: function () {
return {
bandwidth: undefined,
};
},
created() {
let path = "odrive0." + this.data.axis + ".controller.config.input_filter_bandwidth";
this.bandwidth = parseFloat(getVal(path));
},
methods: {
sendConfig() {
console.log("emitting choice event from limits page");
let configStub = undefined;
if (this.data.axis == "axis0") {
configStub = {
axis0: {
controller: {
config: {
input_filter_bandwidth: this.bandwidth,
input_mode: odriveEnums.INPUT_MODE_POS_FILTER,
},
},
},
};
} else if (this.data.axis == "axis1") {
configStub = {
axis1: {
controller: {
config: {
input_filter_bandwidth: this.bandwidth,
input_mode: odriveEnums.INPUT_MODE_POS_FILTER,
},
},
},
};
}
this.$emit("choice", {
choice: "Input Filter " + this.data.axis,
configStub: configStub,
hooks: [],
});
},
setBandwidth(e) {
this.bandwidth = parseFloat(e.target.value);
this.sendConfig();
},
},
};
</script>
<style scoped>
.wizard-choice {
display: flex;
flex-direction: column;
margin: 2rem;
}
.left {
margin-left: auto;
}
.name {
margin-top: auto;
}
input {
width: 5rem;
font-family: inherit;
border-style: none;
border-bottom: 1px solid grey;
text-align: center;
background-color: transparent;
}
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 */
}
.measure-button {
margin-top: 2rem;
}
.measure-button:active {
background-color: var(--bg-color);
}
.name {
text-align: center;
}
</style>
@@ -0,0 +1,160 @@
<template>
<div class="card wizard-motor-custom wizard-choice" :class="{'choice-inactive': !allowed}">
<div class="left">
<span>Velocity limit =</span>
<input type="number" v-on:change="setVelLimit" :placeholder="vel_limit" />
<span>turns / s</span>
</div>
<div class="left">
<span>Acceleration limit =</span>
<input type="number" v-on:change="setAccelLimit" :placeholder="accel_limit" />
<span>turns / s^2</span>
</div>
<div class="left">
<span>Deceleration limit =</span>
<input type="number" v-on:change="setDecelLimit" :placeholder="decel_limit" />
<span>turns / s^2</span>
</div>
<div class="left">
<span>Inertia =</span>
<input type="number" v-on:change="setInertia" :placeholder="inertia" />
</div>
<span class="name">Trajectory Planner</span>
</div>
</template>
<script>
import { getVal } from "../../../lib/odrive_utils.js"
export default {
name: "wizardInputTrajectory",
props: {
data: Object,
allowed: Boolean,
},
data: function () {
return {
vel_limit: undefined,
accel_limit: undefined,
decel_limit: undefined,
inertia: undefined,
};
},
created() {
// get pre-existing values for trap_traj vel limit, accel, devel, and config.inertia
this.vel_limit = getVal("odrive0." + this.data.axis + ".trap_traj.config.vel_limit");
this.accel_limit = getVal("odrive0." + this.data.axis + ".trap_traj.config.accel_limit");
this.decel_limit = getVal("odrive0." + this.data.axis + ".trap_traj.config.decel_limit");
this.inertia = getVal("odrive0." + this.data.axis + ".controller.config.inertia");
},
methods: {
sendConfig() {
console.log("emitting choice event from limits page");
let configStub = undefined;
if (this.data.axis == "axis0") {
configStub = {
axis0: {
controller: {
config: {
inertia: this.inertia,
},
},
trap_traj: {
config: {
vel_limit: this.vel_limit,
accel_limit: this.accel_limit,
decel_limit: this.decel_limit,
},
},
},
};
} else if (this.data.axis == "axis1") {
configStub = {
axis1: {
controller: {
config: {
inertia: this.inertia,
},
},
trap_traj: {
config: {
vel_limit: this.vel_limit,
accel_limit: this.accel_limit,
decel_limit: this.decel_limit,
},
},
},
};
}
this.$emit("choice", {
choice: "Trapezoidal trajectory " + this.data.axis,
configStub: configStub,
hooks: [],
});
},
setVelLimit(e) {
this.vel_limit = parseFloat(e.target.value);
this.sendConfig();
},
setAccelLimit(e) {
this.accel_limit = parseFloat(e.target.value);
this.sendConfig();
},
setDecelLimit(e) {
this.decel_limit = parseFloat(e.target.value);
this.sendConfig();
},
setInertia(e) {
this.inertia = parseFloat(e.target.value);
this.sendConfig();
},
},
};
</script>
<style scoped>
.wizard-choice {
display: flex;
flex-direction: column;
margin: 2rem;
}
.left {
margin-left: auto;
}
.name {
margin-top: auto;
}
input {
width: 5rem;
font-family: inherit;
border-style: none;
border-bottom: 1px solid grey;
text-align: center;
background-color: transparent;
}
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 */
}
.measure-button {
margin-top: 2rem;
}
.measure-button:active {
background-color: var(--bg-color);
}
.name {
text-align: center;
}
</style>
@@ -0,0 +1,117 @@
<template>
<div class="card wizard-motor-custom wizard-choice" :class="{'choice-inactive': !allowed}">
<div class="left">
<span>Velocity ramp rate =</span>
<input type="number" v-on:change="setRampRate" :placeholder="vel_ramp_rate" />
<span>turns / s^2</span>
</div>
<span class="name">Ramped Velocity Input Mode</span>
</div>
</template>
<script>
import odriveEnums from "../../../assets/odriveEnums.json";
import { getVal } from "../../../lib/odrive_utils.js"
export default {
name: "wizardInputVelRamp",
props: {
data: Object,
allowed: Boolean,
},
data: function () {
return {
vel_ramp_rate: undefined,
};
},
created() {
let path = "odrive0." + this.data.axis + ".controller.config.vel_ramp_rate";
this.vel_ramp_rate = parseFloat(getVal(path));
},
methods: {
sendConfig() {
console.log("emitting choice event from limits page");
let configStub = undefined;
if (this.data.axis == "axis0") {
configStub = {
axis0: {
controller: {
config: {
vel_ramp_rate: this.vel_ramp_rate,
input_mode: odriveEnums.INPUT_MODE_VEL_RAMP,
},
},
},
};
} else if (this.data.axis == "axis1") {
configStub = {
axis1: {
controller: {
config: {
vel_ramp_rate: this.vel_ramp_rate,
input_mode: odriveEnums.INPUT_MODE_VEL_RAMP,
},
},
},
};
}
this.$emit("choice", {
choice: "Velocity ramp " + this.data.axis,
configStub: configStub,
hooks: [],
});
},
setRampRate(e) {
this.vel_ramp_rate = parseFloat(e.target.value);
this.sendConfig();
},
},
};
</script>
<style scoped>
.wizard-choice {
display: flex;
flex-direction: column;
margin: 2rem;
}
.left {
margin-left: auto;
}
.name {
margin-top: auto;
}
input {
width: 5rem;
font-family: inherit;
border-style: none;
border-bottom: 1px solid grey;
text-align: center;
background-color: transparent;
}
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 */
}
.measure-button {
margin-top: 2rem;
}
.measure-button:active {
background-color: var(--bg-color);
}
.name {
text-align: center;
}
</style>
@@ -0,0 +1,166 @@
<template>
<div
class="card wizard-motor-custom wizard-choice"
:class="{ 'choice-inactive': !allowed }"
>
<div class="left">
<span>Motor Velocity Limit =</span>
<input
v-on:change="setVelocityLimit"
:placeholder="velocityLimit"
:value="vel_limit"
/>
<span class="unit">[{{ velUnit }}]</span>
</div>
<div class="left">
<span>Motor Current Limit =</span>
<input
v-on:change="setCurrentLimit"
:placeholder="currentLimit"
:value="current_lim"
/>
<span class="unit">[Amps]</span>
</div>
</div>
</template>
<script>
import { getVal, getUnit, parseMath } from "../../../lib/odrive_utils.js";
export default {
name: "wizardMisc",
props: {
data: Object,
allowed: Boolean,
},
data: function () {
return {
current_lim: undefined,
vel_limit: undefined,
vel_set: false,
current_set: false,
};
},
computed: {
velocityLimit: function () {
let path = "odrive0." + this.data.axis + ".controller.config.vel_limit";
return parseFloat(getVal(path));
},
currentLimit: function () {
let path = "odrive0." + this.data.axis + ".motor.config.current_lim";
return parseFloat(getVal(path));
},
velUnit() {
let path = "odrive0." + this.data.axis + ".controller.config.vel_limit";
return getUnit(this.$store.state.odrives.odrive0, path);
},
},
methods: {
sendConfig() {
if (this.vel_set == true && this.current_set == true) {
console.log("emitting choice event from limits page");
let configStub = undefined;
if (this.data.axis == "axis0") {
configStub = {
axis0: {
controller: {
config: {
vel_limit: this.vel_limit,
},
},
motor: {
config: {
current_lim: this.current_lim,
},
},
},
};
} else if (this.data.axis == "axis1") {
configStub = {
axis1: {
controller: {
config: {
vel_limit: this.vel_limit,
},
},
motor: {
config: {
current_lim: this.current_lim,
},
},
},
};
}
this.$emit("choice", {
choice: "Misc " + this.data.axis,
configStub: configStub,
hooks: [],
});
}
},
setVelocityLimit(e) {
let val = parseMath(e.target.value);
if (val !== false) {
this.vel_limit = parseFloat(e.target.value);
this.vel_set = true;
this.sendConfig();
}
},
setCurrentLimit(e) {
let val = parseMath(e.target.value);
if (val !== false) {
this.current_lim = parseFloat(e.target.value);
this.current_set = true;
this.sendConfig();
}
},
},
};
</script>
<style scoped>
.wizard-choice {
display: flex;
flex-direction: column;
margin: 2rem;
}
.left {
margin-right: auto;
}
input {
width: 5rem;
font-family: inherit;
border-style: none;
border-bottom: 1px solid grey;
text-align: center;
background-color: transparent;
}
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 */
}
.measure-button {
margin-top: 2rem;
}
.measure-button:active {
background-color: var(--bg-color);
}
.name {
text-align: center;
}
.unit {
font-family: "Roboto Mono", monospace;
}
</style>
@@ -0,0 +1,198 @@
<template>
<div class="card wizard-motor-custom wizard-choice" :class="{'choice-inactive': !allowed}">
<div class="left">
<span>KV =</span>
<input type="number" v-on:change="setKV" placeholder="Change Me!" />
</div>
<div class="left">
<span>Pole Pairs =</span>
<input type="number" v-on:change="setPP" placeholder="Change Me!" />
</div>
<div class="left">
<span>Phase Resistance =</span>
<span>{{" " + resistance}}</span>
<span class="unit">[Ohms]</span>
</div>
<div class="left">
<span>Phase Inductance =</span>
<span>{{" " + inductance}}</span>
<span class="unit">[Henries]</span>
</div>
<span class="name">Other Motor</span>
</div>
</template>
<script>
import odriveEnums from "../../../assets/odriveEnums.json";
import { getVal } from "../../../lib/odrive_utils.js"
export default {
name: "wizardMotor",
props: {
data: Object,
axis: String,
allowed: Boolean,
},
data: function () {
return {
kv_set: false,
pp_set: false,
r_set: false,
l_set: false,
pole_pairs: undefined,
phase_resistance: undefined,
phase_inductance: undefined,
torque_constant: undefined,
};
},
computed: {
resistance: function () {
let path = "odrive0." + this.data.axis + ".motor.config.phase_resistance";
return parseFloat(getVal(path)).toExponential(3);
},
inductance: function () {
let path = "odrive0." + this.data.axis + ".motor.config.phase_inductance";
return parseFloat(getVal(path)).toExponential(3);
},
},
watch: {
resistance: function (newVal) {
console.log("from resistance watcher: " + newVal);
let path = "odrive0." + this.data.axis + ".motor.config.phase_resistance";
this.phase_resistance = parseFloat(getVal(path));
},
inductance: function (newVal) {
console.log("from inductance watcher: " + newVal);
let path = "odrive0." + this.data.axis + ".motor.config.phase_inductance";
this.phase_inductance = parseFloat(getVal(path));
},
pp_set: function () {
console.log("pp_set: " + this.pp_set);
this.sendConfig();
},
kv_set: function () {
console.log("kv_set: " + this.kv_set);
this.sendConfig();
},
r_set: function () {
console.log("r_set: " + this.r_set);
this.sendConfig();
},
l_set: function () {
console.log("l_set: " + this.l_set);
this.sendConfig();
},
},
methods: {
sendConfig() {
// if all of the set flags are true, emit an event
if (this.phase_resistance != 0) {
this.r_set = true;
} else {
this.r_set = false;
}
if (this.phase_inductance != 0) {
this.l_set = true;
} else {
this.l_set = false;
}
if (
this.r_set == true &&
this.l_set == true &&
this.pp_set == true &&
this.kv_set == true
) {
console.log("emitting choice event from other motor");
let configStub = undefined;
if (this.axis == "axis0") {
configStub = {
axis0: {
motor: {
config: {
pole_pairs: this.pole_pairs,
torque_constant: this.torque_constant,
phase_resistance: this.phase_resistance,
phase_inductance: this.phase_inductance,
type: odriveEnums.MOTOR_TYPE_HIGH_CURRENT,
},
},
},
};
} else if (this.axis == "axis1") {
configStub = {
axis1: {
motor: {
config: {
pole_pairs: this.pole_pairs,
torque_constant: this.torque_constant,
phase_resistance: this.phase_resistance,
phase_inductance: this.phase_inductance,
type: odriveEnums.MOTOR_TYPE_HIGH_CURRENT,
},
},
},
};
}
this.$emit("choice", {
choice: "Other motor",
configStub: configStub,
hooks: [],
});
}
},
setKV(e) {
this.torque_constant = 8.27 / parseFloat(e.target.value);
this.kv_set = true;
console.log(this.torque_constant);
},
setPP(e) {
this.pole_pairs = parseFloat(e.target.value);
this.pp_set = true;
console.log(this.pole_pairs);
},
},
};
</script>
<style scoped>
.wizard-choice {
display: flex;
flex-direction: column;
margin: 2rem;
}
.left {
margin-right: auto;
}
.name {
margin-top: auto;
}
input {
width: 5rem;
font-family: inherit;
border-style: none;
border-bottom: 1px solid grey;
text-align: center;
background-color: transparent;
}
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 */
}
.name {
text-align: center;
}
.unit {
font-family: "Roboto Mono", monospace;
}
</style>
@@ -0,0 +1,38 @@
<template>
<div class="cal-message"
v-tooltip.top="{
content: data.tooltip,
class: 'tooltip-custom tooltip-other-custom fade-in',
delay: 0,
visible: !calStatus,
}"
>{{message}}</div>
</template>
<script>
export default {
name: "wizardCalStatus",
props: {
data: Object,
calStatus: Boolean,
},
computed: {
message() {
let retmsg = "";
if (this.calStatus == true) {
retmsg = this.data.success;
} else if (this.calStatus == false) {
retmsg = this.data.failure;
}
return retmsg;
},
},
};
</script>
<style scoped>
.cal-message {
margin-top: auto;
margin-bottom: auto;
}
</style>
@@ -0,0 +1,35 @@
<template>
<div>
<button class="measure-button card" @click="calibrate">{{text}}</button>
</div>
</template>
<script>
export default {
name: "wizardEncoderCal",
props: {
data: Object,
calibrating: Boolean,
},
computed: {
text() {
return this.calibrating ? "Calibrating..." : "Calibrate Encoder";
}
},
methods: {
calibrate() {
// ask ODrive to measure resistance and inductance
this.$emit('page-comp-event', {data: "encoder calibration", axis: this.data.axis});
},
},
};
</script>
<style scoped>
.measure-button:active {
background-color: var(--bg-color);
}
</style>
@@ -0,0 +1,36 @@
<template>
<div>
<button class="measure-button card" @click="calibrate">{{text}}</button>
</div>
</template>
<script>
//import odriveEnums from "../../../assets/odriveEnums.json";
//import { putVal } from "../../../lib/odrive_utils.js"
export default {
name: "wizardMotorCal",
props: {
data: Object,
calibrating: Boolean,
},
computed: {
text() {
return this.calibrating ? "Calibrating..." : "Calibrate Motor";
}
},
methods: {
calibrate() {
this.$emit('page-comp-event', {data: "motor calibration", axis: this.data.axis});
},
},
};
</script>
<style scoped>
.measure-button:active {
background-color: var(--bg-color);
}
</style>
@@ -0,0 +1,190 @@
<template>
<!-- render a list of choices horizontally in cards -->
<div class="wizard-page card">
<div class="wizard-title">{{title}}</div>
<div class="wizard-choices">
<!--
<wizardChoice
v-on:click.native="selectedChoice=choice.text;$emit('choice', {title: title, choice: choice.text, configStub: choice.configStub, hooks: choice.hooks})"
/>
-->
<template v-for="choice in choices">
<component
:class="{chosen: selectedChoice == choice.title, unchosen: selectedChoice != choice.title, inactive: !requirementsMet(choice.requirements, config)}"
:is="choice.component"
:data="choice.data"
:config="config"
:axis="axis"
:title="choice.title"
:selected="selectedChoice == choice.title"
:hooks="choice.hooks"
:allowed="requirementsMet(choice.requirements, config)"
v-tooltip.top="{
content: requirementsMet(choice.requirements, config) ? choice.tooltip : choice.altTooltip,
class: 'tooltip-custom tooltip-other-custom fade-in',
delay: 0,
visible: choice.tooltip!=null,
}"
:key="choice.title"
v-on:click.native="choiceHandler(choice)"
v-on:choice="handleCustomChoice"
v-on:undo-choice="undoChoice"
/>
</template>
</div>
<div class="page-components">
<template v-for="pageComponent in pageComponents">
<component
:is="pageComponent.component"
:data="pageComponent.data"
:key="pageComponent.id"
:calibrating="calibrating"
:calStatus="calStatus"
v-on:page-comp-event="pageCompEvent"
/>
</template>
</div>
</div>
</template>
<script>
import wizardChoice from "./choices/wizardChoice.vue";
import wizardMisc from "./choices/wizardMisc.vue";
import wizardMotor from "./choices/wizardMotor.vue";
import wizardEncoderIncremental from "./choices/wizardEncoderIncremental.vue";
import wizardEncoderIncrementalIndex from "./choices/wizardEncoderIncrementalIndex.vue";
import wizardEnd from "./choices/wizardEnd.vue";
import wizardMotorCal from "./page_components/wizardMotorCal.vue";
import wizardEncoderCal from "./page_components/wizardEncoderCal.vue";
import wizardCalStatus from "./page_components/wizardCalStatus.vue";
import wizardBrake from "./choices/wizardBrake.vue";
import wizardInputFiltered from "./choices/wizardInputFiltered.vue";
import wizardInputVelRamp from "./choices/wizardInputVelRamp.vue";
import wizardInputTrajectory from "./choices/wizardInputTrajectory.vue";
export default {
name: "wizardPage",
props: {
choices: Array,
title: String,
customComponents: Array,
axis: String,
config: Object,
pageComponents: Array,
calibrating: Boolean,
calStatus: Boolean,
},
components: {
wizardChoice,
wizardMisc,
wizardMotor,
wizardEncoderIncremental,
wizardEncoderIncrementalIndex,
wizardEnd,
wizardMotorCal,
wizardEncoderCal,
wizardCalStatus,
wizardBrake,
wizardInputFiltered,
wizardInputVelRamp,
wizardInputTrajectory,
},
data: function () {
return {
selectedChoice: undefined,
};
},
methods: {
handleCustomChoice(e) {
console.log(e);
this.$emit("choice", e);
},
undoChoice(e) {
this.$emit("undo-choice",e);
},
pageCompEvent(e) {
this.$emit("page-comp-event", e);
},
requirementsMet(reqs, wizardConfig){
let reqsMet = true;
for (const fn of reqs) {
reqsMet = reqsMet && fn(wizardConfig);
}
return reqsMet;
},
choiceHandler(choice) {
console.log(choice.title);
if (this.selectedChoice == choice.title) {
// undo selection
this.selectedChoice = undefined;
}
else if (this.requirementsMet(choice.requirements, this.config)){
this.selectedChoice = choice.title;
}
}
},
};
</script>
<style>
.wizard-page {
display: flex;
flex-direction: column;
text-align: center;
}
.wizard-choices {
display: flex;
flex-direction: row;
flex-wrap: wrap;
}
.page-components {
display: flex;
flex-direction: row;
justify-content: center;
}
.chosen {
border: 2px solid black;
}
.unchosen {
border: 2px solid transparent;
}
.inactive {
background-color: var(--bg-color);
}
.wizard-title {
font-weight: bold;
}
.vue-tooltip.tooltip-custom {
background-color: lightyellow; /* var(--fg-color); */
border-radius: 0px;
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.4);
font-family: "Roboto", sans-serif;
color: black;
}
.vue-tooltip.tooltip-custom .tooltip-arrow {
display: none;
}
.vue-tooltip.fade-in {
opacity: 0;
animation: fadeIn ease 0.25s;
animation-fill-mode: both;
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
</style>
@@ -0,0 +1,242 @@
import store from "../store.js";
import * as socketio from "../comms/socketio.js";
import {wait, waitFor} from "./utils.js";
import odriveEnums from "../assets/odriveEnums.json"
// helper functions and utilities for getting ODrive values
// given a path like "odrive0.axis0.config.blah", return the value
export function getParam(path) {
let keys = path.split('.');
if (store.state.ODrivesConnected[keys[0]]) {
let odriveObj = store.state.odrives;
for (const key of keys) {
odriveObj = odriveObj[key];
}
return odriveObj;
}
else {
console.log("getParam for " + path + " is for disconnected ODrive");
return undefined;
}
}
// wrapper for val field
export function getVal(path) {
return getParam(path + ".val");
}
// wrapper for readonly field
export function getReadonly(path) {
return getParam(path + '.readonly');
}
export function fetchParam(path) {
if (store.state.serverConnected){
socketio.sendEvent({
type: "getProperty",
data: {path: path},
});
}
}
export function parseMath(inString) {
// given an input string that is valid arithmetic, use eval() to evaluate it
//let allowedChars = "0123456789eE/*-+.()";
let send = true;
//for (const c of inString) {
// if (!allowedChars.includes(c)) {
// send = false;
// }
//}
if (send) {
return eval(inString);
}
else {
return false;
}
}
export function putVal(path, value) {
console.log("path: " + path + ", val: " + value + ", type: " + typeof value);
if (store.state.ODrivesConnected[path.split('.')[0]]) {
if (value == Number.POSITIVE_INFINITY) {
value = "Infinity";
}
else if (value == Number.NEGATIVE_INFINITY) {
value = "-Infinity";
}
socketio.sendEvent({
type: "setProperty",
data: {path: path, val: value, type: typeof value}
});
}
else {
console.log("requesting " + path + " from disconnected odrive")
}
}
// path is path to function, args is list of parameters
export function callFcn(path, args = []) {
console.log("calling function: " + path);
socketio.sendEvent({
type: "callFunction",
data: {path: path, args: args}
});
}
export function odriveMinorVersion(odrive) {
// given an odrive object, return the minor version
return odrive.fw_version_minor.val;
}
export function getUnit(odrive, path) {
let unit = undefined;
for ( const key of Object.keys(odriveUnits[odriveMinorVersion(odrive)]) ) {
if (path.includes(key)) {
unit = odriveUnits[odriveMinorVersion(odrive)][key];
}
}
return unit;
}
export function clearErrors(odrive, axis) {
// odrive is odrive path like 'odrive0'
// axis is 'axis0', etc
let paths = [];
[".error", ".motor.error", ".encoder.error", ".controller.error"].forEach((path) => {
paths.push(odrive + axis + path);
});
paths.forEach((path) => {
putVal(path, 0);
})
}
export async function motorCalibration(odrive, axis) {
// set up our continuous fetch
let state;
let motorError;
let updateVals = () => {
fetchParam(odrive + axis + ".current_state");
fetchParam(odrive + axis + ".motor.config.phase_resistance");
fetchParam(odrive + axis + ".motor.config.phase_inductance");
fetchParam(odrive + axis + ".motor.is_calibrated");
fetchParam(odrive + axis + ".motor.error")
state = getVal(odrive + axis + ".current_state");
motorError = getVal(odrive + axis + ".motor.error");
}
// set up our state watch function
let end = () => {
// return {done: boolean, data <whatever>}
if (state != odriveEnums.AXIS_STATE_MOTOR_CALIBRATION) {
return {done: true, data: motorError}
}
else {
return {done: false}
}
}
// start getting live updates
let cylicUpdate = setInterval(updateVals, 100);
// send motor calibration command to correct odrive and axis
putVal(odrive + axis + ".requested_state", odriveEnums.AXIS_STATE_MOTOR_CALIBRATION);
// give it some time to start
await wait(500);
// only two things can happen now, either we successfully calibrate or we error out for some reason
const result = await waitFor(end);
// stop our parameter updates
clearInterval(cylicUpdate);
return result;
}
export async function encoderCalibration(odrive,axis) {
// set up our continuous fetch
let state;
let encoderError;
let updateVals = () => {
fetchParam(odrive + axis + ".current_state");
fetchParam(odrive + axis + ".encoder.is_ready");
fetchParam(odrive + axis + ".encoder.error")
state = getVal(odrive + axis + ".current_state");
encoderError = getVal(odrive + axis + ".encoder.error");
}
// set up our state watch function
let end = () => {
// return {done: boolean, data <whatever>}
if (state != odriveEnums.AXIS_STATE_ENCODER_OFFSET_CALIBRATION) {
return {done: true, data: encoderError}
}
else {
return {done: false}
}
}
// start getting live updates
let cylicUpdate = setInterval(updateVals, 100);
// start encoder offset cal
putVal(odrive + axis + ".requested_state", odriveEnums.AXIS_STATE_ENCODER_OFFSET_CALIBRATION);
// give it some time to start
await wait(500);
// only two things can happen now, either we successfully calibrate or we error out for some reason
const result = await waitFor(end);
// stop our parameter updates
clearInterval(cylicUpdate);
return result;
}
// standins for wizard and gui unit displays
// TODO - convert between odrive api units and user-defined units like degrees and rpm
export let odriveUnits = {
"4": {
"config.brake_resistance": "Ohms",
"controller.pos_setpoint": "counts",
"controller.vel_setpoint": "counts/s",
"controller.current_setpoint": "Amps",
"controller.config.vel_limit": "counts/s",
"controller.config.vel_ramp_rate": "counts/s^2",
"encoder.pos_estimate": "counts",
"encoder.vel_estimate": "counts/s",
"trap_traj.config.vel_limit": "counts/s",
"trap_traj.config.accel_limit": "counts/s^2",
"trap_traj.config.decel_limit": "counts/s^2",
"motor.config.phase_resistance": "Ohms",
"motor.config.phase_inductance": "Henries",
"motor.config.current_lim": "Amps",
"vbus_voltage": "Volts",
},
"5": {
"config.brake_resistance": "Ohms",
"controller.input_pos": "turns",
"controller.input_vel": "turns/s",
"controller.torque_setpoint": "Amps",
"controller.input_torque": "N/m",
"controller.config.vel_limit": "turns/s",
"controller.config.vel_ramp_rate": "turns/s^2",
"encoder.pos_estimate": "turns",
"encoder.vel_estimate": "turns/s",
"trap_traj.config.vel_limit": "turns/s",
"trap_traj.config.accel_limit": "turns/s^2",
"trap_traj.config.decel_limit": "turns/s^2",
"motor.config.phase_resistance": "Ohms",
"motor.config.phase_inductance": "Henries",
"motor.config.current_lim": "Amps",
"motor.config.torque_lim": "N/m",
"vbus_voltage": "Volts",
}
}
+93
View File
@@ -0,0 +1,93 @@
// general utilities for use in the GUI
// given an object and a test function, return a new object where only
// the values where test(value) == true are kept, as well as the parents
// of those objects. Nested non-matching children are set to {}
// ex: obj = {parent: {child1: {child2: "hello"}, child1a: {child2a: "world"}}}, test = ((o) => o == "hello"),
// filterBy(obj, test) -> {parent: {child1: {child2: "hello"}, child1a: {}}}
export let filterBy = (obj, test) => {
let retobj = {};
Object.entries(obj).forEach(([key, val]) => {
if (test(val)) {
retobj[key] = val;
}
else if (typeof val == 'object' && val != null) {
retobj[key] = filterBy(val, test);
}
})
return retobj;
}
// given an object and test function, delete values where test(val) == true
// modifies obj in place
export let deleteBy = (obj, test) => {
Object.keys(obj).forEach((key) => {
if (typeof obj[key] == 'object' && obj[key] != null) {
deleteBy(obj[key], test);
}
if (test(obj[key])) {
delete obj[key];
}
})
}
// sets up a timeout to wait for fun() to evaluate to true;
// fun must return {done: boolean, data: <whatever>}
export let waitFor = (fun) => {
return new Promise(resolve => {
let check = () => {
let res = fun();
if (res.done) {
resolve(res.data);
}
else {
setTimeout(check,100);
}
}
check();
});
}
// utility function to allow time for things to happen
// async wait
export let wait = (time) => {
return new Promise(resolve => {
setTimeout(() => resolve(), time);
});
}
export let pathsFromTree = (tree) => {
let flatpaths = [];
let path = [];
let pathFromTree = (tree) => {
for (const key of Object.keys(tree)) {
path.push(key);
if (tree[key] != null && typeof tree[key] == "object") {
pathFromTree(tree[key]);
}
else {
flatpaths.push(path.join('.'));
}
path.pop();
}
}
// array path
pathFromTree(tree);
return flatpaths;
}
export let numberDisplay = (val) => {
// if a number can be represented with 3 decimals, return it in that form
// otherwise, return scientific notation
let retVal = '';
try {
retVal = parseFloat(val).toFixed(3);
if (retVal == '0.000' && val != 0 || retVal.length > 7) {
retVal = parseFloat(val).toExponential(3);
}
} catch (error) {
console.log(error);
}
return retVal;
}
+14
View File
@@ -0,0 +1,14 @@
import Vue from 'vue'
import App from './App.vue'
import store from './store'
import Tooltip from 'vue-directive-tooltip';
import 'vue-directive-tooltip/dist/vueDirectiveTooltip.css';
import 'typeface-roboto/index.css';
Vue.config.productionTip = false
Vue.use(Tooltip);
new Vue({
store,
render: h => h(App)
}).$mount('#app')
+2
View File
@@ -0,0 +1,2 @@
import { ipcRenderer } from 'electron'
window.ipcRenderer = ipcRenderer
+420
View File
@@ -0,0 +1,420 @@
import Vue from 'vue';
import Vuex from 'vuex';
import TuningDashOld from "./assets/dashboards/Tuning_0_4_12.json";
import TuningDashNew from "./assets/dashboards/Tuning_0_5_1.json";
import * as socketio from "./comms/socketio";
import {filterBy, deleteBy } from "./lib/utils.js";
//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,
odriveParameters: Object,
odriveFunctions: Object,
axes: Array,
odriveServerAddress: String,
serverConnected: Boolean,
ODrivesConnected: Object,
serverOutput: [],
dashboards: [
{
name: "Start",
component: "Start",
},
],
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,
currentDash: "Start",
firstConn: false,
wizardMotor: "odrive0",
},
// mutations are functions that change the data
mutations: {
setWizardMotor(state, odrive) {
// odrive is string, "odrive0" or "odriveN"
state.wizardMotor = odrive;
},
setDash(state, dashName) {
state.currentDash = dashName;
},
setOdrives(state, odrives) {
state.odrives = odrives;
if (state.firstConn == false) {
// first time we're getting odrive data, add correct config page
let TuningDash;
if (state.odrives.odrive0.fw_version_minor.val == "5") {
TuningDash = TuningDashNew;
}
else if (state.odrives.odrive0.fw_version_minor.val == "4") {
TuningDash = TuningDashOld;
}
else {
TuningDash = TuningDashNew;
}
state.dashboards.push({
name: "Wizard",
component: "Wizard",
});
state.dashboards.push(TuningDash);
// plots will have variables associated, add them to sampled variables list
for (const plot of TuningDash.plots) {
for (const plotVar of plot.vars) {
//addsampledprop(path);
let path = plotVar.path;
if (!(path in state.sampledProperties)) {
let newPath = path.split('.');
newPath.splice(0, 1);
state.sampledProperties.push(newPath.join('.'));
state.propSamples[newPath.join('.')] = [];
}
socketio.sendEvent({
type: 'sampledVarNames',
data: {
paths: state.sampledProperties
}
});
}
}
}
state.firstConn = true;
},
setOdriveConfigs(state, payload) {
state.odriveConfigs['full'] = payload.full;
state.odriveConfigs['functions'] = payload.functions;
state.odriveConfigs['params'] = payload.params;
state.odriveConfigs['writeAble'] = payload.writeAble;
state.odriveConfigs['writeAbleNumeric'] = payload.writeAbleNumeric;
},
setODrivesStatus(state, obj) {
// obj is {"odriveX": true/false}
for (const odrive of Object.keys(obj)){
state.ODrivesConnected[odrive] = obj[odrive];
console.log(state.ODrivesConnected);
}
},
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;
};
let val = payload.val
if (val == "Infinity") {
val = Number.POSITIVE_INFINITY;
console.log("val is infinity");
}
else if (val == "-Infinity") {
val = Number.NEGATIVE_INFINITY;
}
Vue.set(createNestedObject(state.odrives, payload.path), "val", val);
},
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('.')] = [];
}
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;
},
logServerMessage(state, payload) {
// payload is string
state.serverOutput.push(payload);
// cut off to 1000 lines
if (state.serverOutput.length > 1000) {
state.serverOutput.splice(0,1);
}
},
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;
break;
}
}
break;
}
}
},
},
// actions trigger mutations
actions: {
getOdrives() {
socketio.sendEvent({
type: 'getODrives',
});
},
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 "str": {
// for handling infinity
let val;
if (odriveObj[key]["val"] == 'Infinity') {
val = Number.POSITIVE_INFINITY;
}
else if (odriveObj[key]["val"] == '-Infinity') {
val = Number.NEGATIVE_INFINITY;
}
retObj[key] = val;
break;
}
case "float":
retObj[key] = parseFloat(parseFloat(odriveObj[key]["val"]).toFixed(3));
break;
case "int8":
case "int16":
case "int32":
case "int64":
case "uint8":
case "uint16":
case "uint32":
case "uint64":
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;
}
// full parameter tree
// create object containing only ODrive functions
let fullTree = treeParse(context.state.odrives);
let fcns = filterBy(fullTree, ((o) => o == "function"));
deleteBy(fcns, ((o) => o.constructor == Object && Object.keys(o).length == 0));
// create object containing only ODrive parameters
let params = filterBy(fullTree, ((o) => o != "function" && typeof o != "object"));
deleteBy(params, ((o) => o.constructor == Object && Object.keys(o).length == 0))
// create object containing only ODrive parameters with write access
let writeAble = filterBy(context.state.odrives, ((o) => o['readonly'] == false));
deleteBy(writeAble, ((o) => o.constructor == Object && Object.keys(o).length == 0))
// create object of only ODrive parameters that are writeable and numeric
let writeAbleNumeric = filterBy(context.state.odrives, ((o) => o['readonly'] == false && ['float', 'int', 'bool'].includes(o['type'])));
deleteBy(writeAbleNumeric, ((o) => o.constructor == Object && Object.keys(o).length == 0))
context.commit('setOdriveConfigs', {full: fullTree,
functions: fcns,
params: params,
writeAble: treeParse(writeAble),
writeAbleNumeric: treeParse(writeAbleNumeric)});
},
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`,
});
}
if ('axis1' in context.state.odrives[odrive]) {
axes.push({
name: `${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');
//context.dispatch("getOdrives");
socketio.sendEvent({
type: "findODrives",
});
}
});
socketio.addEventListener({
type: "odrive-found",
callback: () => {
console.log("odrive-found recieved from server");
context.dispatch("getOdrives");
}
})
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"
});
}
});
socketio.addEventListener({
type: "odrives",
callback: odrives => {
context.commit('setOdrives', JSON.parse(odrives));
context.dispatch('getOdriveConfigs');
context.dispatch('getAxes');
}
});
// getVal event gets sent to server, server emits ODriveProperty event with path and val of property
socketio.addEventListener({
type: "ODriveProperty",
callback: retmsg => {
// retmsg is {path, val}
context.commit('updateOdriveProp', JSON.parse(retmsg));
}
});
socketio.addEventListener({
type: "odrive-disconnected",
callback: (odrive_name) => {
console.log(odrive_name + " disconnected");
//console.log("restarting server...");
//window.ipcRenderer.send('kill-server');
//window.ipcRenderer.send('start-server');
//context.dispatch('setServerAddress', context.state.odriveServerAddress);
}
});
socketio.addEventListener({
type: "odrives-status",
callback: (odrives_status) => {
console.log("From odrives-status msg " + odrives_status);
context.commit('setODrivesStatus', JSON.parse(odrives_status));
}
});
}
}
})
@@ -0,0 +1,561 @@
<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>
<div class="param-tree">
<json-view
:data="treeParams"
:rootKey="'odrives'"
v-on:selected="addVarToElement"
/>
</div>
</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"
:options="control.options"
: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 class="add-button card" @click="addComponent('function')">Add Function</div>
</div>
<template v-for="(action, index) in dash.actions">
<component
:is="action.actionType"
:id="action.id"
:key="index + '-action'"
:path="action.path"
:odrives="odrives"
:initVal="action.val"
:dashID="dash.id"
:options="action.options"
/>
</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 CtrlEnum from "../components/controls/CtrlEnum.vue";
import Plot from "../components/plots/Plot.vue";
import Action from "../components/actions/Action.vue";
import ActionEnum from "../components/actions/ActionEnum.vue"
import { JSONView } from "vue-json-component";
import { v4 as uuidv4 } from "uuid";
let plotColors = [
"#1f77b4", // blue
"#ff7f0e", // orange
"#2ca02c", // green
"#d62728", // red
"#9467bd", // purple
"#8c564b", // brown
"#e377c2", // pink
"#7f7f7f", // gray
"#bcbd22", // olive
"#17becf", // cyan
];
let odriveEnums = {
// axis requested state
requested_state: [
{
text: "Undefined",
value: 0,
},
{
text: "Idle",
value: 1,
},
{
text: "Startup 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,
},
{
text: "Encoder Hall Polarity Calibration",
value: 12,
},
{
text: "Encoder Hall Phase Calibration",
value: 13,
}
],
current_state: [
{
text: "Undefined",
value: 0,
},
{
text: "Idle",
value: 1,
},
{
text: "Startup 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,
},
{
text: "Encoder Hall Polarity Calibration",
value: 12,
},
{
text: "Encoder Hall Phase Calibration",
value: 13,
}
],
// encoder mode
mode: [
{
text: "Incremental",
value: 0,
},
{
text: "Hall Effect",
value: 1,
},
{
text: "Sine Cosine",
value: 2,
},
{
text: "CUI Absolute",
value: 256,
},
{
text: "AMS Absolute",
value: 257,
},
{
text: "AEAT Absolute",
value: 258,
},
{
text: "RLS Absolute",
value: 259,
},
],
// motor type
motor_type: [
{
text: "High Current",
value: 0,
},
{
text: "Gimbal",
value: 2,
},
{
text: "Induction",
value: 3,
},
],
// controller control mode
control_mode: [
{
text: "Voltage Control",
value: 0,
},
{
text: "Torque Control",
value: 1,
},
{
text: "Velocity Control",
value: 2,
},
{
text: "Position Control",
value: 3,
},
],
input_mode: [
{
text: "Inactive",
value: 0
},
{
text: "Passthrough",
value: 1
},
{
text: "Velocity Ramp",
value: 2
},
{
text: "Position Filter",
value: 3
},
{
text: "Mix Channels",
value: 4
},
{
text: "Trapezoidal Trajectory",
value: 5
},
{
text: "Torque Ramp",
value: 6
},
{
text: "Mirror",
value: 7
},
{
text: "Tuning",
value: 8
}
]
}
export default {
name: "Dashboard",
components: {
CtrlBoolean,
CtrlNumeric,
CtrlFunction,
CtrlSlider,
CtrlEnum,
Plot,
Action,
ActionEnum,
"json-view": JSONView,
},
props: ["dash", "odrives"],
data() {
return {
paramsVisible: false,
addCompType: undefined,
currentPlot: undefined,
treeParams: undefined,
};
},
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;
switch (this.addCompType) {
case "control":
this.treeParams = this.$store.state.odriveConfigs['params'];
console.log(this.treeParams);
break;
case "plot":
this.treeParams = this.$store.state.odriveConfigs['params'];
break;
case "slider":
this.treeParams = this.$store.state.odriveConfigs['writeAbleNumeric'];
break;
case "action":
this.treeParams = this.$store.state.odriveConfigs['writeAble'];
break;
case "function":
this.treeParams = this.$store.state.odriveConfigs['functions'];
break;
default:
break;
}
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
console.log(e);
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":
if (Object.keys(odriveEnums).includes(e.key)){
this.dash.controls.push({
controlType: "CtrlEnum",
path: e.path,
options: odriveEnums[e.key],
})
}
else {
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 "function":
this.dash.controls.push({
controlType: "CtrlFunction",
path: e.path,
});
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();
console.log("action, key = " + e.key);
if (Object.keys(odriveEnums).includes(e.key)){
this.dash.actions.push({
actionType: "ActionEnum",
id: id,
path: e.path,
options: odriveEnums[e.key],
})
}
else {
this.dash.actions.push({
actionType: "Action",
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;
overflow-y: scroll;
}
.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: 0 0;
display: inline-block;
}
.dropdown-content {
position: absolute;
z-index: 1;
}
.param-tree {
overflow-y: scroll;
max-height: 85vh;
}
</style>
+118
View File
@@ -0,0 +1,118 @@
<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" :class="{ notConnected: notConnected, connected: connected}" v-on:change="setUrl" :value="serverAddress">
</div>
<button class="show-msg-button" @click="showServerMessages = !showServerMessages;">Having trouble? Click here for debug output</button>
<div class="server-msgs" v-if="showServerMessages">
<div class="server-msg" v-for="(msg, index) in serverMessages" :key="msg + index">{{msg}}</div>
</div>
</div>
</template>
<script>
export default {
name: 'Home',
components: {
},
data() {
return {
showServerMessages: false,
}
},
computed: {
connected() {
return this.$store.state.serverConnected == true;
},
notConnected() {
return this.$store.state.serverConnected != true;
},
serverAddress() {
return this.$store.state.odriveServerAddress;
},
serverMessages() {
if (this.$store.state.serverOutput.length >= 10) {
let len = this.$store.state.serverOutput.length;
return this.$store.state.serverOutput.slice(len-10,len);
}
else {
return this.$store.state.serverOutput;
}
}
},
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;
}
.show-msg-button {
border: none;
background-color: var(--bg-color);
margin-top: 0.5rem;
cursor: pointer;
}
.server-msg {
font-family: "Roboto Mono", monospace;
}
</style>
+395
View File
@@ -0,0 +1,395 @@
<template>
<div class="wizard">
<div class="wizard-container">
<div class="wizard-nav card">
<span class="wizard-nav-title">Wizard Pages</span>
<span
v-for="page in wizardPages"
:key="page.title"
class="wizard-link"
:class="{
'active-link': currentStep == page,
'finished-page': page.choiceMade,
}"
>{{ page.link }}</span
>
</div>
<div class="wizard-page">
<wizardPage
:choices="currentStep.choices"
:customComponents="currentStep.customComponents"
:pageComponents="currentStep.pageComponents"
:title="currentStep.title"
:config="wizardConfig"
:calibrating="calibrating"
:calStatus="calStatus"
v-on:choice="choiceHandler"
v-on:undo-choice="undoChoice"
v-on:page-comp-event="pageEventHandler"
/>
<div class="wizard-controls">
<!-- show breadcrumbs, back, apply, next buttons -->
<button class="wizard-button card" @click="back">Back</button>
<button class="wizard-button card" @click="finish">Finish</button>
<button
class="wizard-button card"
:class="{ 'next-green': choiceMade }"
v-tooltip.right="{
content: currentStep.nextTooltip,
class: 'tooltip-custom tooltip-other-custom fade-in',
delay: 0,
visible: !choiceMade && currentStep.nextTooltip != null,
}"
@click="next"
>
Next
</button>
</div>
</div>
</div>
</div>
</template>
<script>
import configTemplate from "../assets/wizard/configTemplate.json";
import wizardPage from "../components/wizard/wizardPage.vue";
import odriveEnums from "../assets/odriveEnums.json";
import { pages } from "../assets/wizard/wizard.js";
import {
getVal,
putVal,
fetchParam,
clearErrors,
motorCalibration,
encoderCalibration
} from "../lib/odrive_utils.js";
import {wait} from "../lib/utils.js"
export default {
name: "Wizard",
components: {
wizardPage,
},
props: [],
data: function () {
return {
currentStep: pages.ODrive,
wizardConfig: configTemplate,
choiceMade: false,
calibrating: false, // for indicating that calibration is in progress
calStatus: undefined, // for indicating the status of a calibration attempt
odrive: "odrive0.",
};
},
computed: {
wizardPages() {
return pages;
},
},
methods: {
// certain actions, like starting motor or encoder calibration,
// require special handling. This function is used for those events
async pageEventHandler(e) {
this.choiceMade = false;
if (e.data == "motor calibration") {
let apply = () => {
let configStub = {};
configStub[e.axis] = {
motor: {
config: {
phase_resistance: getVal(this.odrive + e.axis + ".motor.config.phase_resistance"),
phase_inductance: getVal(this.odrive + e.axis + ".motor.config.phase_inductance"),
}
}
}
this.choiceHandler({
choice: "Motor Calibration",
configStub: configStub,
hooks: [],
});
this.choiceMade = true;
};
let updateCalInfo = () => {
// parse error code, update calibration information
let motorError = getVal(this.odrive + e.axis + ".motor.error");
if (
motorError == odriveEnums.MOTOR_ERROR_PHASE_RESISTANCE_OUT_OF_RANGE ||
motorError == odriveEnums.MOTOR_ERROR_PHASE_INDUCTANCE_OUT_OF_RANGE
) {
//we got the expected calibration error
console.log("Calibration failed.");
this.calStatus = false;
}
};
// result is motor.error
this.calibrating = true;
let result = await motorCalibration(this.odrive, e.axis);
this.calibrating = false;
fetchParam(this.odrive + e.axis + '.motor.is_calibrated');
// no error, we're good!
if (result == 0) {
wait(250);
apply();
this.calStatus = true;
} else {
console.log("motor cal error" + result.data);
this.calibrating = false;
this.choiceMade = false;
// clear errors, display info.
updateCalInfo();
clearErrors(this.odrive, e.axis);
}
} else if (e.data == "encoder calibration") {
// get old CPR
// apply CPR from this.wizardConfig
// start calibration
// wait for cal to finish
// set odrive cpr back to oldVal
let oldCPR = getVal(this.odrive + e.axis + ".encoder.config.cpr");
console.log("oldCPR = " + oldCPR);
console.log("axis = " + e.axis);
// set up the calib_scan_distance to a value that will work
let pp = this.wizardConfig[e.axis].motor.config.pole_pairs;
// smallest multiple of 4pi that is bigger than pole_pairs * 2pi
let scan_distance = pp % 0 ? pp * 2 * Math.PI : (pp + 1) * 2 * Math.PI;
putVal(this.odrive + e.axis + ".encoder.config.calib_scan_distance", scan_distance);
putVal(
this.odrive + e.axis + ".encoder.config.cpr",
this.wizardConfig[e.axis].encoder.config.cpr
);
await wait(250);
this.calibrating = true;
let result = await encoderCalibration(this.odrive, e.axis);
this.calibrating = false;
putVal(this.odrive + e.axis + ".encoder.config.cpr", oldCPR);
fetchParam(this.odrive + e.axis + '.encoder.is_ready');
if (result == 0){
// no encoder error, we're good
this.choiceMade = true;
this.currentStep.choiceMade = true;
this.calStatus = true;
}
else {
clearErrors(this.odrive, e.axis);
this.calStatus = false;
}
}
},
async choiceHandler(e) {
// apply static configStub
this.updateConfig(this.wizardConfig, e.configStub);
// run hooks
for (const fn of e.hooks) {
this.wizardConfig = fn(this.wizardConfig);
}
// ugly, but a special case.
// for motors, wait for calibration to finish before giving the green light unless motor.is_calibrated == true
// for encoders, wait for calibration to finish unless encoder.is_ready == true
if (
this.currentStep == pages.Motor_0 ||
this.currentStep == pages.Motor_1
) {
let axis;
if (this.currentStep == pages.Motor_0) axis = "axis0";
if (this.currentStep == pages.Motor_1) axis = "axis1";
fetchParam(this.odrive + axis + ".motor.is_calibrated");
await wait(100);
if (getVal(this.odrive + axis + ".motor.is_calibrated") == false) {
this.choiceMade = false;
}
else {
this.choiceMade = true;
}
}
else if (
this.currentStep == pages.Encoder_0 ||
this.currentStep == pages.Encoder_1
) {
let axis;
if (this.currentStep == pages.Encoder_0) axis = "axis0";
if (this.currentStep == pages.Encoder_1) axis = "axis1";
fetchParam(this.odrive + axis + ".encoder.is_ready");
await wait(100);
if (getVal(this.odrive + axis + ".encoder.is_ready") == false) {
this.choiceMade = false;
}
else {
this.choiceMade = true;
}
}
else {
this.choiceMade = true;
}
this.currentStep.choiceMade = this.choiceMade;
console.log(JSON.parse(JSON.stringify(this.wizardConfig)));
},
undoChoice(e) {
this.choiceMade = false;
this.currentStep.choiceMade = this.choiceMade;
this.nullConfig(this.wizardConfig, e.configStub);
console.log(JSON.parse(JSON.stringify(this.wizardConfig)));
},
updateConfig(config, configStub) {
// iterate over keys in configStub
if (configStub != null) {
Object.keys(configStub).forEach((key) => {
if (typeof configStub[key] == "object") {
this.updateConfig(config[key], configStub[key]);
} else {
config[key] = configStub[key];
}
});
}
},
nullConfig(config, configStub) {
// iterate over keys in configStub
if (configStub != null) {
console.log("NullConfig");
Object.keys(configStub).forEach((key) => {
console.log(key);
if (typeof configStub[key] == "object") {
this.nullConfig(config[key], configStub[key]);
} else {
config[key] = null;
}
});
}
},
next() {
console.log("next");
if (this.choiceMade == true) {
this.currentStep = pages[this.currentStep.next];
this.choiceMade = false;
}
this.calStatus = undefined;
},
finish() {
console.log("finish");
this.currentStep = pages.End;
},
back() {
console.log("back");
this.currentStep = pages[this.currentStep.back];
this.choiceMade = false;
},
},
beforeDestroy() {
for (const page of Object.keys(pages)) {
pages[page].choiceMade = false;
}
let erase = (obj) => {
Object.keys(obj).forEach((key) => {
if (typeof obj[key] == "object" && obj[key] != null) {
erase(obj[key]);
} else {
obj[key] = null;
}
});
};
erase(this.wizardConfig);
},
};
</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-nav {
display: flex;
flex-direction: column;
height: fit-content;
}
.wizard-nav-title {
text-align: center;
font-weight: bold;
margin-bottom: 0.4rem;
}
.wizard-link {
margin: 0.2rem;
}
.active-link {
text-decoration: underline;
}
.wizard-container {
display: flex;
flex-direction: row;
height: 95vh;
}
.wizard-page {
margin: auto;
}
.wizard-controls {
margin-top: 0;
display: flex;
justify-content: center;
}
.active {
background-color: var(--fg-color);
}
.inactive {
background-color: var(--bg-color);
color: white;
}
.wizard-button:active {
background-color: var(--bg-color);
}
.next-green {
color: green;
font-weight: bold;
}
.vue-tooltip.tooltip-custom {
background-color: lightyellow; /* var(--fg-color); */
border-radius: 0px;
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.4);
font-family: "Roboto", sans-serif;
color: black;
}
.vue-tooltip.tooltip-custom .tooltip-arrow {
display: none;
}
.vue-tooltip.fade-in {
opacity: 0;
animation: fadeIn ease 0.25s;
animation-fill-mode: both;
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.finished-page {
color: green;
}
</style>