*
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
After Width: | Height: | Size: 169 KiB |
|
After Width: | Height: | Size: 171 KiB |
|
After Width: | Height: | Size: 109 KiB |
|
After Width: | Height: | Size: 122 KiB |
|
After Width: | Height: | Size: 130 KiB |
|
After Width: | Height: | Size: 166 KiB |
|
After Width: | Height: | Size: 165 KiB |
|
After Width: | Height: | Size: 112 KiB |
|
After Width: | Height: | Size: 46 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
@@ -0,0 +1,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
|
||||
}
|
||||
|
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",
|
||||
},
|
||||
}
|
||||
@@ -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",
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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')
|
||||
@@ -0,0 +1,2 @@
|
||||
import { ipcRenderer } from 'electron'
|
||||
window.ipcRenderer = ipcRenderer
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||