This commit is contained in:
2025-05-13 01:34:53 +03:00
parent 427735e23d
commit 83f3f1c7d4
945 changed files with 633484 additions and 0 deletions
@@ -0,0 +1,561 @@
<template>
<div class="dashboard">
<!-- PARAMETER DROPDOWN MENU -->
<div v-show="paramsVisible" class="dropdown">
<div class="card dropdown-content">
<div>
<button class="close-button" @click="hideTree">X</button>
Parameters
</div>
<div class="param-tree">
<json-view
:data="treeParams"
:rootKey="'odrives'"
v-on:selected="addVarToElement"
/>
</div>
</div>
</div>
<div class="dashboard_container">
<div class="controls">
<template v-for="(control, index) in dash.controls">
<component
:is="control.controlType"
:key="index + '-control'"
:path="control.path"
:name="control.name"
:options="control.options"
:odrives="odrives"
:dashID="dash.id"
/>
</template>
<div class="control-buttons">
<div class="add-button card" @click="addComponent('control')">Add Control</div>
<div class="add-button card" @click="addComponent('slider')">Add Slider</div>
<div class="add-button card" @click="addComponent('function')">Add Function</div>
</div>
<template v-for="(action, index) in dash.actions">
<component
:is="action.actionType"
:id="action.id"
:key="index + '-action'"
:path="action.path"
:odrives="odrives"
:initVal="action.val"
:dashID="dash.id"
:options="action.options"
/>
</template>
<div class="add-button card" @click="addComponent('action')">Add Action</div>
</div>
<div class="plots">
<template v-for="(plot, index) in dash.plots">
<plot
:plot="plot"
:key="index"
:name="plot.name"
:dashID="dash.id"
v-on:add-var="currentPlot=plot.name;addComponent('plot')"
/>
</template>
<div class="add-button card" @click="addPlot">Add Plot</div>
</div>
</div>
</div>
</template>
<script>
//this component will get passed a list of controls and plots
//display controls on the left and plots on the right?
//leave full names for plots
//collate controls into individual cards based on the deepest common level
import CtrlBoolean from "../components/controls/CtrlBoolean.vue";
import CtrlNumeric from "../components/controls/CtrlNumeric.vue";
import CtrlFunction from "../components/controls/CtrlFunction.vue";
import CtrlSlider from "../components/controls/CtrlSlider.vue";
import CtrlEnum from "../components/controls/CtrlEnum.vue";
import Plot from "../components/plots/Plot.vue";
import Action from "../components/actions/Action.vue";
import ActionEnum from "../components/actions/ActionEnum.vue"
import { JSONView } from "vue-json-component";
import { v4 as uuidv4 } from "uuid";
let plotColors = [
"#1f77b4", // blue
"#ff7f0e", // orange
"#2ca02c", // green
"#d62728", // red
"#9467bd", // purple
"#8c564b", // brown
"#e377c2", // pink
"#7f7f7f", // gray
"#bcbd22", // olive
"#17becf", // cyan
];
let odriveEnums = {
// axis requested state
requested_state: [
{
text: "Undefined",
value: 0,
},
{
text: "Idle",
value: 1,
},
{
text: "Startup Sequence",
value: 2,
},
{
text: "Full Calibration Sequence",
value: 3,
},
{
text: "Motor Calibration",
value: 4,
},
{
text: "Sensorless Control",
value: 5,
},
{
text: "Encoder Index Search",
value: 6,
},
{
text: "Encoder Offset Calibration",
value: 7,
},
{
text: "Closed Loop Control",
value: 8,
},
{
text: "Lockin Spin",
value: 9,
},
{
text: "Encoder Direction Find",
value: 10,
},
{
text: "Homing",
value: 11,
},
{
text: "Encoder Hall Polarity Calibration",
value: 12,
},
{
text: "Encoder Hall Phase Calibration",
value: 13,
}
],
current_state: [
{
text: "Undefined",
value: 0,
},
{
text: "Idle",
value: 1,
},
{
text: "Startup Sequence",
value: 2,
},
{
text: "Full Calibration Sequence",
value: 3,
},
{
text: "Motor Calibration",
value: 4,
},
{
text: "Sensorless Control",
value: 5,
},
{
text: "Encoder Index Search",
value: 6,
},
{
text: "Encoder Offset Calibration",
value: 7,
},
{
text: "Closed Loop Control",
value: 8,
},
{
text: "Lockin Spin",
value: 9,
},
{
text: "Encoder Direction Find",
value: 10,
},
{
text: "Homing",
value: 11,
},
{
text: "Encoder Hall Polarity Calibration",
value: 12,
},
{
text: "Encoder Hall Phase Calibration",
value: 13,
}
],
// encoder mode
mode: [
{
text: "Incremental",
value: 0,
},
{
text: "Hall Effect",
value: 1,
},
{
text: "Sine Cosine",
value: 2,
},
{
text: "CUI Absolute",
value: 256,
},
{
text: "AMS Absolute",
value: 257,
},
{
text: "AEAT Absolute",
value: 258,
},
{
text: "RLS Absolute",
value: 259,
},
],
// motor type
motor_type: [
{
text: "High Current",
value: 0,
},
{
text: "Gimbal",
value: 2,
},
{
text: "Induction",
value: 3,
},
],
// controller control mode
control_mode: [
{
text: "Voltage Control",
value: 0,
},
{
text: "Torque Control",
value: 1,
},
{
text: "Velocity Control",
value: 2,
},
{
text: "Position Control",
value: 3,
},
],
input_mode: [
{
text: "Inactive",
value: 0
},
{
text: "Passthrough",
value: 1
},
{
text: "Velocity Ramp",
value: 2
},
{
text: "Position Filter",
value: 3
},
{
text: "Mix Channels",
value: 4
},
{
text: "Trapezoidal Trajectory",
value: 5
},
{
text: "Torque Ramp",
value: 6
},
{
text: "Mirror",
value: 7
},
{
text: "Tuning",
value: 8
}
]
}
export default {
name: "Dashboard",
components: {
CtrlBoolean,
CtrlNumeric,
CtrlFunction,
CtrlSlider,
CtrlEnum,
Plot,
Action,
ActionEnum,
"json-view": JSONView,
},
props: ["dash", "odrives"],
data() {
return {
paramsVisible: false,
addCompType: undefined,
currentPlot: undefined,
treeParams: undefined,
};
},
methods: {
deleteAction(e) {
this.$emit("delete-action", e);
console.log(e);
},
deletePlot(e) {
this.$emit("delete-plot", e);
},
addVar(e) {
this.$emit("add-var", e);
},
showTree() {
//show the parameter tree
this.paramsVisible = true;
},
hideTree() {
this.paramsVisible = false;
this.addCompType = undefined;
},
addComponent(componentType) {
this.addCompType = componentType;
switch (this.addCompType) {
case "control":
this.treeParams = this.$store.state.odriveConfigs['params'];
console.log(this.treeParams);
break;
case "plot":
this.treeParams = this.$store.state.odriveConfigs['params'];
break;
case "slider":
this.treeParams = this.$store.state.odriveConfigs['writeAbleNumeric'];
break;
case "action":
this.treeParams = this.$store.state.odriveConfigs['writeAble'];
break;
case "function":
this.treeParams = this.$store.state.odriveConfigs['functions'];
break;
default:
break;
}
this.paramsVisible = true;
},
addVarToElement(e) {
//when the parameter tree is open and a parameter is clicked,
//add the clicked parameter to the list of controls for the
//current dashboard
console.log(e);
switch (this.addCompType) {
case "control":
switch (typeof e.value) {
case "boolean":
this.dash.controls.push({
controlType: "CtrlBoolean",
path: e.path,
});
//this.$store.commit("addSampledProperty", e.path);
break;
case "number":
if (Object.keys(odriveEnums).includes(e.key)){
this.dash.controls.push({
controlType: "CtrlEnum",
path: e.path,
options: odriveEnums[e.key],
})
}
else {
this.dash.controls.push({
controlType: "CtrlNumeric",
path: e.path,
});
}
//this.$store.commit("addSampledProperty", e.path);
break;
case "string":
this.dash.controls.push({
controlType: "CtrlFunction",
path: e.path,
});
break;
default:
break;
}
break;
case "function":
this.dash.controls.push({
controlType: "CtrlFunction",
path: e.path,
});
break;
case "plot":
// add the selected element to the plot var list
// add the selected element to the sampling var list
// find the plot, append path to plot.vars
console.log(e);
for (const plot of this.dash.plots) {
if (plot.name == this.currentPlot) {
plot.vars.push({
path: e.path,
color: plotColors[plot.vars.length % plotColors.length],
});
this.$store.commit("addSampledProperty", e.path);
console.log(plot);
break;
}
}
break;
case "action":
{
// add an action to the current dash
let id = uuidv4();
console.log("action, key = " + e.key);
if (Object.keys(odriveEnums).includes(e.key)){
this.dash.actions.push({
actionType: "ActionEnum",
id: id,
path: e.path,
options: odriveEnums[e.key],
})
}
else {
this.dash.actions.push({
actionType: "Action",
id: id,
path: e.path,
val: undefined,
});
}
}
break;
case "slider":
// add a slider to the list of controls if the selected item is valid (numeric)
switch (typeof e.value) {
case "number":
this.dash.controls.push({
controlType: "CtrlSlider",
path: e.path,
});
break;
default:
break;
}
}
},
addPlot() {
let plotId = uuidv4();
this.dash.plots.push({
name: plotId,
vars: [],
});
},
},
};
</script>
<style scoped>
.dashboard {
background-color: var(--bg-color);
height: 100vh;
max-height: 100vh;
width: 100vw;
padding-top: var(--top-height);
padding-bottom: var(--bottom-height);
}
.dashboard_container {
display: flex;
flex-direction: row;
height: 95vh;
}
.controls {
flex-grow: 1;
border-right: 1px solid lightgrey;
overflow-y: scroll;
}
.add-button {
background-color: lightcyan;
width: 120px;
margin: 10px auto;
margin-left: 10px;
margin-right: 10px;
text-align: center;
border-radius: 20px;
cursor: pointer;
user-select: none;
}
.add-button:active {
background-color: lightblue;
}
.plots {
flex-grow: 1;
max-height: 94vh;
overflow-y: scroll;
display: flex;
flex-direction: column;
}
.control-buttons {
display: flex;
flex-direction: row;
}
.dropdown {
position: absolute;
padding: 0 0;
display: inline-block;
}
.dropdown-content {
position: absolute;
z-index: 1;
}
.param-tree {
overflow-y: scroll;
max-height: 85vh;
}
</style>
+118
View File
@@ -0,0 +1,118 @@
<template>
<div class="home">
<div class="logo">
<img alt="Odrive Logo" src="../assets/odrive_logo.png">
</div>
<div class="home_text">
To set up your ODrive, connect it and power it up.
</div>
<div class="connected-container">
<input type="text" :class="{ notConnected: notConnected, connected: connected}" v-on:change="setUrl" :value="serverAddress">
</div>
<button class="show-msg-button" @click="showServerMessages = !showServerMessages;">Having trouble? Click here for debug output</button>
<div class="server-msgs" v-if="showServerMessages">
<div class="server-msg" v-for="(msg, index) in serverMessages" :key="msg + index">{{msg}}</div>
</div>
</div>
</template>
<script>
export default {
name: 'Home',
components: {
},
data() {
return {
showServerMessages: false,
}
},
computed: {
connected() {
return this.$store.state.serverConnected == true;
},
notConnected() {
return this.$store.state.serverConnected != true;
},
serverAddress() {
return this.$store.state.odriveServerAddress;
},
serverMessages() {
if (this.$store.state.serverOutput.length >= 10) {
let len = this.$store.state.serverOutput.length;
return this.$store.state.serverOutput.slice(len-10,len);
}
else {
return this.$store.state.serverOutput;
}
}
},
methods: {
setUrl(e) {
console.log(e.target.value);
this.$store.dispatch("setServerAddress", e.target.value);
},
}
}
</script>
<style>
.home {
padding: 10% 0;
margin: auto;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background-color: var(--bg-color);
height: 100vh;
}
img {
margin: auto;
display: block;
max-width: 50%;
width: auto;
height: auto;
}
.home_text {
font-size: 1.5rem;
padding: 20px;
}
.connected, .notConnected {
width: 40vw;
margin: 0;
padding: 10px;
border: none;
border-bottom: 1px solid lightgrey;
font-size: 1.5rem;
background-color: var(--bg-color);
text-align: center;
}
input:focus {
outline: none;
}
.connected {
border-bottom: 1px solid lightgreen;
background-color: lightgreen;
}
.notConnected {
border-bottom: 1px solid red;
}
.show-msg-button {
border: none;
background-color: var(--bg-color);
margin-top: 0.5rem;
cursor: pointer;
}
.server-msg {
font-family: "Roboto Mono", monospace;
}
</style>
+395
View File
@@ -0,0 +1,395 @@
<template>
<div class="wizard">
<div class="wizard-container">
<div class="wizard-nav card">
<span class="wizard-nav-title">Wizard Pages</span>
<span
v-for="page in wizardPages"
:key="page.title"
class="wizard-link"
:class="{
'active-link': currentStep == page,
'finished-page': page.choiceMade,
}"
>{{ page.link }}</span
>
</div>
<div class="wizard-page">
<wizardPage
:choices="currentStep.choices"
:customComponents="currentStep.customComponents"
:pageComponents="currentStep.pageComponents"
:title="currentStep.title"
:config="wizardConfig"
:calibrating="calibrating"
:calStatus="calStatus"
v-on:choice="choiceHandler"
v-on:undo-choice="undoChoice"
v-on:page-comp-event="pageEventHandler"
/>
<div class="wizard-controls">
<!-- show breadcrumbs, back, apply, next buttons -->
<button class="wizard-button card" @click="back">Back</button>
<button class="wizard-button card" @click="finish">Finish</button>
<button
class="wizard-button card"
:class="{ 'next-green': choiceMade }"
v-tooltip.right="{
content: currentStep.nextTooltip,
class: 'tooltip-custom tooltip-other-custom fade-in',
delay: 0,
visible: !choiceMade && currentStep.nextTooltip != null,
}"
@click="next"
>
Next
</button>
</div>
</div>
</div>
</div>
</template>
<script>
import configTemplate from "../assets/wizard/configTemplate.json";
import wizardPage from "../components/wizard/wizardPage.vue";
import odriveEnums from "../assets/odriveEnums.json";
import { pages } from "../assets/wizard/wizard.js";
import {
getVal,
putVal,
fetchParam,
clearErrors,
motorCalibration,
encoderCalibration
} from "../lib/odrive_utils.js";
import {wait} from "../lib/utils.js"
export default {
name: "Wizard",
components: {
wizardPage,
},
props: [],
data: function () {
return {
currentStep: pages.ODrive,
wizardConfig: configTemplate,
choiceMade: false,
calibrating: false, // for indicating that calibration is in progress
calStatus: undefined, // for indicating the status of a calibration attempt
odrive: "odrive0.",
};
},
computed: {
wizardPages() {
return pages;
},
},
methods: {
// certain actions, like starting motor or encoder calibration,
// require special handling. This function is used for those events
async pageEventHandler(e) {
this.choiceMade = false;
if (e.data == "motor calibration") {
let apply = () => {
let configStub = {};
configStub[e.axis] = {
motor: {
config: {
phase_resistance: getVal(this.odrive + e.axis + ".motor.config.phase_resistance"),
phase_inductance: getVal(this.odrive + e.axis + ".motor.config.phase_inductance"),
}
}
}
this.choiceHandler({
choice: "Motor Calibration",
configStub: configStub,
hooks: [],
});
this.choiceMade = true;
};
let updateCalInfo = () => {
// parse error code, update calibration information
let motorError = getVal(this.odrive + e.axis + ".motor.error");
if (
motorError == odriveEnums.MOTOR_ERROR_PHASE_RESISTANCE_OUT_OF_RANGE ||
motorError == odriveEnums.MOTOR_ERROR_PHASE_INDUCTANCE_OUT_OF_RANGE
) {
//we got the expected calibration error
console.log("Calibration failed.");
this.calStatus = false;
}
};
// result is motor.error
this.calibrating = true;
let result = await motorCalibration(this.odrive, e.axis);
this.calibrating = false;
fetchParam(this.odrive + e.axis + '.motor.is_calibrated');
// no error, we're good!
if (result == 0) {
wait(250);
apply();
this.calStatus = true;
} else {
console.log("motor cal error" + result.data);
this.calibrating = false;
this.choiceMade = false;
// clear errors, display info.
updateCalInfo();
clearErrors(this.odrive, e.axis);
}
} else if (e.data == "encoder calibration") {
// get old CPR
// apply CPR from this.wizardConfig
// start calibration
// wait for cal to finish
// set odrive cpr back to oldVal
let oldCPR = getVal(this.odrive + e.axis + ".encoder.config.cpr");
console.log("oldCPR = " + oldCPR);
console.log("axis = " + e.axis);
// set up the calib_scan_distance to a value that will work
let pp = this.wizardConfig[e.axis].motor.config.pole_pairs;
// smallest multiple of 4pi that is bigger than pole_pairs * 2pi
let scan_distance = pp % 0 ? pp * 2 * Math.PI : (pp + 1) * 2 * Math.PI;
putVal(this.odrive + e.axis + ".encoder.config.calib_scan_distance", scan_distance);
putVal(
this.odrive + e.axis + ".encoder.config.cpr",
this.wizardConfig[e.axis].encoder.config.cpr
);
await wait(250);
this.calibrating = true;
let result = await encoderCalibration(this.odrive, e.axis);
this.calibrating = false;
putVal(this.odrive + e.axis + ".encoder.config.cpr", oldCPR);
fetchParam(this.odrive + e.axis + '.encoder.is_ready');
if (result == 0){
// no encoder error, we're good
this.choiceMade = true;
this.currentStep.choiceMade = true;
this.calStatus = true;
}
else {
clearErrors(this.odrive, e.axis);
this.calStatus = false;
}
}
},
async choiceHandler(e) {
// apply static configStub
this.updateConfig(this.wizardConfig, e.configStub);
// run hooks
for (const fn of e.hooks) {
this.wizardConfig = fn(this.wizardConfig);
}
// ugly, but a special case.
// for motors, wait for calibration to finish before giving the green light unless motor.is_calibrated == true
// for encoders, wait for calibration to finish unless encoder.is_ready == true
if (
this.currentStep == pages.Motor_0 ||
this.currentStep == pages.Motor_1
) {
let axis;
if (this.currentStep == pages.Motor_0) axis = "axis0";
if (this.currentStep == pages.Motor_1) axis = "axis1";
fetchParam(this.odrive + axis + ".motor.is_calibrated");
await wait(100);
if (getVal(this.odrive + axis + ".motor.is_calibrated") == false) {
this.choiceMade = false;
}
else {
this.choiceMade = true;
}
}
else if (
this.currentStep == pages.Encoder_0 ||
this.currentStep == pages.Encoder_1
) {
let axis;
if (this.currentStep == pages.Encoder_0) axis = "axis0";
if (this.currentStep == pages.Encoder_1) axis = "axis1";
fetchParam(this.odrive + axis + ".encoder.is_ready");
await wait(100);
if (getVal(this.odrive + axis + ".encoder.is_ready") == false) {
this.choiceMade = false;
}
else {
this.choiceMade = true;
}
}
else {
this.choiceMade = true;
}
this.currentStep.choiceMade = this.choiceMade;
console.log(JSON.parse(JSON.stringify(this.wizardConfig)));
},
undoChoice(e) {
this.choiceMade = false;
this.currentStep.choiceMade = this.choiceMade;
this.nullConfig(this.wizardConfig, e.configStub);
console.log(JSON.parse(JSON.stringify(this.wizardConfig)));
},
updateConfig(config, configStub) {
// iterate over keys in configStub
if (configStub != null) {
Object.keys(configStub).forEach((key) => {
if (typeof configStub[key] == "object") {
this.updateConfig(config[key], configStub[key]);
} else {
config[key] = configStub[key];
}
});
}
},
nullConfig(config, configStub) {
// iterate over keys in configStub
if (configStub != null) {
console.log("NullConfig");
Object.keys(configStub).forEach((key) => {
console.log(key);
if (typeof configStub[key] == "object") {
this.nullConfig(config[key], configStub[key]);
} else {
config[key] = null;
}
});
}
},
next() {
console.log("next");
if (this.choiceMade == true) {
this.currentStep = pages[this.currentStep.next];
this.choiceMade = false;
}
this.calStatus = undefined;
},
finish() {
console.log("finish");
this.currentStep = pages.End;
},
back() {
console.log("back");
this.currentStep = pages[this.currentStep.back];
this.choiceMade = false;
},
},
beforeDestroy() {
for (const page of Object.keys(pages)) {
pages[page].choiceMade = false;
}
let erase = (obj) => {
Object.keys(obj).forEach((key) => {
if (typeof obj[key] == "object" && obj[key] != null) {
erase(obj[key]);
} else {
obj[key] = null;
}
});
};
erase(this.wizardConfig);
},
};
</script>
<style scoped>
.wizard {
background-color: var(--bg-color);
height: 100vh;
max-height: 100vh;
width: 100vw;
padding-top: var(--top-height);
padding-bottom: var(--bottom-height);
}
.wizard-nav {
display: flex;
flex-direction: column;
height: fit-content;
}
.wizard-nav-title {
text-align: center;
font-weight: bold;
margin-bottom: 0.4rem;
}
.wizard-link {
margin: 0.2rem;
}
.active-link {
text-decoration: underline;
}
.wizard-container {
display: flex;
flex-direction: row;
height: 95vh;
}
.wizard-page {
margin: auto;
}
.wizard-controls {
margin-top: 0;
display: flex;
justify-content: center;
}
.active {
background-color: var(--fg-color);
}
.inactive {
background-color: var(--bg-color);
color: white;
}
.wizard-button:active {
background-color: var(--bg-color);
}
.next-green {
color: green;
font-weight: bold;
}
.vue-tooltip.tooltip-custom {
background-color: lightyellow; /* var(--fg-color); */
border-radius: 0px;
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.4);
font-family: "Roboto", sans-serif;
color: black;
}
.vue-tooltip.tooltip-custom .tooltip-arrow {
display: none;
}
.vue-tooltip.fade-in {
opacity: 0;
animation: fadeIn ease 0.25s;
animation-fill-mode: both;
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.finished-page {
color: green;
}
</style>