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
+28
View File
@@ -0,0 +1,28 @@
.DS_Store
node_modules
/dist
# local env files
.env.local
.env.*.local
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
#Electron-builder output
/dist_electron
#Electron icon generator output
/build
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020 ODrive Robotics
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+63
View File
@@ -0,0 +1,63 @@
# odrive_gui
Python requirements: `pip install flask flask_socketio flask_cors odrive`
If the default odrive python package is not desired, the path to the modules can be passed as command line arguments.
example on windows 10:
```
./odrive_gui_win.exe C:/Users/<you>/ODrive/tools C:/Users/<you>/ODrive/Firmware
```
The first argument is for your local version of odrivetool, the second is for fibre.
## Development and testing instructions
### Project setup
```
npm install
```
### Compiles and hot-reloads for development
```
npm run serve
```
### Lints and fixes files
```
npm run lint
```
### Serve electron version of GUI
```
npm run electron:serve
```
### Package electron app into executable
```
npm run electron:build
```
### Build electron app for all platforms
```
npm run electron:build -- -mwl
```
### Running from source
On the devel git branch, there may be unreleased changes to dependencies like fibre or the ODrive enumerations.
Use this command to launch the GUI with the dependencies from the repo:
```
npm run electron:serve -- ../tools/
```
### Building for rpi and potentially other ARM platform devices
PhantomJS is required as a dependency, so it must be installed first:
```
sudo apt install phantomjs
```
After it is installed, `npm run electron:build` can be used to build the AppImage for ARM
### Customize configuration
See [Configuration Reference](https://cli.vuejs.org/config/).
+5
View File
@@ -0,0 +1,5 @@
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset'
]
}
+83
View File
@@ -0,0 +1,83 @@
# fibre-js
This directory provides JavaScript bindings for [Fibre](https://github.com/samuelsadok/fibre). Its home is located [here](https://github.com/samuelsadok/fibre/tree/master/js). There's also a standalone repository for this directory [here](https://github.com/samuelsadok/fibre-js).
## Current Status
So far fibre-js was only tested to run in Chrome but it should work in any browser that [supports WebAssembly](https://caniuse.com/wasm).
fibre-js currently supports the following transport providers:
- WebUSB ([Chrome and Edge only](https://caniuse.com/webusb))
## How to use
So you want your website or Electron App to connect to some resource(s) using Fibre. Let's start with a simple HTML scaffolding.
```HTML
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>fibre-js Test</title>
</head>
<body>
<h1 id="statusText">not connected</h1>
<button id="connectBtn" disabled>Connect...</button>
</body>
</html>
```
Now, right before the closing `</body>`, insert a script tag to load and open fibre-js:
```HTML
<script type="module">
import { fibreOpen } from './fibre.js';
fibreOpen().then((libfibre) => {
// fibre-js is ready to use!
});
</script>
```
This will load the files `fibre.js`, `libfibre-wasm.js` and `libfibre-wasm.wasm` from the same directory the HTML file comes from so make sure those files exist.
Now that fibre-js is ready to use we can start discovering remote objects. For this we need to specify a filter string which tells Fibre where to look for objects. In this example we want to talk to a USB device that runs Fibre (more specfically an [ODrive](https://odriverobotics.com/)). The parameters we use here are something you can find out from reading the docs of your USB device or inspecting the device by using `lsusb` on a Linux or macOS computer.
```JS
const filter = 'usb:idVendor=0x1209,idProduct=0x0D32,bInterfaceClass=0,bInterfaceSubClass=1,bInterfaceProtocol=0';
const onFoundObject = async (obj) => {
console.log("found an object!", obj);
// TODO: do something with obj
}
libfibre.startDiscovery(filter, onFoundObject);
```
Now that we have the object we can start calling functions on it. Let's assume our object has the property `vbus_voltage`. In the `onFoundObject` handle, add:
```JS
while (true) {
document.getElementById("statusText").innerHTML = "vbus_voltage: " + (await obj.vbus_voltage.read());
await new Promise((resolve) => setTimeout(resolve, 100));
}
```
This will read the `vbus_voltage` property every 100ms and display the value on the web page.
Optionally you could add a callback to the `obj._onLost` promise in order to gracefully stop polling when the device disconnects.
Since we're using WebUSB in this example, there's one more thing left to do: The user needs to authorize the website (only once) to access the USB device. This must happen on a user gesture, which is why earlier we put a Connect-button into our HTML.
So directly after the call to `libfibre.startDiscovery()`, add:
```JS
document.getElementById("connectBtn").onclick = libfibre.usbDiscoverer.showDialog;
document.getElementById("connectBtn").removeAttribute('disabled');
```
That's it! You can now open the HTML page in the browser and should see something like this (using Chrome 86 on Linux):
![Example](example.gif)
Note how the user doesn't need to manually reselect the device after the website is reloaded. The website is now already authorized to access the device so Fibre immediately connects to it. The same is true when the device is unpluggend and replugged to the computer.
You can find the complete example code [here](example.html). A more elaborate example which can connect to multiple objects can be found here [here](multidevice_example.html).
Binary file not shown.

After

Width:  |  Height:  |  Size: 173 KiB

@@ -0,0 +1,28 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>fibre-js Test</title>
</head>
<body>
<h1 id="statusText">not connected</h1>
<button id="connectBtn" disabled>Connect...</button>
<script type="module">
import { fibreOpen } from './fibre.js';
fibreOpen().then((libfibre) => {
const filter = 'usb:idVendor=0x1209,idProduct=0x0D32,bInterfaceClass=0,bInterfaceSubClass=1,bInterfaceProtocol=0';
const onFoundObject = async (obj) => {
console.log("found an object!", obj);
while (true) {
document.getElementById("statusText").innerHTML = "vbus_voltage: " + (await obj.vbus_voltage.read());
await new Promise((resolve) => setTimeout(resolve, 100));
}
}
libfibre.startDiscovery(filter, onFoundObject);
document.getElementById("connectBtn").onclick = libfibre.usbDiscoverer.showDialog;
document.getElementById("connectBtn").removeAttribute('disabled');
});
</script>
</body>
</html>
+724
View File
@@ -0,0 +1,724 @@
import wasm from './libfibre-wasm.js';
function assert(expression) {
if (!expression) {
throw Error("assert failed");
}
}
const FIBRE_STATUS_OK = 0;
const FIBRE_STATUS_CANCELLED = 1;
const FIBRE_STATUS_CLOSED = 2;
const FIBRE_STATUS_INVALID_ARGUMENT = 3;
const FIBRE_STATUS_INTERNAL_ERROR = 4;
const ptrSize = 4;
function _getError(code) {
if (code == FIBRE_CANCELLED) {
return Error("libfibre: Operation Cancelled");
} else if (code == FIBRE_STATUS_CLOSED) {
return Error("libfibre: Closed");
} else if (code == FIBRE_STATUS_INVALID_ARGUMENT) {
return Error("libfibre: Invalid Argument");
} else if (code == FIBRE_STATUS_INTERNAL_ERROR) {
return Error("libfibre: Internal Error");
} else {
return Error("libfibre: Unknown Error " + code);
}
}
class BasicCodec {
constructor(typeName, byteLength, littleEndian) {
this.getSize = () => byteLength;
this.serialize = (libfibre, val) => {
var myBuf = new ArrayBuffer(byteLength);
new DataView(myBuf)['set' + typeName](0, val, littleEndian)
return Array.from(new Uint8Array(myBuf));
}
this.deserialize = (libfibre, buf) => {
var myBuf = new ArrayBuffer(buf.length);
new Uint8Array(myBuf).set(buf);
return new DataView(myBuf)['get' + typeName](0, littleEndian);
}
}
}
const codecs = {
'int8': new BasicCodec('Int8', 1, true),
'uint8': new BasicCodec('Uint8', 1, true),
'int16': new BasicCodec('Int16', 2, true),
'uint16': new BasicCodec('Uint16', 2, true),
'int32': new BasicCodec('Int32', 4, true),
'uint32': new BasicCodec('Uint32', 4, true),
'int64': new BasicCodec('BigInt64', 8, true),
'uint64': new BasicCodec('BigUint64', 8, true),
'float': new BasicCodec('Float32', 4, true),
};
class EofError extends Error {
constructor(msg) {
super(msg);
}
}
class TxStream {
constructor(libfibre, handle) {
this._libfibre = libfibre;
this._handle = handle;
this.isClosed = false;
this._libfibre._txStreamMap[this._handle] = this;
}
write(data) {
assert(this._handle);
return new Promise((resolve, reject) => {
let byteLength = Array.isArray(data) ? data.length : data.byteLength;
if (!Array.isArray(data)) {
data = new Uint8Array(data.buffer, data.byteOffset, data.byteLength)
}
this._buf = [this._libfibre.wasm.malloc(byteLength), byteLength];
this._libfibre.wasm.Module.HEAPU8.set(data, this._buf[0]);
this._writeResolve = resolve;
this._writeReject = reject;
this._libfibre.wasm.Module._libfibre_start_tx(this._handle, this._buf[0], this._buf[1], this._libfibre._onTxCompleted, this._handle);
});
}
async writeAll(data) {
while (true) {
const nWritten = await this.write(data);
data = data.slice(nWritten);
if (data.length == 0) {
break;
}
if (this.isClosed) {
throw EofError(`the TX stream was closed but there are still ${data.length} bytes left to send`);
}
assert(nWritten > 0); // ensure progress
}
}
close(status) {
assert(this._handle);
this._libfibre.wasm.Module._libfibre_close_tx(this._handle, status);
delete this._handle;
}
_complete(status, txEnd) {
const len = txEnd - this._buf[0];
assert(len <= this._buf[1]);
this._libfibre.wasm.free(this._buf[0]);
delete this._buf;
const resolve = this._writeResolve;
const reject = this._writeReject;
delete this._writeResolve;
delete this._writeReject;
this.isClosed = this.isClosed || (status == FIBRE_STATUS_CLOSED);
if (status == FIBRE_STATUS_OK || status == FIBRE_STATUS_CLOSED) {
resolve(len);
} else {
reject(_getError(status));
}
}
}
class RxStream {
constructor(libfibre, handle) {
this._libfibre = libfibre;
this._handle = handle;
this.isClosed = false;
this._libfibre._rxStreamMap[this._handle] = this;
}
read(len) {
assert(this._handle);
return new Promise((resolve, reject) => {
this._buf = [this._libfibre.wasm.malloc(len), len];
this._readResolve = resolve;
this._readReject = reject;
this._libfibre.wasm.Module._libfibre_start_rx(this._handle, this._buf[0], this._buf[1], this._libfibre._onRxCompleted, this._handle);
});
}
async readAll(len) {
let data = [];
while (true) {
let chunk = await this.read(len - data.length);
data.push(...chunk);
if (data.length >= len) {
break;
}
if (this.isClosed) {
throw EofError(`the RX stream was closed but there are still ${len - data.length} bytes left to receive`);
}
assert(chunk.length > 0); // ensure progress
}
return data;
}
close(status) {
assert(this._handle);
this._libfibre.wasm.Module._libfibre_close_rx(this._handle, status);
delete this._handle;
}
_complete(status, rxEnd) {
const len = rxEnd - this._buf[0];
assert(len <= this._buf[1]);
const result = new Uint8Array(this._libfibre.wasm.Module.HEAPU8.buffer, this._buf[0], len).slice(0);
this._libfibre.wasm.free(this._buf[0]);
delete this._buf;
const resolve = this._readResolve;
const reject = this._readReject;
delete this._readResolve;
delete this._readReject;
this.isClosed = this.isClosed || (status == FIBRE_STATUS_CLOSED);
if (status == FIBRE_STATUS_OK || status == FIBRE_STATUS_CLOSED) {
resolve(result);
} else {
reject(_getError(status));
}
}
}
class RemoteInterface {
constructor(libfibre, intfName) {
this._libfibre = libfibre;
this.intfName = intfName;
this._onLost = new Promise((resolve) => this._onLostResolve = resolve);
}
}
const _staleObject = {};
class RemoteAttribute {
constructor(libfibre, handle, subintf, subintfName) {
this._libfibre = libfibre;
this._handle = handle;
this._subintf = subintf;
this._subintfName = subintfName;
}
get(obj) {
let objHandlePtr = this._libfibre.wasm.malloc(ptrSize);
try {
this._libfibre.wasm.Module._libfibre_get_attribute(obj._handle, this._handle, objHandlePtr);
return this._libfibre._objMap[this._libfibre._derefIntPtr(objHandlePtr)];
} finally {
this._libfibre.wasm.free(objHandlePtr);
}
}
}
class RemoteFunction extends Function {
constructor(libfibre, handle, inputArgs, outputArgs) {
let closure = function(...args) {
return closure._call(this, ...args);
}
closure._libfibre = libfibre;
closure._handle = handle;
closure._inputArgs = inputArgs;
closure._outputArgs = outputArgs;
// Return closure instead of the original "this" object that was
// associated with the constructor call.
return Object.setPrototypeOf(closure, new.target.prototype);
}
/**
* @brief Starts a function call on this function.
* @return {TxStream, RxStream, Promise} A TX stream that can be used to
* send data on this function call, an RX stream that can be used to receive
* data on this function call and a promise that will be completed once the
* function call completes.
*/
startCall(obj) {
let _callHandle, _txStreamHandle, _rxStreamHandle;
let completionPromise = new Promise((resolve, reject) => {
let completer = (status) => {
if (status == FIBRE_STATUS_OK) {
resolve();
} else {
reject(_getError(status));
}
};
[_callHandle, _txStreamHandle, _rxStreamHandle] = this._libfibre._withOutputArg(
(_callHandlePtr, _txStreamHandlePtr, _rxStreamHandlePtr) =>
this._libfibre.wasm.Module._libfibre_start_call(obj._handle, this._handle,
_callHandlePtr,
_txStreamHandlePtr, _rxStreamHandlePtr, this._libfibre._onCallCompleted,
this._libfibre._allocRef(completer)));
});
return [
new TxStream(this._libfibre, _txStreamHandle),
new RxStream(this._libfibre, _rxStreamHandle),
completionPromise];
}
async _call(obj, ...args) {
assert(args.length == this._inputArgs.length);
let txBuf = [];
for (let argSpec of this._inputArgs) {
txBuf.push(...argSpec.codec.serialize());
}
const rxLen = this._outputArgs.reduce((a, b) => a + b.codec.getSize(), 0);
assert(Number.isInteger(rxLen));
const [txStream, rxStream, completionPromise] = this.startCall(obj);
// RX must be started before TX
const rxPromise = rxStream.readAll(rxLen);
await txStream.writeAll(txBuf);
let rxBuf = await rxPromise;
await completionPromise;
let outputs = [];
for (let argSpec of this._outputArgs) {
let argLength = argSpec.codec.getSize()
outputs.push(argSpec.codec.deserialize(self._libfibre, rxBuf.slice(0, argLength)))
rxBuf = rxBuf.slice(argLength);
}
if (outputs.length == 1) {
return outputs[0];
} else if (outputs.length > 1) {
return outputs;
}
}
}
class LibFibre {
constructor(wasm) {
this.wasm = wasm;
// The following functions are JavaScript callbacks that get invoked from C
// code (WebAssembly). We convert each function to a pointer which can then
// be passed to C code.
// Function signature codes are documented here:
// https://github.com/aheejin/emscripten/blob/master/site/source/docs/porting/connecting_cpp_and_javascript/Interacting-with-code.rst#calling-javascript-functions-as-function-pointers-from-c
this._onPost = wasm.addFunction((cb, cbCtx) => {
// TODO
console.log("event loop post");
return new Promise((resolve, reject) => {
cb(cbCtx);
resolve();
});
}, 'iii');
this._onCallLater = wasm.addFunction((delay, cb, cbCtx) => {
console.log("event loop call later");
return setTimeout(() => cb(cbCtx), delay * 1000);
}, 'ifii')
this._onCancelTimer = wasm.addFunction((timer) => {
return cancelTimeout(timer);
}, 'ii')
this._onConstructObject = wasm.addFunction((ctx, obj, intf, intfName, intfNameLength) => {
let jsIntf = this._loadJsIntf(intf, this.wasm.UTF8ArrayToString(this.wasm.Module.HEAPU8, intfName, intfNameLength));
this._objMap[obj] = new jsIntf(this, obj);
}, 'viiiii')
this._onDestroyObject = wasm.addFunction((ctx, obj) => {
let jsObj = this._objMap[obj];
delete this._objMap[obj];
delete jsObj._handle;
Object.setPrototypeOf(jsObj, _staleObject);
console.log("destroyed remote object");
jsObj._oldHandle = obj;
jsObj._onLostResolve();
}, 'vii')
this._onStartDiscovery = wasm.addFunction((ctx, discCtx, specs, specsLength) => {
console.log("start discovery");
let discoverer = this._deref(ctx);
discoverer.startChannelDiscovery(
this.wasm.UTF8ArrayToString(this.wasm.Module.HEAPU8, specs, specsLength),
discCtx);
}, 'viiii')
this._onStopDiscovery = wasm.addFunction((ctx, discCtx) => {
console.log("stop discovery");
this._freeRef(ctx);
}, 'viiii')
this._onFoundObject = wasm.addFunction((ctx, obj) => {
let onFoundObject = this._deref(ctx);
onFoundObject(this._objMap[obj]);
}, 'vii')
this._onAttributeAdded = wasm.addFunction((ctx, attr, name, nameNength, subintf, subintfName, subintfNameLength) => {
let jsIntf = this._intfMap[ctx];
let jsAttr = new RemoteAttribute(this, attr, subintf, this.wasm.UTF8ArrayToString(this.wasm.Module.HEAPU8, subintfName, subintfNameLength));
Object.defineProperty(jsIntf.prototype, this.wasm.UTF8ArrayToString(this.wasm.Module.HEAPU8, name, nameNength), {
get: function () { return jsAttr.get(this); }
});
}, 'viiiiiii')
this._onAttributeRemoved = wasm.addFunction((ctx, attr) => {
console.log("attribute removed"); // TODO
}, 'vii')
this._onFunctionAdded = wasm.addFunction((ctx, func, name, nameLength, inputNames, inputCodecs, outputNames, outputCodecs) => {
const jsIntf = this._intfMap[ctx];
const jsInputParams = [...this._decodeArgList(inputNames, inputCodecs)];
const jsOutputParams = [...this._decodeArgList(outputNames, outputCodecs)];
let jsFunc = new RemoteFunction(this, func, jsInputParams, jsOutputParams);
jsIntf.prototype[this.wasm.UTF8ArrayToString(this.wasm.Module.HEAPU8, name, nameLength)] = jsFunc;
}, 'viiiiiiii')
this._onFunctionRemoved = wasm.addFunction((ctx, func) => {
console.log("function removed"); // TODO
}, 'vii')
this._onCallCompleted = wasm.addFunction((ctx, status) => {
let completer = this._freeRef(ctx);
completer(status);
}, 'vii');
this._onTxCompleted = wasm.addFunction((ctx, txStream, status, txEnd) => {
this._txStreamMap[txStream]._complete(status, txEnd);
}, 'viiii')
this._onRxCompleted = wasm.addFunction((ctx, rxStream, status, rxEnd) => {
this._rxStreamMap[rxStream]._complete(status, rxEnd);
}, 'viiii')
const ptr = this.wasm.Module._libfibre_get_version();
const version_array = (new Uint16Array(wasm.Module.HEAPU8.buffer, ptr, 3))
this.version = {
major: version_array[0],
minor: version_array[1],
patch: version_array[2]
};
this._intfMap = {};
this._refMap = {};
this._objMap = {};
this._txStreamMap = {};
this._rxStreamMap = {};
this._handle = this.wasm.Module._libfibre_open(
this._onPost,
0,
0,
this._onCallLater,
this._onCancelTimer,
this._onConstructObject,
this._onDestroyObject
);
this.usbDiscoverer = new WebUsbDiscoverer(this);
let buf = [this.wasm.malloc(4), 4];
try {
let len = this.wasm.stringToUTF8Array("usb", this.wasm.Module.HEAPU8, buf[0], buf[1]);
this.wasm.Module._libfibre_register_discoverer(this._handle, buf[0], len,
this._onStartDiscovery, this._onStopDiscovery, this._allocRef(this.usbDiscoverer));
} finally {
this.wasm.free(buf[0]);
}
}
addChannels(mtu, channelDiscoveryHandle) {
console.log("add channel with mtu " + mtu);
const [txChannelId, rxChannelId] = this._withOutputArg((txChannelIdPtr, rxChannelIdPtr) =>
this.wasm.Module._libfibre_add_channels(this._handle, channelDiscoveryHandle, txChannelIdPtr, rxChannelIdPtr, mtu)
);
return [
new RxStream(this, txChannelId),
new TxStream(this, rxChannelId)
];
}
startDiscovery(filter, onFoundObject) {
let buf = [this.wasm.malloc(filter.length + 1), filter.length + 1];
try {
let len = this.wasm.stringToUTF8Array(filter, this.wasm.Module.HEAPU8, buf[0], buf[1]);
this._withOutputArg((discoveryHandlePtr) =>
this.wasm.Module._libfibre_start_discovery(this._handle, buf[0], len,
discoveryHandlePtr,
this._onFoundObject, this._onStopped, this._allocRef(onFoundObject)));
} finally {
this.wasm.free(buf[0]);
}
}
/**
* @brief Allocates a unique integer key for the specified JavaScript object.
*
* The unique key can later be used to look up the JavaScript object using
* _freeRef().
*/
_allocRef(obj) {
let id = 0;
while (++id in this._refMap) {}
console.log("allocating id " + id, obj);
this._refMap[id] = obj;
return id;
}
_deref(id) {
return this._refMap[id];
}
_derefIntPtr(ptr) {
return new Uint32Array(this.wasm.Module.HEAPU8.buffer, ptr, 1)[0]
}
* _getStringList(ptr) {
for (let i = 0; this._derefIntPtr(ptr + i * ptrSize); ++i) {
yield this.wasm.UTF8ArrayToString(this.wasm.Module.HEAPU8, this._derefIntPtr(ptr + i * ptrSize));
}
}
/**
* @bried Deallocates a unique integer key that was previously obtained with
* _allocRef.
*
* @return The JavaScript object that was passed to the corresponding
* _allocRef call.
*/
_freeRef(id) {
let obj = this._refMap[id];
delete this._refMap[id];
console.log("freeing id " + id, obj);
return obj;
}
_withOutputArg(func) {
let ptrs = [];
try {
for (let i = 0; i < func.length; ++i) {
ptrs.push(this.wasm.malloc(ptrSize));
}
func(...ptrs);
return ptrs.map((ptr) => this._derefIntPtr(ptr));
} finally {
for (let ptr of ptrs) {
this.wasm.free(ptr);
}
}
}
* _decodeArgList(argNames, codecNames) {
argNames = [...this._getStringList(argNames)];
codecNames = [...this._getStringList(codecNames)];
assert(argNames.length == codecNames.length);
for (let i in argNames) {
yield {
name: argNames[i],
codecName: codecNames[i],
codec: codecs[codecNames[i]]
};
}
}
_loadJsIntf(intfHandle, intfName) {
if (intfHandle in this._intfMap) {
return this._intfMap[intfHandle];
} else {
class RemoteObject extends RemoteInterface {
constructor(libfibre, handle) {
super(libfibre, intfName);
this._handle = handle;
}
};
let jsIntf = RemoteObject;
this._intfMap[intfHandle] = jsIntf;
this.wasm.Module._libfibre_subscribe_to_interface(intfHandle,
this._onAttributeAdded,
this._onAttributeRemoved,
this._onFunctionAdded,
this._onFunctionRemoved,
intfHandle);
return jsIntf;
}
}
}
export function fibreOpen() {
return new Promise(async (resolve) => {
let Module = {
preRun: [function() {Module.ENV.FIBRE_LOG = "4"}], // enable logging
instantiateWasm: async (info, receiveInstance) => {
const isWebpack = typeof __webpack_require__ === 'function';
const wasmPath = isWebpack ? (await import("!!file-loader!./libfibre-wasm.wasm")).default
: "./libfibre-wasm.wasm";
const response = fetch(wasmPath, { credentials: 'same-origin' });
const result = await WebAssembly.instantiateStreaming(response, info);
receiveInstance(result['instance']);
return {};
}
};
Module = await wasm(Module);
await Module.ready;
wasm.addFunction = Module.addFunction;
wasm.stringToUTF8Array = Module.stringToUTF8Array;
wasm.UTF8ArrayToString = Module.UTF8ArrayToString;
wasm.malloc = Module._malloc;
wasm.free = Module._free;
wasm.Module = Module;
resolve(new LibFibre(wasm));
});
}
class WebUsbDiscoverer {
constructor(libfibre) {
this._libfibre = libfibre;
this._discoveryProcesses = [];
if (navigator.usb.onconnect != null) {
console.warn("There was already a subscriber for usb.onconnect.");
}
navigator.usb.onconnect = (event) => {
for (let discovery of this._discoveryProcesses) {
this._consider(event.device, discovery.filter, discovery.handle);
}
}
navigator.usb.ondisconnect = () => console.log("disconnect");
this._filterKeys = {
'idVendor': 'vendorId',
'idProduct': 'productId',
'bInterfaceClass': 'classCode',
'bInterfaceSubClass': 'subclassCode',
'bInterfaceProtocol': 'protocolCode',
};
this.showDialog = async () => {
let filters = this._discoveryProcesses.map((d) => d.filter);
let dev = await navigator.usb.requestDevice({filters: filters});
for (let discovery of this._discoveryProcesses) {
this._consider(dev, discovery.filter, discovery.handle);
}
};
}
async startChannelDiscovery(specs, discoveryHandle) {
let filter = {}
for (let item of specs.split(',')) {
filter[this._filterKeys[item.split('=')[0]]] = item.split('=')[1]
}
this._discoveryProcesses.push({
filter: filter,
handle: discoveryHandle
});
for (let dev of await navigator.usb.getDevices({filters: [filter]})) {
this._consider(dev, filter, discoveryHandle);
}
}
async _consider(device, filter, channelDiscoveryHandle) {
if ((filter.vendorId != undefined) && (filter.vendorId != device.vendorId)) {
return;
}
if ((filter.productId != undefined) && (filter.productId != device.productId)) {
return;
}
for (let config of device.configurations) {
if (device.configuration !== null && device.configuration.configurationValue != config.configurationValue) {
continue; // A configuration was already set and it's different from this one
}
for (let intf of config.interfaces) {
for (let alternate of intf.alternates) {
const mismatch = ((filter.classCode != undefined) && (filter.classCode != alternate.interfaceClass))
|| ((filter.subclassCode != undefined) && (filter.subclassCode != alternate.interfaceSubclass))
|| ((filter.protocolCode != undefined) && (filter.protocolCode != alternate.interfaceProtocol));
if (mismatch) {
continue;
}
await device.open();
await device.selectConfiguration(config.configurationValue);
await device.claimInterface(intf.interfaceNumber);
let epOut = null, epIn = null;
for (let ep of alternate.endpoints) {
if (ep.type == "bulk" && ep.direction == "in")
epIn = ep;
else if (ep.type == "bulk" && ep.direction == "out")
epOut = ep;
}
device.knownInEndpoints = device.knownInEndpoints || [];
device.knownOutEndpoints = device.knownOutEndpoints || [];
console.log(device.knownOutEndpoints);
if (device.knownInEndpoints.indexOf(epIn.endpointNumber) >= 0) {
continue;
}
if (device.knownOutEndpoints.indexOf(epOut.endpointNumber) >= 0) {
continue;
}
device.knownInEndpoints.push(epIn.endpointNumber);
device.knownOutEndpoints.push(epOut.endpointNumber);
let mtu = Math.min(epIn.packetSize, epOut.packetSize);
const [txChannel, rxChannel] = this._libfibre.addChannels(mtu, channelDiscoveryHandle);
this._connectBulkInEp(device, epIn, rxChannel);
this._connectBulkOutEp(device, epOut, txChannel);
}
}
}
}
async _connectBulkOutEp(dev, ep, stream) {
while (true) {
//console.log("waiting for data from libfibre...");
const data = await stream.read(ep.packetSize);
//console.log("forwarding " + data.length + " bytes to USB...");
//console.log(data);
let result;
try {
result = await dev.transferOut(ep.endpointNumber, data);
} catch (e) { // TODO: propagate actual transfer errors
result = {status: "stall"};
}
assert(result.bytesWritten == data.length);
console.log(dev.opened);
if (!dev.opened || result.status == "stall") {
stream.close(FIBRE_STATUS_CLOSED);
break;
}
assert(result.status == "ok")
};
}
async _connectBulkInEp(dev, ep, stream) {
while (true) {
//console.log("waiting for up to " + epIn.packetSize + " bytes from USB...");
let result;
try {
result = await dev.transferIn(ep.endpointNumber, ep.packetSize);
} catch (e) { // TODO: propagate actual transfer errors
result = {status: "stall"};
}
//console.log("got for data from USB...");
console.log(dev.opened);
console.log(dev);
if (!dev.opened || result.status == "stall") {
stream.close(FIBRE_STATUS_CLOSED);
break;
}
assert(result.status == "ok");
//console.log("forwarding " + result.data.byteLength + " bytes to libfibre...");
const len = await stream.write(result.data);
assert(len == result.data.byteLength);
}
}
}
File diff suppressed because it is too large Load Diff
Binary file not shown.
@@ -0,0 +1,47 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>WebAssembly Test</title>
<style>
div {
font-size : 30px; text-align : left; color:darkblue;
}
</style>
</head>
<body>
<div id="textcontent">loading...</div>
<script src="fibre.js"></script>
<script async>
const filter = 'usb:idVendor=0x1209,idProduct=0x0D32,bInterfaceClass=0,bInterfaceSubClass=1,bInterfaceProtocol=0';
let showStatus = (status) => document.getElementById("textcontent").innerHTML = status;
var connectedObjects = [];
displayLoop = async () => {
while (connectedObjects.length) {
console.log(connectedObjects.length);
let results = await Promise.all(connectedObjects.map(async (dev) => await dev.vbus_voltage.read()));
showStatus('connected to ' + results.length + ' devices' + results.map((x) => "<br>vbus_voltage: " + x + "V").join());
await new Promise((resolve) => setTimeout(resolve, 100));
}
showStatus("no devices connected");
}
let onFoundObject = async (obj) => {
connectedObjects.push(obj);
obj._onLost.then(() => connectedObjects.splice(connectedObjects.indexOf(obj), 1));
if (connectedObjects.length == 1) {
displayLoop();
}
}
fibreOpen().then((libfibre) => {
libfibre.startDiscovery(filter, onFoundObject);
document.getElementById("connectBtn").onclick = libfibre.usbDiscoverer.showDialog;
document.getElementById("connectBtn").removeAttribute('disabled');
showStatus("loaded WASM libfibre version " + libfibre.version.major + "." + libfibre.version.minor + "." + libfibre.version.patch);
});
</script>
<button id="connectBtn" disabled>Connect</button>
</body>
</html>
@@ -0,0 +1,19 @@
{
"name": "fibre-js",
"version": "0.1.0",
"eslintConfig": {
"rules": {
"no-empty": "off",
"no-unused-vars": "off",
"no-undef": "off",
"no-constant-condition": "off",
"no-extra-semi": "off",
"no-async-promise-executor": "off",
"no-redeclare": "off",
"no-prototype-builtins": "off",
"no-global-assign": "off",
"no-useless-escape": "off",
"no-useless-catch": "off"
}
}
}
File diff suppressed because it is too large Load Diff
+70
View File
@@ -0,0 +1,70 @@
{
"name": "ODriveGUI",
"version": "0.1.0",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint",
"enumGenerate": "node scripts/enumGenerate.js",
"preelectron:build": "electron-icon-builder --input=./public/icon.png --output=build --flatten && npm run enumGenerate",
"electron:build": "vue-cli-service electron:build",
"preelectron:serve": "npm run enumGenerate",
"electron:serve": "vue-cli-service electron:serve",
"postinstall": "electron-builder install-app-deps",
"postuninstall": "electron-builder install-app-deps",
"electron:generate-icons": "electron-icon-builder --input=./public/icon.png --output=build --flatten"
},
"main": "background.js",
"dependencies": {
"chart.js": "^2.9.3",
"chartjs-plugin-streaming": "^1.8.0",
"core-js": "^3.6.5",
"electron": "^15.5.5",
"file-saver": "^2.0.2",
"socket.io-client": "^3.0.4",
"typeface-roboto": "0.0.75",
"uuid": "^8.2.0",
"vue": "^2.6.11",
"vue-chartjs": "^3.5.0",
"vue-context": "^5.2.0",
"vue-directive-tooltip": "^1.6.3",
"vue-json-component": "^0.4.1",
"vue-slider-component": "^3.2.11",
"vuex": "^3.5.1"
},
"devDependencies": {
"@vue/cli-plugin-babel": "~4.4.0",
"@vue/cli-plugin-eslint": "~4.4.0",
"@vue/cli-plugin-router": "^4.4.6",
"@vue/cli-service": "~4.4.0",
"babel-eslint": "^10.1.0",
"electron": "^15.5.5",
"electron-devtools-installer": "^3.1.0",
"electron-icon-builder": "^1.0.2",
"eslint": "^6.7.2",
"eslint-plugin-vue": "^6.2.2",
"vue-cli-plugin-electron-builder": "~2.0.0-rc.4",
"vue-cli-plugin-yaml": "^1.0.2",
"vue-template-compiler": "^2.6.11"
},
"eslintConfig": {
"root": true,
"env": {
"node": true
},
"extends": [
"plugin:vue/essential",
"eslint:recommended"
],
"parserOptions": {
"parser": "babel-eslint"
},
"rules": {}
},
"browserslist": [
"> 1%",
"last 2 versions",
"not dead"
]
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

+17
View File
@@ -0,0 +1,17 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title><%= htmlWebpackPlugin.options.title %></title>
</head>
<body>
<noscript>
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>
@@ -0,0 +1,23 @@
const fs = require('fs');
const path = require('path');
fs.readFile(path.resolve(__dirname,'..','..','tools','odrive','enums.py'), 'utf8', function(err, data) {
if (err) throw err;
let enumsString = data;
let lines = enumsString.split(process.platform == 'win32' ? '\r\n' : '\n');
let enums = {};
for (const line of lines) {
if (line != '' && line[0] != '#') {
let name;
let value;
name = line.split('=')[0];
value = line.split('=')[1];
enums[name.trim()] = parseInt(value.trim());
}
}
fs.writeFile(path.resolve(__dirname, '../src/assets/odriveEnums.json'), JSON.stringify(enums, null, 4), function(err) {
if (err) throw err;
});
console.log('Wrote ODrive enums to GUI/src/assets/odriveEnums.json');
});
@@ -0,0 +1,298 @@
import sys
import flask
import os
from flask import make_response, request, jsonify, session
from flask_socketio import SocketIO, send, emit
from flask_cors import CORS
from engineio.payload import Payload
import json
import time
import argparse
import logging
import math
import traceback
# interface for odrive GUI to get data from odrivetool
# Flush stdout by default
# Source:
# https://stackoverflow.com/questions/230751/how-to-flush-output-of-python-print
old_print = print
def print(*args, **kwargs):
kwargs.pop('flush', False)
old_print(*args, **kwargs)
file = kwargs.get('file', sys.stdout)
file.flush() if file is not None else sys.stdout.flush()
app = flask.Flask(__name__)
# disable logging, very noisy!
log = logging.getLogger('werkzeug')
log.disabled = True
app.config['SECRET_KEY'] = 'secret'
app.config.update(
SESSION_COOKIE_SECURE=True,
SESSION_COOKIE_HTTPONLY=True,
SESSION_COOKIE_SAMESITE='None'
)
CORS(app, support_credentials=True)
Payload.max_decode_packets = 500
socketio = SocketIO(app, cors_allowed_origins="*", async_mode = "threading")
#def get_odrive():
# globals()['odrives'] = []
# globals()['odrives'].append(odrive.find_any())
# globals()['odrives'][0].__channel__._channel_broken.subscribe(lambda: handle_disconnect())
# print("odrives found")
# socketio.emit('odrive-found')
async def discovered_device(device):
# when device is discovered, add it to list of serial numbers and global odrive list
# shamelessly lifted from odrive python package
serial_number = await odrive.get_serial_number_str(device)
if serial_number in globals()['discovered_devices']:
index = globals()['discovered_devices'].index(serial_number)
else:
globals()['discovered_devices'].append(serial_number)
index = len(globals()['discovered_devices']) - 1
odrive_name = "odrive" + str(index)
# add to list of odrives
while globals()['inUse']:
time.sleep(0.1)
globals()['odrives'][odrive_name] = device
globals()['odrives_status'][odrive_name] = True
print("Found " + str(serial_number))
print("odrive list: " + str([key for key in globals()['odrives'].keys()]))
# tell GUI the status of known ODrives (previously connected and then disconnected ODrives will be "False")
socketio.emit('odrives-status', json.dumps(globals()['odrives_status']))
# triggers a getODrives socketio message
socketio.emit('odrive-found')
def start_discovery():
print("starting disco loop...")
log = fibre.Logger(verbose = False)
domain = fibre.Domain("usb:idVendor=0x1209,idProduct=0x0D32,bInterfaceClass=0,bInterfaceSubClass=1,bInterfaceProtocol=0")
domain = domain.__enter__()
discovery = domain.run_discovery(discovered_device)
def handle_disconnect(odrive_name):
print("lost odrive")
globals()['odrives_status'][odrive_name] = False
# emit the whole list of odrive statuses
# in the GUI, mark and use status as ODrive state.
socketio.emit('odrives-status', json.dumps(globals()['odrives_status']))
@socketio.on('findODrives')
def getODrives(message):
print("looking for odrive")
start_discovery()
@socketio.on('enableSampling')
def enableSampling(message):
print("sampling enabled")
session['samplingEnabled'] = True
emit('samplingEnabled')
@socketio.on('stopSampling')
def stopSampling(message):
session['samplingEnabled'] = False
emit('samplingDisabled')
@socketio.on('sampledVarNames')
def sampledVarNames(message):
session['sampledVars'] = message
print(session['sampledVars'])
@socketio.on('startSampling')
def sendSamples(message):
print(session['samplingEnabled'])
while session['samplingEnabled']:
emit('sampledData', json.dumps(getSampledData(session['sampledVars'])))
time.sleep(0.02)
@socketio.on('message')
def handle_message(message):
print(message)
emit('response', 'hello from server!')
@socketio.on('getODrives')
def get_odrives(data):
# spinlock
while globals()['inUse']:
time.sleep(0.1)
globals()['inUse'] = True
odriveDict = {}
#for (index, odrv) in enumerate(globals()['odrives']):
# odriveDict["odrive" + str(index)] = dictFromRO(odrv)
for key in globals()['odrives_status'].keys():
if globals()['odrives_status'][key] == True:
odriveDict[key] = dictFromRO(globals()['odrives'][key])
globals()['inUse'] = False
emit('odrives', json.dumps(odriveDict))
@socketio.on('getProperty')
def get_property(message):
# message is dict natively
# will be {"path": "odriveX.axisY.blah.blah"}
while globals()['inUse']:
time.sleep(0.1)
if globals()['odrives_status'][message["path"].split('.')[0]]:
globals()['inUse'] = True
val = getVal(globals()['odrives'], message["path"].split('.'))
globals()['inUse'] = False
emit('ODriveProperty', json.dumps({"path": message["path"], "val": val}))
@socketio.on('setProperty')
def set_property(message):
# message is {"path":, "val":, "type":}
while globals()['inUse']:
time.sleep(0.1)
globals()['inUse'] = True
print("From setProperty event handler: " + str(message))
postVal(globals()['odrives'], message["path"].split('.'), message["val"], message["type"])
val = getVal(globals()['odrives'], message["path"].split('.'))
globals()['inUse'] = False
emit('ODriveProperty', json.dumps({"path": message["path"], "val": val}))
@socketio.on('callFunction')
def call_function(message):
# message is {"path"}, no args yet (do we know which functions accept arguments from the odrive tree directly?)
while globals()['inUse']:
time.sleep(0.1)
print("From callFunction event handler: " + str(message))
globals()['inUse'] = True
callFunc(globals()['odrives'], message["path"].split('.'))
globals()['inUse'] = False
@app.route('/', methods=['GET'])
def home():
return "<h1>ODrive GUI Server</h1>"
def dictFromRO(RO):
# create dict from an odrive RemoteObject that's suitable for sending as JSON
returnDict = {}
for key in dir(RO):
v = getattr(RO, key)
if not key.startswith('_') and isinstance(v, fibre.libfibre.RemoteObject):
# recurse
returnDict[key] = dictFromRO(v)
elif key.startswith('_') and key.endswith('_property'):
# grab value of that property
# indicate if this property can be written or not
val = str(v.read())
_type = str(v.read._outputs[0][1])
if val == "inf":
val = "Infinity"
_type = "str"
elif val == "-inf":
val = "-Infinity"
_type = "str"
returnDict[key[1:-9]] = {"val": val,
"readonly": not hasattr(v, 'exchange'),
"type": _type}
elif not key.startswith('_') and hasattr(v, '__call__'):
# this is a function - do nothing for now.
print('found a function!',key)
returnDict[key] = "function"
return returnDict
def postVal(odrives, keyList, value, argType):
# expect a list of keys in the form of ["key1", "key2", "keyN"]
# "key1" will be "odriveN"
# like this: postVal(odrives, ["odrive0","axis0","config","calibration_lockin","accel"], 17.0)
try:
odrv = keyList.pop(0)
RO = odrives[odrv]
keyList[-1] = '_' + keyList[-1] + '_property'
for key in keyList:
RO = getattr(RO, key)
if argType == "number":
RO.exchange(float(value))
elif argType == "boolean":
RO.exchange(value)
elif argType == "string":
if value == "Infinity":
RO.exchange(math.inf)
elif value == "-Infinity":
RO.exchange(-math.inf)
else:
pass # dont support that type yet
except fibre.ObjectLostError:
handle_disconnect(odrv)
except Exception as ex:
print("exception in postVal: ", traceback.format_exc())
def getVal(odrives, keyList):
try:
odrv = keyList.pop(0)
RO = odrives[odrv]
keyList[-1] = '_' + keyList[-1] + '_property'
for key in keyList:
RO = getattr(RO, key)
retVal = RO.read()
if retVal == math.inf:
retVal = "Infinity"
elif retVal == -math.inf:
retVal = "-Infinity"
return retVal
except fibre.ObjectLostError:
handle_disconnect(odrv)
except Exception as ex:
print("exception in getVal: ", traceback.format_exc())
return 0
def getSampledData(vars):
#use getVal to populate a dict
#return a dict {path:value}
samples = {}
for path in vars["paths"]:
keys = path.split('.')
samples[path] = getVal(globals()['odrives'], keys)
return samples
def callFunc(odrives, keyList):
try:
#index = int(''.join([char for char in keyList.pop(0) if char.isnumeric()]))
odrv = keyList.pop(0)
RO = odrives[odrv]
for key in keyList:
RO = getattr(RO, key)
if hasattr(RO, '__call__'):
RO.__call__()
except fibre.ObjectLostError:
handle_disconnect(odrv)
except:
print("fcn call failed")
if __name__ == "__main__":
print("args from python: " + str(sys.argv[1:0]))
#print(sys.argv[1:])
# try to import based on command line arguments or config file
for optPath in sys.argv[1:]:
print("adding " + str(optPath.rstrip()) + " to import path for odrive_server.py")
sys.path.insert(0,optPath.rstrip())
import odrive
import odrive.utils # for dump_errors()
import fibre
# global for holding references to all connected odrives
globals()['odrives'] = {}
# global dict {'odriveX': True/False} where True/False reflects status of connection
# on handle_disconnect, set it to False. On connection, set it to True
globals()['odrives_status'] = {}
globals()['discovered_devices'] = []
# spinlock
globals()['inUse'] = False
log = fibre.Logger(verbose=False)
shutdown = fibre.Event()
socketio.run(app, host='0.0.0.0', port=5000)
+571
View File
@@ -0,0 +1,571 @@
<template>
<div id="app">
<!-- HEADER -->
<div class="header" id="header">
<button class="dash-button menu-button" @click="toggleMenu">Menu</button>
<div class="card menu" id="menu" :style="{top: menuTop}" v-show="showMenu">
<button class="dash-button menu-item" @click="exportDash">Export dash</button>
<button class="dash-button menu-item" @click="importDashWrapper">
Import dash
<input
type="file"
id="inputDash"
ref="fileInput"
@change="
importDashFile($event.target.files);
$refs.fileInput.value = null;
"
value
style="display: none"
/>
</button>
<button class="dash-button menu-item" id="importButton" @click="calcImportLeft();calcImportTop();toggleImport()">Import ODrive config</button>
<button class="dash-button menu-item" id="exportButton" @click="calcExportLeft();calcExportTop();toggleExport()">Export ODrive config</button>
</div>
<div class="card import-menu" :style="{top: importTop, left: importLeft}" v-show="showImport">
<button v-for="odrive in Object.keys(odrives)" :key="odrive" class="dash-button" @click="importConfigWrapper">
{{odrive}}
<input
type="file"
id="inputConfig"
ref="fileInputConfig"
@change="
importConfigFile($event.target, odrive);
"
onclick="this.value=null"
value
style="display: none"
/>
</button>
</div>
<div class="card export-menu" :style="{top: exportTop, left: exportLeft}" v-show="showExport">
<button v-for="odrive in Object.keys(odrives)" :key="odrive" class="dash-button" @click="exportConfig(odrive)">{{odrive}}</button>
</div>
<button
v-for="dash in dashboards"
:key="dash.id"
:class="['dash-button', { active: currentDash === dash.name }]"
v-on:click.self="changeDash(dash.name)"
v-on:dblclick="changeDashName(dash.id)"
>
<button
v-if="
dash.name !== 'Start' &&
dash.name !== 'Tuning' &&
dash.name !== 'Wizard'
"
class="close-button"
v-on:click="deleteDash(dash.id)"
>
X
</button>
{{ dash.name }}
</button>
<button class="dash-button dash-add" @click="addDash">+</button>
<button
class="dash-button sample-button"
:class="[{ active: sampling === true }]"
@click="sampleButton"
>
{{ samplingText }}
</button>
</div>
<!-- PAGE CONTENT -->
<component
:is="currentDashName"
:odrives="odrives"
:dash="dash"
></component>
<!-- FOOTER -->
<div class="footer">
<Axis
v-for="axis in axes"
:key="axis.name"
:axis="axis.name"
:odrives="odrives"
></Axis>
</div>
</div>
</template>
<script>
import Start from "./views/Start.vue";
import Dashboard from "./views/Dashboard.vue";
import Axis from "./components/Axis.vue";
import Wizard from "./views/Wizard.vue";
import * as socketio from "./comms/socketio";
import { saveAs } from "file-saver";
import { v4 as uuidv4 } from "uuid";
import { pathsFromTree } from "./lib/utils.js";
import { getVal, putVal } from "./lib/odrive_utils.js";
export default {
name: "App",
components: {
Start,
Dashboard,
Axis,
Wizard,
},
data: function () {
return {
//currentDash: "Start",
showMenu: false,
showImport: false,
showExport: false,
menuTop: '0px',
importTop: '0px',
importLeft: '0px',
exportTop: '0px',
exportLeft: '0px',
};
},
mounted() {
var elem = document.getElementById('header');
this.menuTop = elem.offsetHeight + 'px';
},
computed: {
currentDashName: function () {
//get the appropriate component name from the currentDash variable
let comp = {};
for (const dash of this.dashboards) {
if (dash.name === this.$store.state.currentDash) {
comp = dash.component;
}
}
return comp;
},
dash: function () {
let comp = {};
for (const dash of this.dashboards) {
if (dash.name === this.$store.state.currentDash) {
comp = dash;
}
}
return comp;
},
currentCtrlList: function () {
let comp = {};
for (const dash of this.dashboards) {
if (dash.name === this.$store.state.currentDash) {
comp = dash.controls;
}
}
return comp;
},
axes: function () {
return this.$store.state.axes;
},
odrives: function () {
return this.$store.state.odrives;
},
dashboards: function () {
return this.$store.state.dashboards;
},
sampling: function () {
return this.$store.state.sampling;
},
currentDash: function () {
return this.$store.state.currentDash;
},
samplingText: function () {
let ret;
if (this.$store.state.sampling) {
ret = "stop sampling";
} else {
ret = "start sampling";
}
return ret;
},
},
methods: {
toggleMenu() {
this.showMenu = !this.showMenu;
if (!this.showMenu) {
this.showImport = false;
this.showExport = false;
}
},
toggleImport() {
this.showImport = !this.showImport;
this.showExport = false;
},
toggleExport() {
this.showExport = !this.showExport;
this.showImport= false;
},
importConfigWrapper() {
const inputElem = document.getElementById("inputConfig");
if (inputElem) {
console.log("importing config");
inputElem.click();
}
},
importConfigFile(event, odrive) {
console.log("Importing config to odrive " + odrive);
let file = event.files[0];
const reader = new FileReader();
// this is ugly, but it gets around scoping problems the "load" callback
let importConfig = (config) => {
let paths = pathsFromTree(config);
for (const path of paths) {
let val = config;
for(const key of path.split('.')) {
val = val[key];
}
putVal(odrive + '.' + path, val);
}
console.log("Config imported to odrive " + odrive);
};
reader.addEventListener("load", function (e) {
let importString = e.target.result;
importString = importString.replace(/ Infinity/g, '" Infinity"');
importString = importString.replace(/ -Infinity/g, '" -Infinity"');
importConfig(JSON.parse(importString));
});
reader.readAsText(file);
},
exportConfig(odrive) {
console.log("Exporting config from odrive " + odrive);
let configPaths = [];
let exportConfig = {};
let paramTree = this.$store.state.odriveConfigs.params[odrive];
for (const path of pathsFromTree(paramTree)) {
if (path.includes('config.') && !path.includes('mapping')) {
configPaths.push(path);
}
}
for (const path of configPaths) {
let keys = path.split('.');
let val = String(getVal(odrive + '.' + path));
// super ugly! Need a better way to do this and handle tuples in odrive_utils
if (val.includes('True')) {
val = true;
}
else if (val.includes('False')) {
val = false;
}
else if (val.includes('.')) {
val = parseFloat(val);
}
else if (!val.includes(',') && !val.includes('Infinity')){
val = parseInt(val);
}
let obj = exportConfig;
for (const key of keys.slice(0,-1)) {
if (!Object.keys(obj).includes(key)) {
obj[key] = {}
}
obj = obj[key];
}
obj[keys.pop()] = val;
}
let exportString = JSON.stringify(exportConfig, null, 2);
if (exportString.includes('"Infinity"')) {
console.log("found infinity!");
}
console.log(typeof exportString);
exportString = exportString.replace(/"Infinity"/g, 'Infinity');
exportString = exportString.replace(/"-Infinity"/g, '-Infinity');
const blob = new Blob([exportString], {
type: "application/json",
});
saveAs(blob, odrive + '-config.json');
},
calcImportLeft() {
let elem = document.getElementById('menu');
this.importLeft = elem.offsetWidth + 'px';
},
calcImportTop() {
this.importTop = document.getElementById('importButton').getBoundingClientRect().top + 'px';
},
calcExportLeft() {
let elem = document.getElementById('menu')
this.exportLeft = elem.offsetWidth + 'px';
},
calcExportTop() {
this.exportTop = document.getElementById('exportButton').getBoundingClientRect().top + 'px';
},
changeDash(dashName) {
console.log(dashName);
this.$store.commit("setDash", dashName);
},
addDash() {
let dashname = "Dashboard " + (this.dashboards.length - 2);
this.dashboards.push({
component: "Dashboard",
name: dashname,
id: uuidv4(),
controls: [],
actions: [],
plots: [],
});
},
deleteDash(dashID) {
this.$store.commit("setDash", "Start");
console.log("Deleting dash " + dashID);
for (const dash of this.dashboards) {
if (dashID === dash.id) {
this.dashboards.splice(this.dashboards.indexOf(dash), 1);
}
}
},
exportDash() {
console.log("exporting dashboard");
const blob = new Blob([JSON.stringify(this.dash, null, 2)], {
type: "application/json",
});
saveAs(blob, this.dash.name);
},
importDashWrapper() {
const inputElem = document.getElementById("inputDash");
if (inputElem) {
console.log("importing dashboard");
inputElem.click();
}
},
importDashFile(files) {
console.log("file handler callback");
let file = files[0];
const reader = new FileReader();
// this is ugly, but it gets around scoping problems the "load" callback
let dashes = this.dashboards;
let addImportedDash = (dash) => {
console.log(dash);
dashes.push(dash);
// plots will have variables associated, add them to sampled variables list
for (const plot of dash.plots) {
console.log(plot);
for (const plotVar of plot.vars) {
console.log(plotVar);
//addsampledprop(path);
this.$store.commit("addSampledProperty", plotVar.path);
}
}
};
reader.addEventListener("load", function (e) {
addImportedDash(JSON.parse(e.target.result));
});
reader.readAsText(file);
},
changeDashName(e) {
console.log(e);
console.log("double clicked dashboard name");
},
sampleButton() {
if (this.$store.state.sampling) {
// sampling acttive, stop
socketio.sendEvent({
type: "stopSampling",
});
this.$store.state.sampling = false;
} else {
// sampling inactive, start sampling
socketio.sendEvent({
type: "sampledVarNames",
data: {
paths: this.$store.state.sampledProperties,
},
});
socketio.sendEvent({
type: "enableSampling",
});
this.$store.state.timeSampleStart = Date.now();
this.$store.state.sampling = true;
}
},
startsample() {
socketio.sendEvent({
type: "sampledVarNames",
data: {
paths: this.$store.state.sampledProperties,
},
});
socketio.sendEvent({
type: "enableSampling",
});
this.$store.state.timeSampleStart = Date.now();
this.$store.state.sampling = true;
},
stopsample() {
socketio.sendEvent({
type: "stopSampling",
});
this.$store.state.sampling = false;
},
estop() {
// send stop command to odrives
// behavior on reset?
},
emitFindODrives() {
socketio.sendEvent({
type: "findODrives",
data: {},
});
},
},
created() {
// on app creation, set the address to the default
this.$store.dispatch("setServerAddress", "http://127.0.0.1:5000");
// to allow running as web app
if (window.ipcRenderer != undefined) {
window.ipcRenderer.on("server-stdout", (event, arg) => {
this.$store.commit("logServerMessage", arg);
});
window.ipcRenderer.on("server-stderr", (event, arg) => {
this.$store.commit("logServerMessage", arg);
});
window.ipcRenderer.send("start-server");
}
},
};
</script>
<style>
@import "./assets/styles/vars.css";
@import "./assets/styles/style.css";
* {
/* font-family: Arial, Helvetica, sans-serif; */
font-family: "Roboto", sans-serif;
margin: 0;
padding: 0;
box-sizing: border-box;
}
#app {
height: 100vh;
}
.header {
/* want this fixed and full width */
position: fixed;
top: 0px;
left: 0px;
width: 100vw;
display: flex;
background-color: var(--fg-color);
box-shadow: 0 0px 8px 0 rgba(0, 0, 0, 0.4);
z-index: 1;
}
button {
cursor: pointer;
font-size: 1rem;
color: black;
text-decoration: none;
padding: 10px;
background-color: var(--fg-color);
border-style: none;
outline: none;
}
.dash-button {
cursor: pointer;
font-size: 1rem;
color: black;
text-decoration: none;
padding: 10px;
background-color: var(--fg-color);
border-style: none;
outline: none;
}
.dash-button:active {
background-color: var(--bg-color);
}
.dash-button:hover {
background-color: var(--bg-color);
}
.menu-item {
text-align: left;
}
.active {
color: #000000;
background-color: var(--bg-color);
}
.footer {
position: fixed;
width: 100vw;
left: 0px;
bottom: 0px;
display: flex;
background-color: var(--fg-color);
box-shadow: 0 0px 8px 0 rgba(0, 0, 0, 0.4);
z-index: 1;
}
.odrvSer,
.errorState {
font-weight: bold;
font-family: "Roboto Mono", monospace;
margin: auto 5px;
background-color: var(--fg-color);
}
.errorState {
color: #13a100;
}
.dash-add {
font-weight: bold;
}
.emergency-stop {
margin-left: auto;
padding-left: 2rem;
padding-right: 2rem;
background-color: red;
font-weight: bold;
color: white;
display: none;
}
.sample-button {
margin-left: auto;
}
.odrive-status {
margin-left: auto;
font-family: "Roboto Mono", monospace;
padding: 5px 10px;
}
.menu-button {
border-right: 1px solid grey;
}
.menu {
position: absolute;
margin: 0;
padding: 0;
left: 0;
display: flex;
flex-direction: column;
}
.import-menu {
position: absolute;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
}
.export-menu {
position: absolute;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
}
</style>
@@ -0,0 +1,127 @@
{
"name": "Tuning",
"component": "Dashboard",
"id": "deadb33f",
"controls": [
{
"controlType": "CtrlNumeric",
"path": "odrives.odrive0.axis0.motor.config.current_lim"
},
{
"controlType": "CtrlNumeric",
"path": "odrives.odrive0.axis0.controller.config.vel_limit"
},
{
"controlType": "CtrlSlider",
"path": "odrives.odrive0.axis0.controller.config.pos_gain"
},
{
"controlType": "CtrlSlider",
"path": "odrives.odrive0.axis0.controller.config.vel_gain"
},
{
"controlType": "CtrlSlider",
"path": "odrives.odrive0.axis0.controller.config.vel_integrator_gain"
},
{
"controlType": "CtrlEnum",
"path": "odrives.odrive0.axis0.requested_state",
"options": [
{
"text": "Undefined",
"value": 0
},
{
"text": "Idle",
"value": 1
},
{
"text": "Startup Sequence",
"value": 2
},
{
"text": "Full Calibration Sequence",
"value": 3
},
{
"text": "Motor Calibration",
"value": 4
},
{
"text": "Sensorless Control",
"value": 5
},
{
"text": "Encoder Index Search",
"value": 6
},
{
"text": "Encoder Offset Calibration",
"value": 7
},
{
"text": "Closed Loop Control",
"value": 8
},
{
"text": "Lockin Spin",
"value": 9
},
{
"text": "Encoder Direction Find",
"value": 10
},
{
"text": "Homing",
"value": 11
}
]
},
{
"controlType": "CtrlFunction",
"path": "odrives.odrive0.save_configuration"
}
],
"actions": [
{
"actionType": "Action",
"id": "a699c462-cf8d-4b4e-aedf-0b200e84f97a",
"path": "odrives.odrive0.axis0.controller.pos_setpoint",
"val": 0
},
{
"actionType": "Action",
"id": "db267ae4-fdf4-4770-a2d3-eb61de026659",
"path": "odrives.odrive0.axis0.controller.pos_setpoint",
"val": 50000
}
],
"plots": [
{
"name": "65a2768d-e61e-4dd5-bf80-dba153838f74",
"vars": [
{
"path": "odrives.odrive0.axis0.controller.pos_setpoint",
"color": "#1f77b4"
},
{
"path": "odrives.odrive0.axis0.encoder.pos_estimate",
"color": "#ff7f0e"
}
]
},
{
"name": "d9ee2474-3e53-408f-9eab-1656342eb531",
"vars": [
{
"path": "odrives.odrive0.axis0.controller.vel_setpoint",
"color": "#1f77b4"
},
{
"path": "odrives.odrive0.axis0.encoder.vel_estimate",
"color": "#ff7f0e"
}
]
}
]
}
@@ -0,0 +1,127 @@
{
"name": "Tuning",
"component": "Dashboard",
"id": "deadb33f",
"controls": [
{
"controlType": "CtrlNumeric",
"path": "odrives.odrive0.axis0.motor.config.current_lim"
},
{
"controlType": "CtrlNumeric",
"path": "odrives.odrive0.axis0.controller.config.vel_limit"
},
{
"controlType": "CtrlSlider",
"path": "odrives.odrive0.axis0.controller.config.pos_gain"
},
{
"controlType": "CtrlSlider",
"path": "odrives.odrive0.axis0.controller.config.vel_gain"
},
{
"controlType": "CtrlSlider",
"path": "odrives.odrive0.axis0.controller.config.vel_integrator_gain"
},
{
"controlType": "CtrlEnum",
"path": "odrives.odrive0.axis0.requested_state",
"options": [
{
"text": "Undefined",
"value": 0
},
{
"text": "Idle",
"value": 1
},
{
"text": "Startup Sequence",
"value": 2
},
{
"text": "Full Calibration Sequence",
"value": 3
},
{
"text": "Motor Calibration",
"value": 4
},
{
"text": "Sensorless Control",
"value": 5
},
{
"text": "Encoder Index Search",
"value": 6
},
{
"text": "Encoder Offset Calibration",
"value": 7
},
{
"text": "Closed Loop Control",
"value": 8
},
{
"text": "Lockin Spin",
"value": 9
},
{
"text": "Encoder Direction Find",
"value": 10
},
{
"text": "Homing",
"value": 11
}
]
},
{
"controlType": "CtrlFunction",
"path": "odrives.odrive0.save_configuration"
}
],
"actions": [
{
"actionType": "Action",
"id": "a699c462-cf8d-4b4e-aedf-0b200e84f97a",
"path": "odrives.odrive0.axis0.controller.input_pos",
"val": 0
},
{
"actionType": "Action",
"id": "db267ae4-fdf4-4770-a2d3-eb61de026659",
"path": "odrives.odrive0.axis0.controller.input_pos",
"val": 10
}
],
"plots": [
{
"name": "65a2768d-e61e-4dd5-bf80-dba153838f74",
"vars": [
{
"path": "odrives.odrive0.axis0.controller.input_pos",
"color": "#1f77b4"
},
{
"path": "odrives.odrive0.axis0.encoder.pos_estimate",
"color": "#ff7f0e"
}
]
},
{
"name": "d9ee2474-3e53-408f-9eab-1656342eb531",
"vars": [
{
"path": "odrives.odrive0.axis0.controller.vel_setpoint",
"color": "#1f77b4"
},
{
"path": "odrives.odrive0.axis0.encoder.vel_estimate",
"color": "#ff7f0e"
}
]
}
]
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 169 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 171 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 165 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

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

After

Width:  |  Height:  |  Size: 52 KiB

@@ -0,0 +1,23 @@
.card {
padding: 10px;
margin: 10px;
background-color: #fff;
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.4);
}
.close-button {
font-weight: bold;
cursor: pointer;
padding: 0 5px;
margin-right: 10px;
border: 1px solid black;
}
.close-button:active {
background-color: var(--bg-color);
}
.choice-inactive {
background-color: var(--bg-color);
color: grey;
}
@@ -0,0 +1,6 @@
:root {
--bg-color: rgb(235, 235, 235);
--fg-color: #fff;
--top-height: 39px;
--bottom-height: 29px;
}
@@ -0,0 +1,77 @@
{
"config": {
"brake_resistance": null
},
"axis0": {
"motor": {
"config": {
"pole_pairs": null,
"phase_resistance": null,
"phase_inductance": null,
"motor_type": null,
"current_lim": null,
"torque_constant": null
}
},
"encoder": {
"config": {
"mode": null,
"use_index": null,
"cpr": null,
"calib_scan_distance": null,
"abs_spi_cs_gpio_pin": null
}
},
"controller": {
"config": {
"input_mode": null,
"control_mode": null,
"vel_limit": null,
"inertia": null
}
},
"trap_traj": {
"config": {
"vel_limit": null,
"accel_limit": null,
"decel_limit": null
}
}
},
"axis1": {
"motor": {
"config": {
"pole_pairs": null,
"phase_resistance": null,
"phase_inductance": null,
"motor_type": null,
"current_lim": null,
"torque_constant": null
}
},
"encoder": {
"config": {
"mode": null,
"use_index": null,
"cpr": null,
"calib_scan_distance": null,
"abs_spi_cs_gpio_pin": null
}
},
"controller": {
"config": {
"input_mode": null,
"control_mode": null,
"vel_limit": null,
"inertia": null
}
},
"trap_traj": {
"config": {
"vel_limit": null,
"accel_limit": null,
"decel_limit": null
}
}
}
}
@@ -0,0 +1,929 @@
import odriveEnums from "../odriveEnums.json";
// wizard is an object of pages.
// each page has a number, title, vue component name, next destination, back destination, and array of choices
// certain choices require a hook - for example, hall effect encoder cpr is 6 * motor.config.pole_pairs
// each hook takes a config object, modifies it, and returns it after running appropriate calculations.
// see choiceHandler() in Wizard.vue for how this happens
// Pages can also have custom components displayed below the choice tiles
// Example - on motor pages, there are buttons to run motor calibration and clear errors.
export let pages = {
ODrive: {
title: "Which ODrive do you have?",
link: "ODrive Version",
component: "wizardPage",
next: "Brake",
back: "ODrive",
nextTooltip: "Make a choice!",
choiceMade: false,
choices: [
{
component: "wizardChoice",
data: {
imageURL: require("../images/24v_300x300.png"),
},
title: "ODrive v3.6 24V",
requirements: [],
hooks: [],
tooltip: null,
altTooltip: null,
},
{
component: "wizardChoice",
data: {
imageURL: require("../images/56v_300x300.png"),
},
title: "ODrive v3.6 56V",
requirements: [],
hooks: [],
tooltip: null,
altTooltip: null,
},
],
customComponents: [],
pageComponents: [],
},
Brake: {
title: "Brake Resistor",
link: "Brake Resistor",
component: "wizardPage",
next: "Motor_0",
back: "ODrive",
nextTooltip: "Make a choice!",
choiceMade: false,
choices: [
{
component: "wizardBrake",
data: {},
title: "Brake Resistor",
requirements: [],
hooks: [],
tooltip: "When you slow down a motor, the energy has to go somewhere.\n\r \
It either goes to the brake resistor or back to your power supply.",
altTooltip: null,
},
{
component: "wizardChoice",
data: {
imageURL: null,
configStub: {
config: {
brake_resistance: 0,
}
},
},
title: "I don't have a brake resistor",
hooks: [],
requirements: [],
tooltip: "Only operate without a brake resistor if your power source can handle reverse current, like a battery."
}
],
},
Motor_0: {
title: "Which motor are you using on Axis 0?",
link: "Motor 0",
component: "wizardPage",
next: "Encoder_0",
back: "Brake",
nextTooltip: "ODrive needs to know the inductance and resistance of your motor in order to control it.\
Click the Calibrate Motor button to start the measurement process. The motor will beep\
during calibration.",
choiceMade: false,
choices: [
{
component: "wizardChoice",
data: {
imageURL: require("../images/D5065_300x300.png"),
configStub: {
axis0: {
motor: {
config: {
pole_pairs: 7,
torque_constant: 8.27 / 270,
// R and L come from "Calibrate Motor" button on page
//phase_resistance: 0.039, // from test rig
//phase_inductance: 1.57e-5,
motor_type: odriveEnums.MOTOR_TYPE_HIGH_CURRENT, // MOTOR_TYPE_HIGH_CURRENT
},
},
},
},
},
title: "ODrive D5065",
requirements: [],
hooks: [],
tooltip: "D5065 motor from the ODrive shop",
altTooltip: null,
},
{
component: "wizardChoice",
data: {
imageURL: require("../images/D6374_300x300.png"),
configStub: {
axis0: {
motor: {
config: {
pole_pairs: 7,
torque_constant: 8.27 / 150,
// R and L come from "Calibrate Motor" button on page
//phase_resistance: 0.041, // measurement from PJ
//phase_inductance: 2.23e-5,
motor_type: odriveEnums.MOTOR_TYPE_HIGH_CURRENT, // MOTOR_TYPE_HIGH_CURRENT,
},
},
},
},
},
title: "ODrive D6374",
requirements: [],
hooks: [],
tooltip: "D6374 motor from the ODrive shop",
altTooltip: null,
},
{
component: "wizardMotor",
data: {
axis: "axis0",
},
title: "Other Motor",
requirements: [],
hooks: [],
tooltip: "Bring your own motor!",
altTooltip: null,
},
],
pageComponents: [
{
component: "wizardMotorCal",
id: 0,
data: {
axis: "axis0",
}
},
{
component: "wizardCalStatus",
id: 1,
data: {
success: "Calibration succeeded!",
failure: "Calibration failed",
tooltip: "Double check your motor connections. If the connections are good, it is \
possible that your motor has a higher resistance or lower inductance than ODrive expects."
}
}
]
},
Encoder_0: {
title: "Which encoder are you using for Axis 0?",
link: "Encoder 0",
component: "wizardPage",
next: "Control_0",
back: "Motor_0",
nextTooltip: "ODrive needs to know the relation between the encoder position and motor position in order to function. \
Click the Calibrate Encoder button to start the measurement process. The motor will slowly rotate back \
and forth during calibration.",
choiceMade: false,
choices: [
{
component: "wizardChoice",
data: {
imageURL: require("../images/amt102-v_300x300.png"),
configStub: {
axis0: {
encoder: {
config: {
cpr: 8192,
use_index: true,
mode: odriveEnums.ENCODER_MODE_INCREMENTAL, // ENCODER_MODE_INCREMENTAL
}
}
}
},
},
title: "CUI AMT102V",
requirements: [],
hooks: [],
tooltip: "Incremental 8192cpr encoder from the ODrive shop",
altTooltip: null,
},
{
component: "wizardChoice",
data: {
imageURL: require("../images/hall_effect_300x300.png"),
configStub: {
axis0: {
encoder: {
config: {
use_index: false,
mode: odriveEnums.ENCODER_MODE_HALL,
}
}
}
},
},
title: "Hall Effect",
requirements: [],
hooks: [
// hook takes a config object (normally this.wizardConfig in Wizard.vue), creates and returns a copy
(configObj) => {
let newConfig = configObj;
newConfig.axis0.encoder.config.cpr = 6 * newConfig.axis0.motor.config.pole_pairs;
return newConfig;
},
],
tooltip: "If your motor is 'sensored' and you don't have another encoder, select this option",
altTooltip: null,
},
{
component: "wizardEncoderIncremental",
data: {
axis: "axis0",
},
title: "Incremental Without Index",
requirements: [],
hooks: [],
tooltip: "Generic incremental encoder without index. Set cpr to 4 * PPR (pulses per revolution).",
altTooltip: null,
},
{
component: "wizardEncoderIncrementalIndex",
data: {
axis: "axis0",
},
title: "Incremental With Index",
requirements: [],
hooks: [],
tooltip: "Generic incremental encoder with index. Set cpr to 4 * PPR (pulses per revolution).",
altTooltip: null,
},
],
pageComponents: [
{
component: "wizardEncoderCal",
id: 0,
data: {
axis: "axis0",
}
},
{
component: "wizardCalStatus",
id: 1,
data: {
success: "Calibration succeeded!",
failure: "Calibration failed",
tooltip: "Double check your encoder connections. If the connections are good, you \
have a wrong setting for motor pole pairs or encoder cpr."
}
}
]
},
Control_0: {
title: "Control mode for Axis 0",
link: "Control Modes 0",
component: "wizardPage",
next: "Misc_0",
back: "Encoder_0",
nextTooltip: "Make a choice!",
choiceMade: false,
choices: [
{
component: "wizardChoice",
data: {
imageURL: require("../images/temp.png"),
configStub: {
axis0: {
controller: {
config: {
control_mode: odriveEnums.CONTROL_MODE_POSITION_CONTROL,
},
},
},
},
},
title: "Position Control",
requirements: [],
hooks: [],
tooltip: "Use this mode to control the position of the motor",
altTooltip: null,
},
{
component: "wizardChoice",
data: {
imageURL: require("../images/temp.png"),
configStub: {
axis0: {
controller: {
config: {
control_mode: odriveEnums.CONTROL_MODE_VELOCITY_CONTROL,
},
},
},
},
},
title: "Velocity Control",
requirements: [],
hooks: [],
tooltip: "Use this mode to control the velocity of the motor",
altTooltip: null,
},
{
component: "wizardChoice",
data: {
imageURL: require("../images/temp.png"),
configStub: {
axis0: {
controller: {
config: {
control_mode: odriveEnums.CONTROL_MODE_TORQUE_CONTROL,
},
},
},
},
},
title: "Torque Control",
requirements: [],
hooks: [],
tooltip: "Use this mode to control the torque output of the motor",
altTooltip: null,
},
{
component: "wizardChoice",
data: {
imageURL: require("../images/temp.png"),
configStub: {
axis0: {
controller: {
config: {
control_mode: odriveEnums.CONTROL_MODE_VOLTAGE_CONTROL,
},
},
},
},
},
title: "Voltage Control",
requirements: [],
hooks: [],
tooltip: "This mode is used for gimbal motors",
altTooltip: null,
},
],
customComponents: [],
pageComponents: [],
},
/*
Input_0: {
title: "Input mode selection for Axis 0",
link: "Input Modes 0",
component: "wizardPage",
next: "Misc_0",
back: "Control_0",
nextTooltip: "Make a choice!",
choiceMade: false,
choices: [
{
component: "wizardChoice",
data: {
imageURL: require("../images/temp.png"),
configStub: {
axis0: {
controller: {
config: {
input_mode: odriveEnums.INPUT_MODE_PASSTHROUGH,
},
},
},
},
},
title: "Input Passthrough",
requirements: [],
hooks: [],
tooltip: "This is the default input mode. Input commands are passed to the controller without modification.",
altTooltip: null,
},
{
component: "wizardInputFiltered",
data: {
axis: "axis0",
},
title: "Position Input Filtering",
requirements: [
(configObj) => {
return configObj.axis0.controller.config.control_mode == odriveEnums.CONTROL_MODE_POSITION_CONTROL;
},
],
hooks: [],
tooltip: "Filtered position mode. This provides smoother motion than the passthrough mode. \
Input position commands are sent through a 2nd order filter. A higher bandwidth will cause quicker position changes.",
altTooltip: "Control mode must be Position Control for this input mode",
},
{
component: "wizardInputVelRamp",
data: {
axis: "axis0",
},
title: "Velocity Input Ramping",
requirements: [
(configObj) => {
return configObj.axis0.controller.config.control_mode == odriveEnums.CONTROL_MODE_VELOCITY_CONTROL;
},
],
hooks: [],
tooltip: "For velocity control mode, you can choose to have the input velocities ramped between values. vel_ramp_rate is the \
acceleration between velocities.",
altTooltip: "Control mode must be Velocity Control for this input mode",
},
{
component: "wizardInputTrajectory",
data: {
axis: "axis0",
},
title: "Trajectory Planning",
requirements: [
(configObj) => {
return configObj.axis0.controller.config.control_mode == odriveEnums.CONTROL_MODE_POSITION_CONTROL;
},
],
hooks: [],
tooltip: "This mode lets you smoothly accelerate, coast, and decelerate from one position to another. vel_limit is your coasting speed, \
accel_limit is the maximum acceleration in turns/s^2, decel_limit is the maximum decelaration in turns/s^2 and inertia is an \
optional value to correlate acceleration with motor torque.",
altTooltip: "Control mode must be Position Control for this input mode",
}
],
pageComponents: [],
},
*/
Misc_0: {
title: "Finishing touches for Axis 0",
link: "Misc 0",
component: "wizardPage",
next: "Motor_1",
back: "Control_0",
nextTooltip: "Make a choice!",
choiceMade: false,
choices: [
{
component: "wizardMisc",
data: {
axis: "axis0",
},
title: "Miscellaneous Parameters",
requirements: [],
hooks: [],
tooltip: null,
altTooltip: null,
},
],
pageComponents: [],
},
Motor_1: {
title: "Which motor are you using for Axis 1?",
link: "Motor 1",
component: "wizardPage",
next: "Encoder_1",
back: "Misc_0",
nextTooltip: "ODrive needs to know the inductance and resistance of your motor in order to control it.\
Click the Calibrate Motor button to start the measurement process. The motor will beep\
during calibration.",
choiceMade: false,
choices: [
{
component: "wizardChoice",
data: {
imageURL: require("../images/D5065_300x300.png"),
configStub: {
axis1: {
motor: {
config: {
pole_pairs: 7,
torque_constant: 8.27 / 270,
// R and L come from "Calibrate Motor" button on page
//phase_resistance: 0.039, // from test rig
//phase_inductance: 1.57e-5,
motor_type: odriveEnums.MOTOR_TYPE_HIGH_CURRENT, // MOTOR_TYPE_HIGH_CURRENT
},
},
},
},
},
title: "ODrive D5065",
requirements: [],
hooks: [],
tooltip: "D5065 motor from the ODrive shop",
altTooltip: null,
},
{
component: "wizardChoice",
data: {
imageURL: require("../images/D6374_300x300.png"),
configStub: {
axis1: {
motor: {
config: {
pole_pairs: 7,
torque_constant: 8.27 / 150,
// R and L come from "Calibrate Motor" button on page
//phase_resistance: 0.041, // measurement from PJ
//phase_inductance: 2.23e-5,
motor_type: odriveEnums.MOTOR_TYPE_HIGH_CURRENT, // MOTOR_TYPE_HIGH_CURRENT,
},
},
},
},
},
title: "ODrive D6374",
requirements: [],
hooks: [],
tooltip: "D6374 motor from the ODrive shop",
altTooltip: null,
},
{
component: "wizardMotor",
data: {
axis: "axis1",
},
title: "Other Motor",
requirements: [],
hooks: [],
tooltip: "Bring your own motor!",
altTooltip: null,
},
],
pageComponents: [
{
component: "wizardMotorCal",
id: 0,
data: {
axis: "axis1",
}
},
{
component: "wizardCalStatus",
id: 1,
data: {
success: "Calibration succeeded!",
failure: "Calibration failed",
tooltip: "Double check your motor connections. If the connections are good, it is \
possible that your motor has a higher resistance or lower inductance than ODrive expects."
}
}
]
},
Encoder_1: {
title: "Which encoder are you using for Axis 1?",
link: "Encoder 1",
component: "wizardPage",
next: "Control_1",
back: "Motor_1",
nextTooltip: "ODrive needs to know the relation between the encoder position and motor position in order to function. \
Click the Calibrate Encoder button to start the measurement process. The motor will slowly rotate back \
and forth during calibration.",
choiceMade: false,
choices: [
{
component: "wizardChoice",
data: {
imageURL: require("../images/amt102-v_300x300.png"),
configStub: {
axis1: {
encoder: {
config: {
cpr: 8192,
use_index: true,
mode: odriveEnums.ENCODER_MODE_INCREMENTAL, // ENCODER_MODE_INCREMENTAL
}
}
}
},
},
title: "CUI AMT102V",
requirements: [],
hooks: [],
tooltip: "Incremental 8192cpr encoder from the ODrive shop",
altTooltip: null,
},
{
component: "wizardChoice",
data: {
imageURL: require("../images/hall_effect_300x300.png"),
configStub: {
axis1: {
encoder: {
config: {
use_index: false,
mode: odriveEnums.ENCODER_MODE_HALL,
}
}
}
},
},
title: "Hall Effect",
requirements: [],
hooks: [
// hook takes a config object (normally this.wizardConfig in Wizard.vue), creates and returns a copy
(configObj) => {
let newConfig = configObj;
newConfig.axis1.encoder.config.cpr = 6 * newConfig.axis1.motor.config.pole_pairs;
return newConfig;
},
],
tooltip: "If your motor is 'sensored' and you don't have another encoder, select this option",
altTooltip: null,
},
{
component: "wizardEncoderIncremental",
data: {
axis: "axis1",
},
title: "Incremental Without Index",
requirements: [],
hooks: [],
tooltip: "Generic incremental encoder without index. Set cpr to 4 * PPR (pulses per revolution).",
altTooltip: null,
},
{
component: "wizardEncoderIncrementalIndex",
data: {
axis: "axis1",
},
title: "Incremental With Index",
requirements: [],
hooks: [],
tooltip: "Generic incremental encoder with index. Set cpr to 4 * PPR (pulses per revolution).",
altTooltip: null,
},
],
pageComponents: [
{
component: "wizardEncoderCal",
id: 0,
data: {
axis: "axis1",
}
},
{
component: "wizardCalStatus",
id: 1,
data: {
success: "Calibration succeeded!",
failure: "Calibration failed",
tooltip: "Double check your encoder connections. If the connections are good, you either \
have a wrong setting for motor pole pairs or encoder cpr."
}
}
]
},
Control_1: {
title: "Control mode for Axis 1",
link: "Control Modes 1",
component: "wizardPage",
next: "Misc_1",
back: "Encoder_1",
nextTooltip: "Make a choice!",
choiceMade: false,
choices: [
{
component: "wizardChoice",
data: {
imageURL: require("../images/temp.png"),
configStub: {
axis1: {
controller: {
config: {
control_mode: odriveEnums.CONTROL_MODE_POSITION_CONTROL,
},
},
},
},
},
title: "Position Control",
requirements: [],
hooks: [],
tooltip: "Use this mode to control the position of the motor",
altTooltip: null,
},
{
component: "wizardChoice",
data: {
imageURL: require("../images/temp.png"),
configStub: {
axis1: {
controller: {
config: {
control_mode: odriveEnums.CONTROL_MODE_VELOCITY_CONTROL,
},
},
},
},
},
title: "Velocity Control",
requirements: [],
hooks: [],
tooltip: "Use this mode to control the velocity of the motor",
altTooltip: null,
},
{
component: "wizardChoice",
data: {
imageURL: require("../images/temp.png"),
configStub: {
axis1: {
controller: {
config: {
control_mode: odriveEnums.CONTROL_MODE_TORQUE_CONTROL,
},
},
},
},
},
title: "Torque Control",
requirements: [],
hooks: [],
tooltip: "Use this mode to control the torque output of the motor",
altTooltip: null,
},
{
component: "wizardChoice",
data: {
imageURL: require("../images/temp.png"),
configStub: {
axis1: {
controller: {
config: {
control_mode: odriveEnums.CONTROL_MODE_VOLTAGE_CONTROL,
},
},
},
},
},
title: "Voltage Control",
requirements: [],
hooks: [],
tooltip: "This mode is used for gimbal motors",
altTooltip: null,
},
],
customComponents: [],
pageComponents: [],
},
/*
Input_1: {
title: "Input mode selection for Axis 1",
link: "Input Modes 1",
component: "wizardPage",
next: "Misc_1",
back: "Control_1",
nextTooltip: "Make a choice!",
choiceMade: false,
choices: [
{
component: "wizardChoice",
data: {
imageURL: require("../images/temp.png"),
configStub: {
axis1: {
controller: {
config: {
input_mode: odriveEnums.INPUT_MODE_PASSTHROUGH,
},
},
},
},
},
title: "Input Passthrough",
requirements: [],
hooks: [],
tooltip: "This is the default input mode. Input commands are passed to the controller without modification.",
altTooltip: null,
},
{
component: "wizardInputFiltered",
data: {
axis: "axis1",
},
title: "Position Input Filtering",
requirements: [
(configObj) => {
return configObj.axis1.controller.config.control_mode == odriveEnums.CONTROL_MODE_POSITION_CONTROL;
},
],
hooks: [],
tooltip: "Filtered position mode. This provides smoother motion than the passthrough mode. \
Input position commands are sent through a 2nd order filter. A higher bandwidth will cause quicker position changes.",
altTooltip: "Control mode must be Position Control for this input mode",
},
{
component: "wizardInputVelRamp",
data: {
axis: "axis1",
},
title: "Velocity Input Ramping",
requirements: [
(configObj) => {
return configObj.axis1.controller.config.control_mode == odriveEnums.CONTROL_MODE_VELOCITY_CONTROL;
},
],
hooks: [],
tooltip: "For velocity control mode, you can choose to have the input velocities ramped between values. vel_ramp_rate is the \
acceleration between velocities.",
altTooltip: "Control mode must be Velocity Control for this input mode",
},
{
component: "wizardInputTrajectory",
data: {
axis: "axis1",
},
title: "Trajectory Planning",
requirements: [
(configObj) => {
return configObj.axis1.controller.config.control_mode == odriveEnums.CONTROL_MODE_POSITION_CONTROL;
},
],
hooks: [],
tooltip: "This mode lets you smoothly accelerate, coast, and decelerate from one position to another. vel_limit is your coasting speed, \
accel_limit is the maximum acceleration in turns/s^2, decel_limit is the maximum decelaration in turns/s^2 and inertia is an \
optional value to correlate acceleration with motor torque.",
altTooltip: "Control most must be Position Control for this input mode",
}
],
pageComponents: [],
},
*/
Misc_1: {
title: "Finishing touches for Axis 1",
link: "Misc 1",
component: "wizardPage",
next: "End",
back: "Control_1",
nextTooltip: "Make a choice!",
choiceMade: false,
choices: [
{
component: "wizardMisc",
data: {
axis: "axis1",
},
title: "Miscellaneous Parameters",
requirements: [],
hooks: [],
tooltip: null,
altTooltip: null,
},
],
pageComponents: [],
},
End: {
title: "Review choices",
link: "Review and Apply",
component: "wizardPage",
next: "End",
back: "Misc_1",
choiceMade: false,
choices: [
{
component: "wizardEnd",
data: {},
title: null,
requirements: [],
hooks: [],
tooltip: "Old config values are on the left, new config values are on the right in green",
altTooltip: null,
}
],
pageComponents: [],
}
}
export let enumVars = {
// for wizardEnd.vue
motor_type: {
0: "MOTOR_TYPE_HIGH_CURRENT",
2: "MOTOR_TYPE_GIMBAL",
3: "MOTOR_TYPE_ACIM",
},
mode: {
0: "ENCODER_MODE_INCREMENTAL",
1: "ENCODER_MODE_HALL",
2: "ENCODER_MODE_SINCOS",
256: "ENCODER_MODE_SPI_ABS_CUI",
257: "ENCODER_MODE_SPI_ABS_AMS",
258: "ENCODER_MODE_SPI_ABS_AEAT",
259: "ENCODER_MODE_SPI_ABS_RLS",
},
input_mode: {
0: "INPUT_MODE_INACTIVE",
1: "INPUT_MODE_PASSTHROUGH",
2: "INPUT_MODE_VEL_RAMP",
3: "INPUT_MODE_POS_FILTER",
4: "INPUT_MODE_MIX_CHANNELS",
5: "INPUT_MODE_TRAP_TRAJ",
6: "INPUT_MODE_TORQUE_RAMP",
7: "INPUT_MODE_MIRROR",
},
control_mode: {
0: "CONTROL_MODE_VOLTAGE_CONTROL",
1: "CONTROL_MODE_TORQUE_CONTROL",
2: "CONTROL_MODE_VELOCITY_CONTROL",
3: "CONTROL_MODE_POSITION_CONTROL",
},
}
+186
View File
@@ -0,0 +1,186 @@
'use strict'
import { app, protocol, BrowserWindow, ipcMain } from 'electron'
import { createProtocol } from 'vue-cli-plugin-electron-builder/lib'
import installExtension, { VUEJS_DEVTOOLS } from 'electron-devtools-installer'
const isDevelopment = process.env.NODE_ENV !== 'production'
const { spawnSync, execSync } = require('child_process');
const spawn = require('child_process').spawn;
const path = require('path');
// Keep a global reference of the window object, if you don't, the window will
// be closed automatically when the JavaScript object is garbage collected.
let win;
// var for python server
let server;
// Scheme must be registered before the app is ready
protocol.registerSchemesAsPrivileged([
{ scheme: 'app', privileges: { secure: true, standard: true } }
])
// function to get determine correct command for python
function getPyCmd() {
// call both 'python' and 'python3' to figure out the correct command
let spawnRet = spawnSync('python', ['-V']);
let success = spawnRet.status != null;
let outputString;
let cmd = '';
if (success) {
if (spawnRet.stdout.toString().length > 1) {
outputString = spawnRet.stdout.toString();
}
else {
outputString = spawnRet.stderr.toString();
}
if (outputString.includes("Python 3")) {
cmd = 'python';
}
}
if (cmd == '') {
spawnRet = spawnSync('python3', ['-V']);
success = spawnRet.status != null;
if (success) {
if (spawnRet.stdout.toString().length > 1) {
outputString = spawnRet.stdout.toString();
}
else {
outputString = spawnRet.stderr.toString();
}
if (outputString.includes("Python 3")) {
cmd = 'python3';
}
}
}
return cmd;
}
function createWindow() {
// Create the browser window.
win = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
// Use pluginOptions.nodeIntegration, leave this alone
// See nklayman.github.io/vue-cli-plugin-electron-builder/guide/security.html#node-integration for more info
nodeIntegration: process.env.ELECTRON_NODE_INTEGRATION,
preload: path.join(__dirname, 'preload.js')
},
icon: path.join(__static, 'icon.png')
})
win.maximize();
win.setMenu(null);
if (process.env.WEBPACK_DEV_SERVER_URL) {
// Load the url of the dev server if in development mode
win.loadURL(process.env.WEBPACK_DEV_SERVER_URL)
if (!process.env.IS_TEST) win.webContents.openDevTools()
} else {
createProtocol('app')
// Load the index.html when not in development
win.loadURL('app://./index.html')
}
win.on('closed', () => {
win = null
})
}
// Quit when all windows are closed.
app.on('window-all-closed', () => {
// kill flask server
if (process.platform !== 'win32') {
execSync('kill $(ps aux | grep \'[o]drive_server.py\' | awk \'{print $2}\')');
console.log("killed flask server");
}
// On macOS it is common for applications and their menu bar
// to stay active until the user quits explicitly with Cmd + Q
if (process.platform !== 'darwin') {
app.quit()
}
})
app.on('activate', () => {
// On macOS it's common to re-create a window in the app when the
// dock icon is clicked and there are no other windows open.
if (win === null) {
createWindow()
}
})
// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
app.on('ready', async () => {
if (isDevelopment && !process.env.IS_TEST) {
// Install Vue Devtools
try {
await installExtension(VUEJS_DEVTOOLS)
} catch (e) {
console.error('Vue Devtools failed to install:', e.toString())
}
}
createWindow()
// Figure out correct path and arguments to launch python server
let scriptFilename = path.join(app.getAppPath(), '../server', 'odrive_server.py');
const args = process.argv;
let effectiveCommand = [];
effectiveCommand.push(scriptFilename);
if (app.isPackaged === true) {
for (const arg of args.slice(1)) {
effectiveCommand.push(arg);
}
}
else {
for (const arg of args.slice(2)) {
effectiveCommand.push(arg);
}
}
// launch python server on event from renderer process (gui) and pipe stdout/stderr to it
ipcMain.on('start-server', () => {
server = spawn(getPyCmd(), effectiveCommand);
server.stdout.on('data', function (data) {
try {
console.log(data.toString('utf8'));
} catch (error) {
console.log(error);
}
try {
win.webContents.send('server-stdout', String(data.toString('utf8')));
} catch (error) {
console.log(error);
}
});
server.stderr.on('data', function (data) {
console.log(data.toString('utf8'));
try {
win.webContents.send('server-stderr', String(data.toString('utf8')));
} catch (error) {
console.log(error);
}
});
})
ipcMain.on('kill-server', () => {
server.kill('SIGINT');
})
})
// Exit cleanly on request from parent process in development mode.
if (isDevelopment) {
if (process.platform === 'win32') {
process.on('message', (data) => {
if (data === 'graceful-exit') {
server.kill('SIGINT');
app.quit()
}
})
} else {
process.on('SIGTERM', () => {
server.kill('SIGINT');
app.quit()
})
}
}
@@ -0,0 +1,33 @@
import io from 'socket.io-client';
let socket = undefined;
let url = 'https://0.0.0.0:8080';
function initSocket(url) {
socket = io(url);
}
export function setUrl(path) {
if (socket) {
socket.close();
socket = undefined;
}
url = path;
initSocket(url);
}
export function closeSocket() {
socket.close();
socket = undefined;
}
export function addEventListener(event) {
if (!socket) {
initSocket(url);
}
socket.on(event.type, event.callback);
}
export function sendEvent(event) {
socket.emit(event.type, event.data);
}
@@ -0,0 +1,202 @@
<template>
<div
class="axis"
@click.self="showError = !showError"
:class="{ inactive: !connected, noError: !error, error: error }"
>
{{ axis }}
<div
v-show="showError"
class="error-popup card"
@click.self="showError = !showError"
>
<clear-errors :data="{ axis: axis }" />
axis:
<span :class="{ noError: !axisError, error: axisError }">{{
axisErrorMsg
}}</span>
<br />motor:
<span :class="{ noError: !motorError, error: motorError }">{{
motorErrorMsg
}}</span>
<br />encoder:
<span :class="{ noError: !encoderError, error: encoderError }">{{
encoderErrorMsg
}}</span>
<br />controller:
<span :class="{ noError: !controllerError, error: controllerError }">{{
controllerErrorMsg
}}</span>
</div>
</div>
</template>
<script>
import odriveEnums from "../assets/odriveEnums.json";
import clearErrors from "./clearErrors.vue";
import { getVal, fetchParam } from "../lib/odrive_utils";
export default {
name: "Axis",
components: {
clearErrors,
},
props: ["axis", "odrives"],
data() {
return {
showError: false,
axisErr: undefined,
motorErr: undefined,
controllerErr: undefined,
encoderErr: undefined,
};
},
methods: {
getErrorString(errCode, errType) {
let retMsg = "none";
if (errCode != 0) {
// we got an error!
// mask errors depending on which page is active
let errs = [];
for (const [name, value] of Object.entries(odriveEnums)) {
if (name.includes(errType) && errCode & value) {
errs.push(name);
}
}
retMsg = errs.join(", ");
}
return retMsg;
},
},
computed: {
connected() {
return this.$store.state.ODrivesConnected[this.axis.split(".")[0]];
},
axisErrorMsg() {
let errCode = this.axisErr;
if (this.$store.state.currentDash == "Wizard") {
errCode &= ~odriveEnums.AXIS_ERROR_MOTOR_FAILED;
errCode &= ~odriveEnums.AXIS_ERROR_ENCODER_FAILED;
}
return this.getErrorString(errCode, 'AXIS_ERROR');
},
motorErrorMsg() {
let errCode = this.motorErr;
if (this.$store.state.currentDash == "Wizard") {
errCode &= ~odriveEnums.MOTOR_ERROR_PHASE_RESISTANCE_OUT_OF_RANGE;
errCode &= ~odriveEnums.MOTOR_ERROR_PHASE_INDUCTANCE_OUT_OF_RANGE;
}
return this.getErrorString(errCode, 'MOTOR_ERROR');
},
encoderErrorMsg() {
let errCode = this.encoderErr;
if (this.$store.state.currentDash == "Wizard") {
errCode &= ~odriveEnums.ENCODER_ERROR_CPR_POLEPAIRS_MISMATCH;
errCode &= ~odriveEnums.ENCODER_ERROR_NO_RESPONSE;
}
return this.getErrorString(errCode, 'ENCODER_ERROR');
},
controllerErrorMsg() {
let errCode = this.controllerErr;
return this.getErrorString(errCode, 'CONTROLLER_ERROR');
},
axisError() {
let errCode = this.axisErr;
if (this.$store.state.currentDash == "Wizard") {
errCode &= ~odriveEnums.AXIS_ERROR_MOTOR_FAILED;
errCode &= ~odriveEnums.AXIS_ERROR_ENCODER_FAILED;
}
return errCode != 0;
},
motorError() {
let errCode = this.motorErr;
if (this.$store.state.currentDash == "Wizard") {
errCode &= ~odriveEnums.MOTOR_ERROR_PHASE_RESISTANCE_OUT_OF_RANGE;
errCode &= ~odriveEnums.MOTOR_ERROR_PHASE_INDUCTANCE_OUT_OF_RANGE;
}
return errCode != 0;
},
encoderError() {
let errCode = this.encoderErr;
if (this.$store.state.currentDash == "Wizard") {
errCode &= ~odriveEnums.ENCODER_ERROR_CPR_POLEPAIRS_MISMATCH;
errCode &= ~odriveEnums.ENCODER_ERROR_NO_RESPONSE;
}
return errCode != 0;
},
controllerError() {
let errCode = this.controllerErr;
return errCode != 0;
},
error() {
return (
this.axisError ||
this.motorError ||
this.encoderError ||
this.controllerError
);
},
},
created() {
// set up timeout loop for grabbing axis error values
let update = () => {
// Do we have an active connection to the ODrive that contains this axis?
if (this.$store.state.ODrivesConnected[this.axis.split(".")[0]]) {
fetchParam(this.axis + ".error");
fetchParam(this.axis + ".motor.error");
fetchParam(this.axis + ".controller.error");
fetchParam(this.axis + ".encoder.error");
this.axisErr = getVal(this.axis + ".error");
this.motorErr = getVal(this.axis + ".motor.error");
this.controllerErr = getVal(this.axis + ".controller.error");
this.encoderErr = getVal(this.axis + ".encoder.error");
}
};
this.cyclicUpdate = setInterval(update, 1000);
},
beforeDestroy() {
clearInterval(this.cyclicUpdate);
}
};
</script>
<style scoped>
.axis {
padding: 5px 10px;
border: 2px black;
cursor: pointer;
}
.axis:active {
background-color: var(--bg-color);
}
.noError {
color: green;
}
.error {
color: red;
font-weight: bold;
}
.error-popup {
position: absolute;
bottom: 2rem;
color: black;
margin-left: 0px;
}
.inactive {
color: grey;
}
</style>
@@ -0,0 +1,101 @@
<template>
<div class="card action-card">
<button class="close-button" @click=deleteAction>X</button>
<span class="ctrlName">{{shortPath}}:</span>
<div class="right">
<input v-on:change="newVal" :placeholder="initVal" :value="valueDisplay" spellcheck="false"/>
<button class="action-button close-button" @click="putVal">Send</button>
</div>
</div>
</template>
<script>
import { putVal, parseMath } from "../../lib/odrive_utils.js";
import { numberDisplay } from "../../lib/utils.js"
export default {
name: "Action",
props: {
id: String,
path: String,
initVal: Number,
dashID: String
},
data: function () {
return {
value: 0,
};
},
mounted() {
if (this.initVal !== undefined) {
this.value = this.initVal;
}
},
computed: {
shortPath() {
let keys = this.path.split('.');
keys.shift();
return keys.join('.');
},
valueDisplay() {
return numberDisplay(this.value);
}
},
methods: {
newVal: function (e) {
let val = parseMath(e.target.value);
if (val !== false) {
this.value = val;
console.log("input = " + e.target.value + ", val = " + this.value);
this.$store.commit("setActionVal", {dashID: this.dashID, actionID: this.id, val: this.value});
}
},
putVal: function () {
let keys = this.path.split(".");
keys.shift();
console.log('path = ' + keys.join('.') + ", val = " + this.value);
putVal(keys.join('.'), this.value);
},
deleteAction: function() {
// commit a mutation to remove this action from the dashboard
this.$store.commit("removeActionFromDash", {dashID: this.dashID, actionID: this.id});
}
},
};
</script>
<style scoped>
input {
border-style: none;
border-bottom: 1px solid grey;
width: 5rem;
margin-left: 0.5rem;
margin-right: 0.5rem;
text-align: center;
}
.action-card {
display: flex;
/* border: 1px solid lightcoral; */
box-sizing: border-box;
}
.action-button {
margin-right: 0;
}
.right {
margin-left: auto;
}
input::-webkit-outer-spin-button,
input::-webkit-inner-spin-button {
/* display: none; <- Crashes Chrome on hover */
-webkit-appearance: none;
margin: 0; /* <-- Apparently some margin are still there even though it's hidden */
}
input[type=number] {
-moz-appearance:textfield; /* Firefox */
}
</style>
@@ -0,0 +1,95 @@
<template>
<div class="card action-card">
<button class="close-button" @click=deleteAction>X</button>
<span class="ctrlName">{{shortPath}}:</span>
<div class="right">
<select class="action-enum" v-model="value" @change="newVal">
<option v-for="option in options" :key="option.value" :value="option.value">
{{ option.text }}
</option>
</select>
<button class="action-button close-button" @click="putVal">Send</button>
</div>
</div>
</template>
<script>
import { putVal } from "../../lib/odrive_utils.js";
export default {
name: "ActionEnum",
props: {
id: String,
path: String,
initVal: Number,
dashID: String,
options: Array,
},
data: function () {
return {
value: 0,
};
},
created() {
if (this.initVal !== undefined) {
this.value = this.initVal;
}
},
computed: {
shortPath() {
let keys = this.path.split('.');
keys.shift();
return keys.join('.');
}
},
methods: {
newVal: function (e) {
console.log(this.selected);
this.value = parseFloat(e.target.value);
this.$store.commit("setActionVal", {dashID: this.dashID, actionID: this.id, val: this.value});
},
putVal: function () {
let keys = this.path.split(".");
keys.shift();
console.log('path = ' + keys.join('.') + ", val = " + this.value);
putVal(keys.join('.'), this.value);
},
deleteAction: function() {
// commit a mutation to remove this action from the dashboard
this.$store.commit("removeActionFromDash", {dashID: this.dashID, actionID: this.id});
}
},
};
</script>
<style scoped>
input {
border-style: none;
border-bottom: 1px solid grey;
width: 5rem;
margin-left: 0.5rem;
margin-right: 0.5rem;
text-align: center;
}
.action-card {
display: flex;
/* border: 1px solid lightcoral; */
box-sizing: border-box;
}
.action-button {
margin-right: 0;
}
.right {
margin-left: auto;
}
.action-enum {
border: none;
border-bottom: 1px solid grey;
background-color: var(--fg-color);
text-align-last: right;
}
</style>
@@ -0,0 +1,38 @@
<template>
<div>
<button class="measure-button card" @click="clear">Clear Errors</button>
</div>
</template>
<script>
import { putVal } from "../lib/odrive_utils.js";
export default {
name: "clearErrors.vue",
props: {
data: Object,
},
methods: {
clear() {
// clear odrive errors
// set them to 0?
console.log(this.data);
let paths = [
this.data.axis + ".error",
this.data.axis + ".motor.error",
this.data.axis + ".encoder.error",
this.data.axis + ".controller.error",
];
for (const path of paths) {
putVal(path, 0);
}
},
},
};
</script>
<style scoped>
.measure-button:active {
background-color: var(--bg-color);
}
</style>
@@ -0,0 +1,90 @@
<template>
<div class="card">
<button class="close-button" @click=deleteCtrl>X</button>
<span class="ctrlName">{{name}}:</span>
<div class="right">
<span class="ctrlVal">{{value}}</span>
<input
class="ctrlInput"
v-if="writeAccess"
type="checkbox"
:value="value"
:checked="value"
@click="putVal"
/>
</div>
</div>
</template>
<script>
import { getVal, getReadonly, putVal, fetchParam } from "../../lib/odrive_utils.js";
export default {
name: "CtrlBoolean",
//type checking here for properties
props: {
path: String,
odrives: Object,
dashID: String,
},
data() {
return {
}
},
computed: {
value: function () {
let keys = this.path.split(".");
keys.shift(); // don't need first key here
return getVal(keys.join('.'));
},
name: function () {
let keys = this.path.split(".");
keys.shift();
return keys.join(".");
},
writeAccess: function () {
let keys = this.path.split(".");
keys.shift(); // don't need first key here
return getReadonly(keys.join('.')) == false;
},
},
methods: {
putVal: function (e) {
let keys = this.path.split('.');
keys.shift();
putVal(keys.join('.'), e.target.checked);
fetchParam(keys.join('.'));
},
deleteCtrl: function() {
// commit a mutation in the store with the relevant information
this.$store.commit("removeCtrlFromDash", {dashID: this.dashID, path: this.path});
}
},
created() {
// update parameter value on component creation
let keys = this.path.split('.');
keys.shift();
fetchParam(keys.join('.'));
}
};
</script>
<style scoped>
.ctrlVal {
font-weight: bold;
}
.ctrlInput {
margin-left: 10px;
}
.right {
display: flex;
flex-direction: row;
margin-left: auto;
}
.card {
display: flex;
}
</style>
@@ -0,0 +1,107 @@
<template>
<div class="card">
<button class="close-button" @click=deleteCtrl>X</button>
<span class="ctrlName">{{name}}:</span>
<div class="right">
<select class="ctrl-enum" v-model="selected" @change="putVal">
<option v-for="option in options" :key="option.value" :value="option.value">
{{ option.text }}
</option>
</select>
</div>
</div>
</template>
<script>
import { getVal, getReadonly, putVal, fetchParam } from "../../lib/odrive_utils.js";
export default {
name: "CtrlNumeric",
//type checking here for properties
props: {
path: String,
odrives: Object,
dashID: String,
options: Array,
},
data() {
return {
selected: undefined,
}
},
computed: {
value: function () {
let keys = this.path.split('.');
keys.shift();
return parseFloat(getVal(keys.join('.')));
},
name: function () {
let keys = this.path.split(".");
keys.shift();
return keys.join(".");
},
writeAccess: function () {
let keys = this.path.split(".");
keys.shift(); // don't need first key here
return getReadonly(keys.join('.')) === false;
},
},
watch: {
value: function(newVal) {
this.selected = newVal;
}
},
methods: {
putVal: function (e) {
let keys = this.path.split('.');
keys.shift();
console.log("Calling putVal from CtrlEnum");
putVal(keys.join('.'), parseFloat(e.target.value));
},
deleteCtrl: function() {
// commit a mutation in the store with the relevant information
this.$store.commit("removeCtrlFromDash", {dashID: this.dashID, path: this.path});
},
},
created() {
// update parameter value on component creation
let keys = this.path.split('.');
keys.shift();
fetchParam(keys.join('.'));
this.selected = this.value;
console.log(this.value);
}
};
</script>
<style scoped>
.ctrlVal {
font-weight: bold;
}
input {
width: 5rem;
font-family: inherit;
border-style: none;
border-bottom: 1px solid grey;
text-align: center;
}
.right {
display: flex;
flex-direction: row;
margin-left: auto;
}
.card {
display: flex;
}
.ctrl-enum {
border: none;
border-bottom: 1px solid grey;
background-color: var(--fg-color);
text-align-last: right;
}
</style>
@@ -0,0 +1,57 @@
<template>
<div class="card" @click.self="executeFunction">
<button class="close-button" @click=deleteCtrl>X</button>
<button class="execute" @click="executeFunction">{{name}}()</button>
</div>
</template>
<script>
import { callFcn } from "../../lib/odrive_utils.js";
export default {
name: "CtrlFunction",
//type checking here for properties
props: {
path: String,
odrives: Object,
dashID: String,
},
computed: {
name: function() {
let keys = this.path.split(".");
keys.shift();
return keys.join(".");
}
},
methods: {
executeFunction: function() {
//execute this function on the odrive
let keys = this.path.split(".");
keys.shift();
callFcn(keys.join('.'));
},
deleteCtrl: function() {
// commit a mutation in the store with the relevant information
this.$store.commit("removeCtrlFromDash", {dashID: this.dashID, path: this.path});
}
}
};
</script>
<style scoped>
.card {
background-color: lightcyan;
}
.execute {
color: black;
font-family: "Roboto Mono", monospace;
margin: 0;
padding: 0;
background-color: rgba(0, 0, 0, 0);
}
.card:active {
background-color: lightblue;
}
</style>
@@ -0,0 +1,109 @@
<template>
<div class="card">
<button class="close-button" @click=deleteCtrl>X</button>
<span class="ctrlName">{{name}}:</span>
<div class="right">
<span v-if="!writeAccess" class="ctrlVal">{{value}}</span>
<input v-if="writeAccess" :placeholder="value" v-on:change="putVal" :value="value" spellcheck="false"/>
<!-- <span class="unit">[{{unit}}]</span> -->
</div>
</div>
</template>
<script>
import { getVal, getReadonly, putVal, fetchParam, getUnit, parseMath } from "../../lib/odrive_utils.js";
import { numberDisplay } from "../../lib/utils.js";
export default {
name: "CtrlNumeric",
//type checking here for properties
props: {
path: String,
odrives: Object,
dashID: String
},
computed: {
value: function () {
let keys = this.path.split('.');
keys.shift();
let val = getVal(keys.join('.'));
return numberDisplay(val);
},
name: function () {
let keys = this.path.split(".");
keys.shift();
return keys.join(".");
},
writeAccess: function () {
let keys = this.path.split(".");
keys.shift(); // don't need first key here
return getReadonly(keys.join('.')) === false;
},
unit() {
let keys = this.path.split(".");
keys.shift();
return getUnit(this.$store.state.odrives.odrive0,keys.join('.'));
},
},
methods: {
putVal: function (e) {
let keys = this.path.split('.');
keys.shift();
console.log("input recieved: " + e.target.value);
let val = parseMath(e.target.value);
if (val !== false) {
putVal(keys.join('.'), val);
}
},
deleteCtrl: function() {
// commit a mutation in the store with the relevant information
this.$store.commit("removeCtrlFromDash", {dashID: this.dashID, path: this.path});
}
},
created() {
// update parameter value on component creation
let keys = this.path.split('.');
keys.shift();
fetchParam(keys.join('.'));
},
};
</script>
<style scoped>
.ctrlVal {
font-weight: bold;
}
input {
width: 5rem;
font-family: inherit;
border-style: none;
border-bottom: 1px solid grey;
text-align: center;
}
.right {
display: flex;
flex-direction: row;
margin-left: auto;
}
.card {
display: flex;
}
input::-webkit-outer-spin-button,
input::-webkit-inner-spin-button {
/* display: none; <- Crashes Chrome on hover */
-webkit-appearance: none;
margin: 0; /* <-- Apparently some margin are still there even though it's hidden */
}
input[type=number] {
-moz-appearance:textfield; /* Firefox */
}
.unit {
font-family: "Roboto Mono", monospace;
}
</style>
@@ -0,0 +1,151 @@
<template>
<div class="card">
<div>
<button class="close-button" @click=deleteCtrl>X</button>
<span class="ctrlName">{{name}}</span>
</div>
<div class="slider-container">
<input :value="displayMin" :placeholder="displayMin" v-on:change="setMin"/>
<!-- <vue-slider v-model="value" :min="min" :max="max" :interval="interval" /> -->
<vue-slider v-model="value" :data="data" :adsorb="true" @change="putVal"/>
<input :value="displayMax" :placeholder="displayMax" v-on:change="setMax" />
</div>
</div>
</template>
<script>
import VueSlider from "vue-slider-component";
import "vue-slider-component/theme/default.css";
import { getVal, putVal, parseMath } from "../../lib/odrive_utils.js";
import { numberDisplay } from "../../lib/utils.js";
export default {
name: "CtrlSlider",
components: {
VueSlider,
},
//type checking here for properties
props: {
path: String,
odrives: Object,
dashID: String,
},
data: function () {
return {
value: 0,
min: 0,
max: 1,
data: []
};
},
computed: {
name: function () {
let keys = this.path.split(".");
keys.shift();
return keys.join(".");
},
interval: function () {
return (this.max - this.min) / 100;
},
sliderData: function () {
let interval = (this.max - this.min) / 100;
return Array.from(Array(101), (_, i) => this.min + interval * i);
},
displayMin() {
return numberDisplay(this.min);
},
displayMax() {
return numberDisplay(this.max);
}
},
methods: {
putVal: function (value) {
console.log(value);
let keys = this.path.split(".");
keys.shift();
putVal(keys.join('.'), value);
},
setMin: function (e) {
let min = parseMath(e.target.value);
if (min < this.max) {
this.min = min;
this.data = Array.from(Array(101), (_, i) => this.min + (this.max-this.min) / 100 * i);
this.value = this.findNearest(this.data, this.value);
}
},
setMax: function (e) {
let max = parseMath(e.target.value);
if (max > this.min) {
this.max = max;
this.data = Array.from(Array(101), (_, i) => this.min + (this.max-this.min) / 100 * i);
this.value = this.findNearest(this.data, this.value);
}
},
deleteCtrl: function() {
// commit a mutation in the store with the relevant information
this.$store.commit("removeCtrlFromDash", {dashID: this.dashID, path: this.path});
},
findNearest(data, value) {
// find item in data closest to value
let diff = Number.POSITIVE_INFINITY;
let retVal = value;
for (const val of data) {
if (Math.abs(val - value) < diff) {
diff = Math.abs(val - value);
retVal = val;
}
}
return retVal;
}
},
mounted() {
let initVal = () => {
let keys = this.path.split(".");
keys.shift(); // don't need first key here
return parseFloat(getVal(keys.join('.')));
};
this.value = initVal();
this.max = this.value * 4;
this.min = this.value / 4;
this.data = Array.from(Array(101), (_, i) => this.min + (this.max-this.min) / 100 * i);
},
};
</script>
<style scoped>
.ctrlVal {
font-weight: bold;
}
.slider-container {
display: flex;
flex-direction: row;
margin-top: 0.4rem;
}
.vue-slider {
flex-grow: 3;
margin-left: 0.4rem;
margin-right: 0.4rem;
z-index: 0;
}
input {
width: 5rem;
font-family: inherit;
border-style: none;
border-bottom: 1px solid grey;
text-align: center;
}
input::-webkit-outer-spin-button,
input::-webkit-inner-spin-button {
/* display: none; <- Crashes Chrome on hover */
-webkit-appearance: none;
margin: 0; /* <-- Apparently some margin are still there even though it's hidden */
}
input[type=number] {
-moz-appearance:textfield; /* Firefox */
}
</style>
@@ -0,0 +1,14 @@
import { Line, mixins } from 'vue-chartjs';
import 'chartjs-plugin-streaming';
const { reactiveProp } = mixins;
export default {
extends: Line,
mixins: [reactiveProp],
props: ['options'],
mounted () {
// this.chartData is created in the mixin.
// If you want to pass options please create a local options object
this.renderChart(this.chartData, this.options)
}
}
@@ -0,0 +1,167 @@
<template>
<div class="card plot">
<div class="plot-header">
<button class="close-button" @click="deletePlot">X</button>
<button class="close-button" @click="exportCSV">Export</button>
<button class="close-button" @click="$emit('add-var', name)">+</button>
</div>
<line-chart v-if="loaded" :chart-data="datacollection" :options="dataOptions"></line-chart>
</div>
</template>
<script>
import LineChart from "./LineChart.js";
import { saveAs } from "file-saver";
export default {
components: {
LineChart,
},
props: ["name", "plot", "dashID"],
data() {
return {
datacollection: null,
timeStart: null,
loaded: false,
dataOptions: {
animation: {
duration: 0, // general animation time
},
events: [],
hover: {
animationDuration: 0, // duration of animations when hovering an item
},
responsiveAnimationDuration: 0, // animation duration after a resize
elements: {
point: {
radius: 0,
},
line: {
// borderColor: "rbga(0,0,0,0)",
fill: false,
borderWidth: 0,
tension: 0,
},
},
responsive: true,
maintainAspectRatio: false,
},
};
},
mounted() {
this.timeStart = Date.now();
//this.initData(); //set label for dataset and color
this.cyclicUpdate = setInterval(() => this.fillData(), 50);
},
beforeDestroy() {
clearInterval(this.cyclicUpdate);
},
methods: {
fillData() {
let newData = {
labels: this.$store.state.propSamples["time"],
datasets: [],
};
for (const plotVar of this.plot.vars) {
let newPath = plotVar.path.split(".");
newPath.splice(0, 1);
newPath = newPath.join(".");
newData.datasets.push({
label: newPath,
borderColor: plotVar.color, //"rgba(0,0,0,0)",
data: this.$store.state.propSamples[newPath],
});
}
this.datacollection = newData;
this.loaded = true;
},
initData() {
this.datacollection = {
labels: [0],
datasets: [
{
label: "Sine wave",
backgroundColor: "rgba(0,0,0,0)", //"#f87979",
data: [0],
},
],
};
},
deletePlot: function () {
// commit a mutation in the store with the relevant information
this.$store.commit("removePlotFromDash", {
dashID: this.dashID,
plotID: this.name,
});
},
exportCSV: function () {
// make a sensible data structure
let csvData = {};
csvData["time"] = this.$store.state.propSamples["time"];
for (const dataset of this.datacollection.datasets) {
csvData[dataset.label] = dataset.data;
}
let csvString = "";
let dataKeys = Object.keys(csvData);
// turn data structure into a string that's suitable for exporting
for (const label of dataKeys) {
csvString += label;
if (label != dataKeys.slice(-1)) {
csvString += ",";
} else {
csvString += "\n";
}
}
for (let idx = 0; idx < csvData["time"].length; idx = idx + 1) {
for (const label of dataKeys) {
csvString += csvData[label][idx];
if (label != dataKeys.slice(-1)) {
csvString += ",";
} else {
csvString += "\n";
}
}
}
var blob = new Blob([csvString], { type: "text/plain;charset=utf-8" });
saveAs(blob, "plot.csv");
console.log("exporting plot");
},
},
};
</script>
<style scoped>
div {
position: relative;
}
.plot {
z-index: 0;
}
.plot-header {
display: flex;
}
.plotname {
flex-grow: 10;
margin: auto 0;
}
.delete {
font-weight: bold;
cursor: pointer;
padding: 0 5px;
margin-right: 10px;
border: 1px solid black;
}
.add-var {
font-weight: bold;
cursor: pointer;
padding: 0 5px;
margin-right: 10px;
border: 1px solid black;
}
</style>
@@ -0,0 +1,91 @@
<template>
<div class="card wizard-choice" :class="{ 'choice-inactive': !allowed }">
<div class="left">
<span>Brake resistor value = </span>
<input v-on:change="setBR" placeholder="Change Me!" :value="brake_resistance"/>
<span class="unit"> [{{ unit }}] </span>
</div>
</div>
</template>
<script>
import { getUnit, parseMath, getVal } from "../../../lib/odrive_utils.js";
export default {
name: "wizardBrake",
props: {
data: Object,
allowed: Boolean,
},
data: function () {
return {
brake_resistance: undefined,
};
},
computed: {
unit() {
// goal is to return "Ohms" from odriveUnits
let path = "odrive0.config.brake_resistance";
return getUnit(this.$store.state.odrives.odrive0, path);
},
value() {
let path = "odrive0.config.brake_resistance";
return getVal(path);
}
},
methods: {
setBR(e) {
console.log("from setBR " + e.target.value);
let val = parseMath(e.target.value);
if (val !== false) {
this.brake_resistance = val;
let configStub = undefined;
configStub = {
config: {
brake_resistance: this.brake_resistance,
},
};
this.$emit("choice", {
choice: "Brake Resistor",
configStub: configStub,
hooks: [],
});
}
},
},
};
</script>
<style scoped>
.wizard-choice {
display: flex;
flex-direction: column;
margin: 2rem;
}
.left {
margin-left: auto;
}
input {
width: 5rem;
font-family: inherit;
border-style: none;
border-bottom: 1px solid grey;
text-align: center;
}
input::-webkit-outer-spin-button,
input::-webkit-inner-spin-button {
/* display: none; <- Crashes Chrome on hover */
-webkit-appearance: none;
margin: 0; /* <-- Apparently some margin are still there even though it's hidden */
}
input[type="number"] {
-moz-appearance: textfield; /* Firefox */
}
.unit {
font-family: "Roboto Mono", monospace;
}
</style>
@@ -0,0 +1,56 @@
<template>
<!-- single choice for a wizard page -->
<div class="wizard-choice card" :class="{'choice-inactive': !allowed}">
<img v-if="data.imageURL != null" :alt="title" :src="data.imageURL" />
{{ title }}
</div>
</template>
<script>
export default {
name: "wizardChoice",
props: {
data: Object,
selected: Boolean,
title: String,
hooks: Array,
allowed: Boolean,
},
data() {
return {
oldSelected: undefined,
}
},
watch: {
selected(newVal) {
if (newVal == true) {
// emit choice event
this.$emit('choice', {choice: this.title, configStub: this.data.configStub, hooks: this.hooks});
}
if (this.oldSelected == true && newVal == false){
console.log("emitting undo-choice");
this.$emit('undo-choice', {choice: this.title, configStub: this.data.configStub, hooks: this.hooks})
}
this.oldSelected = newVal;
}
}
};
</script>
<style scoped>
.wizard-choice {
text-align: center;
display: flex;
flex-direction: column;
margin: 2rem;
}
.inactive {
background-color: var(--bg-color);
}
img {
padding: 0px;
margin: auto;
}
</style>
@@ -0,0 +1,100 @@
<template>
<div class="card wizard-choice" :class="{ 'choice-inactive': !allowed }">
<div class="left">
<span>counts per revolution = </span>
<input v-on:change="setCPR" placeholder="Change Me!" :value="cpr" />
</div>
<span class="name">Incremental Encoder without Index</span>
</div>
</template>
<script>
import odriveEnums from "../../../assets/odriveEnums.json";
import { parseMath } from "../../../lib/odrive_utils.js";
export default {
name: "wizardEncoderIncremental",
props: {
data: Object,
allowed: Boolean,
},
data: function () {
return {
cpr_set: false,
cpr: undefined,
};
},
methods: {
setCPR(e) {
let val = parseMath(e.target.value);
if (val !== false) {
this.cpr = val;
let configStub = undefined;
if (this.data.axis == "axis0") {
configStub = {
axis0: {
encoder: {
config: {
cpr: this.cpr,
use_index: false,
mode: odriveEnums.ENCODER_MODE_INCREMENTAL,
},
},
},
};
} else if (this.data.axis == "axis1") {
configStub = {
axis1: {
encoder: {
config: {
cpr: this.cpr,
use_index: false,
mode: odriveEnums.ENCODER_MODE_INCREMENTAL,
},
},
},
};
}
this.$emit("choice", {
choice: "Incremental",
configStub: configStub,
hooks: [],
});
}
},
},
};
</script>
<style scoped>
.wizard-choice {
display: flex;
flex-direction: column;
margin: 2rem;
}
.left {
margin-left: auto;
}
.name {
margin-top: auto;
}
input {
width: 5rem;
font-family: inherit;
border-style: none;
border-bottom: 1px solid grey;
text-align: center;
}
input::-webkit-outer-spin-button,
input::-webkit-inner-spin-button {
/* display: none; <- Crashes Chrome on hover */
-webkit-appearance: none;
margin: 0; /* <-- Apparently some margin are still there even though it's hidden */
}
input[type="number"] {
-moz-appearance: textfield; /* Firefox */
}
</style>
@@ -0,0 +1,101 @@
<template>
<div class="card wizard-choice" :class="{ 'choice-inactive': !allowed }">
<div class="left">
<span>counts per revolution = </span>
<input v-on:change="setCPR" placeholder="Change Me!" :value="cpr" />
</div>
<span class="name">Incremental Encoder with Index</span>
</div>
</template>
<script>
import odriveEnums from "../../../assets/odriveEnums.json";
import { parseMath } from "../../../lib/odrive_utils.js";
export default {
name: "wizardEncoderIncremental",
props: {
data: Object,
allowed: Boolean,
},
data: function () {
return {
cpr_set: false,
cpr: undefined,
};
},
methods: {
setCPR(e) {
let val = parseMath(e.target.value);
if (val !== false) {
this.cpr = val;
let configStub = undefined;
if (this.data.axis == "axis0") {
configStub = {
axis0: {
encoder: {
config: {
cpr: this.cpr,
use_index: true,
mode: odriveEnums.ENCODER_MODE_INCREMENTAL,
},
},
},
};
} else if (this.data.axis == "axis1") {
configStub = {
axis1: {
encoder: {
config: {
cpr: this.cpr,
use_index: true,
mode: odriveEnums.ENCODER_MODE_INCREMENTAL,
},
},
},
};
}
this.$emit("choice", {
choice: "IncrementalIndex",
configStub: configStub,
hooks: [],
});
}
},
},
};
</script>
<style scoped>
.wizard-choice {
display: flex;
flex-direction: column;
margin: 2rem;
}
.left {
margin-left: auto;
}
.name {
margin-top: auto;
}
input {
width: 5rem;
font-family: inherit;
border-style: none;
border-bottom: 1px solid grey;
text-align: center;
}
input::-webkit-outer-spin-button,
input::-webkit-inner-spin-button {
/* display: none; <- Crashes Chrome on hover */
-webkit-appearance: none;
margin: 0; /* <-- Apparently some margin are still there even though it's hidden */
}
input[type="number"] {
-moz-appearance: textfield; /* Firefox */
}
</style>
@@ -0,0 +1,127 @@
<template>
<div class="card" :class="{'choice-inactive': !allowed}">
<div class="diff" v-for="diff in configDiffs" :key="diff.path">
<span class="diff-path">{{diff.path}}: </span>
<span class="diff-old">{{diffRepresentation(diff.path,diff.oldVal)}}</span>
<span class="diff-seperator"> ⮕ </span>
<span :class="{'diff-new': diff.oldVal != diff.newVal, 'diff-same': diff.oldVal == diff.newVal}">{{diffRepresentation(diff.path,diff.newVal)}}</span>
</div>
<button class="wizard-button card" @click="applyConfig">Apply</button>
</div>
</template>
<script>
import { enumVars } from "../../../assets/wizard/wizard.js";
import { putVal, fetchParam } from "../../../lib/odrive_utils.js"
export default {
name: "wizardEnd",
props: {
config: Object,
axis: String,
allowed: Boolean,
// actual odrive config is in this.$store (Vuex store)
},
data: function() {
return {
configDiffs: [], // array of objects {path, oldval, newval}
flatpaths: [],
path: [],
}
},
created() {
// flatten config tree into array of full variable paths
this.pathFromTree(this.config);
//for (const path of this.flatpaths){
// console.log(path);
//}
for (const path of this.flatpaths) {
let odrvObj = this.$store.state.odrives.odrive0;
let configObj = this.config;
for (const key of path.split('.')){
odrvObj = odrvObj[key];
configObj = configObj[key];
}
if (configObj != null) {
console.log("oldVal is " + odrvObj["val"] + " path is " + path);
if (Number.isInteger(parseFloat(odrvObj["val"]))){
this.configDiffs.push({path: path, oldVal: parseFloat(odrvObj["val"]), newVal: configObj});
}
else if (typeof configObj == 'boolean'){
this.configDiffs.push({path: path, oldVal: odrvObj["val"] == "True" || odrvObj["val"] == true, newVal: configObj});
}
else {
this.configDiffs.push({path: path, oldVal: parseFloat(odrvObj["val"]), newVal: configObj});
}
}
}
},
methods: {
pathFromTree(tree){
// array path
for (const key of Object.keys(tree)) {
this.path.push(key);
if (tree[key] != null && typeof tree[key] == "object") {
this.pathFromTree(tree[key]);
}
else {
this.flatpaths.push(this.path.join('.'));
}
this.path.pop();
}
},
applyConfig(){
// flatpaths will already be populated at this point
for (const diff of this.configDiffs) {
putVal("odrive0." + diff.path, diff.newVal);
fetchParam("odrive0." + diff.path);
console.log("applying " + diff.newVal + " to " + diff.path);
}
},
diffRepresentation(path, val) {
// for the diff view, check if the path indicates that a value is an Enum and return enum string
// otherwise, just the numeric value
let keys = path.split('.');
let retval;
if (Object.keys(enumVars).includes(keys[keys.length - 1])) {
retval = enumVars[keys[keys.length-1]][val];
}
else if (!Number.isInteger(val) && typeof val != 'boolean') {
retval = parseFloat(val).toExponential(3);
}
else {
retval = val;
}
return retval;
}
}
}
</script>
<style scoped>
.diff {
text-align: left;
}
.diff-path {
font-family: "Roboto Mono", monospace;
}
.diff-old {
font-weight: bold;
}
.diff-new {
font-weight: bold;
color: green;
}
.diff-same {
font-weight: bold;
color: blue;
}
.wizard-button:active {
background-color: var(--bg-color);
}
</style>
@@ -0,0 +1,117 @@
<template>
<div class="card wizard-motor-custom wizard-choice" :class="{'choice-inactive': !allowed}">
<div class="left">
<span>Input filter bandwidth =</span>
<input type="number" v-on:change="setBandwidth" :placeholder="bandwidth" />
<span>Hz</span>
</div>
<span class="name">Filtered Position Input Mode</span>
</div>
</template>
<script>
import odriveEnums from "../../../assets/odriveEnums.json";
import { getVal } from "../../../lib/odrive_utils.js"
export default {
name: "wizardInputFiltered",
props: {
data: Object,
allowed: Boolean,
},
data: function () {
return {
bandwidth: undefined,
};
},
created() {
let path = "odrive0." + this.data.axis + ".controller.config.input_filter_bandwidth";
this.bandwidth = parseFloat(getVal(path));
},
methods: {
sendConfig() {
console.log("emitting choice event from limits page");
let configStub = undefined;
if (this.data.axis == "axis0") {
configStub = {
axis0: {
controller: {
config: {
input_filter_bandwidth: this.bandwidth,
input_mode: odriveEnums.INPUT_MODE_POS_FILTER,
},
},
},
};
} else if (this.data.axis == "axis1") {
configStub = {
axis1: {
controller: {
config: {
input_filter_bandwidth: this.bandwidth,
input_mode: odriveEnums.INPUT_MODE_POS_FILTER,
},
},
},
};
}
this.$emit("choice", {
choice: "Input Filter " + this.data.axis,
configStub: configStub,
hooks: [],
});
},
setBandwidth(e) {
this.bandwidth = parseFloat(e.target.value);
this.sendConfig();
},
},
};
</script>
<style scoped>
.wizard-choice {
display: flex;
flex-direction: column;
margin: 2rem;
}
.left {
margin-left: auto;
}
.name {
margin-top: auto;
}
input {
width: 5rem;
font-family: inherit;
border-style: none;
border-bottom: 1px solid grey;
text-align: center;
background-color: transparent;
}
input::-webkit-outer-spin-button,
input::-webkit-inner-spin-button {
/* display: none; <- Crashes Chrome on hover */
-webkit-appearance: none;
margin: 0; /* <-- Apparently some margin are still there even though it's hidden */
}
input[type="number"] {
-moz-appearance: textfield; /* Firefox */
}
.measure-button {
margin-top: 2rem;
}
.measure-button:active {
background-color: var(--bg-color);
}
.name {
text-align: center;
}
</style>
@@ -0,0 +1,160 @@
<template>
<div class="card wizard-motor-custom wizard-choice" :class="{'choice-inactive': !allowed}">
<div class="left">
<span>Velocity limit =</span>
<input type="number" v-on:change="setVelLimit" :placeholder="vel_limit" />
<span>turns / s</span>
</div>
<div class="left">
<span>Acceleration limit =</span>
<input type="number" v-on:change="setAccelLimit" :placeholder="accel_limit" />
<span>turns / s^2</span>
</div>
<div class="left">
<span>Deceleration limit =</span>
<input type="number" v-on:change="setDecelLimit" :placeholder="decel_limit" />
<span>turns / s^2</span>
</div>
<div class="left">
<span>Inertia =</span>
<input type="number" v-on:change="setInertia" :placeholder="inertia" />
</div>
<span class="name">Trajectory Planner</span>
</div>
</template>
<script>
import { getVal } from "../../../lib/odrive_utils.js"
export default {
name: "wizardInputTrajectory",
props: {
data: Object,
allowed: Boolean,
},
data: function () {
return {
vel_limit: undefined,
accel_limit: undefined,
decel_limit: undefined,
inertia: undefined,
};
},
created() {
// get pre-existing values for trap_traj vel limit, accel, devel, and config.inertia
this.vel_limit = getVal("odrive0." + this.data.axis + ".trap_traj.config.vel_limit");
this.accel_limit = getVal("odrive0." + this.data.axis + ".trap_traj.config.accel_limit");
this.decel_limit = getVal("odrive0." + this.data.axis + ".trap_traj.config.decel_limit");
this.inertia = getVal("odrive0." + this.data.axis + ".controller.config.inertia");
},
methods: {
sendConfig() {
console.log("emitting choice event from limits page");
let configStub = undefined;
if (this.data.axis == "axis0") {
configStub = {
axis0: {
controller: {
config: {
inertia: this.inertia,
},
},
trap_traj: {
config: {
vel_limit: this.vel_limit,
accel_limit: this.accel_limit,
decel_limit: this.decel_limit,
},
},
},
};
} else if (this.data.axis == "axis1") {
configStub = {
axis1: {
controller: {
config: {
inertia: this.inertia,
},
},
trap_traj: {
config: {
vel_limit: this.vel_limit,
accel_limit: this.accel_limit,
decel_limit: this.decel_limit,
},
},
},
};
}
this.$emit("choice", {
choice: "Trapezoidal trajectory " + this.data.axis,
configStub: configStub,
hooks: [],
});
},
setVelLimit(e) {
this.vel_limit = parseFloat(e.target.value);
this.sendConfig();
},
setAccelLimit(e) {
this.accel_limit = parseFloat(e.target.value);
this.sendConfig();
},
setDecelLimit(e) {
this.decel_limit = parseFloat(e.target.value);
this.sendConfig();
},
setInertia(e) {
this.inertia = parseFloat(e.target.value);
this.sendConfig();
},
},
};
</script>
<style scoped>
.wizard-choice {
display: flex;
flex-direction: column;
margin: 2rem;
}
.left {
margin-left: auto;
}
.name {
margin-top: auto;
}
input {
width: 5rem;
font-family: inherit;
border-style: none;
border-bottom: 1px solid grey;
text-align: center;
background-color: transparent;
}
input::-webkit-outer-spin-button,
input::-webkit-inner-spin-button {
/* display: none; <- Crashes Chrome on hover */
-webkit-appearance: none;
margin: 0; /* <-- Apparently some margin are still there even though it's hidden */
}
input[type="number"] {
-moz-appearance: textfield; /* Firefox */
}
.measure-button {
margin-top: 2rem;
}
.measure-button:active {
background-color: var(--bg-color);
}
.name {
text-align: center;
}
</style>
@@ -0,0 +1,117 @@
<template>
<div class="card wizard-motor-custom wizard-choice" :class="{'choice-inactive': !allowed}">
<div class="left">
<span>Velocity ramp rate =</span>
<input type="number" v-on:change="setRampRate" :placeholder="vel_ramp_rate" />
<span>turns / s^2</span>
</div>
<span class="name">Ramped Velocity Input Mode</span>
</div>
</template>
<script>
import odriveEnums from "../../../assets/odriveEnums.json";
import { getVal } from "../../../lib/odrive_utils.js"
export default {
name: "wizardInputVelRamp",
props: {
data: Object,
allowed: Boolean,
},
data: function () {
return {
vel_ramp_rate: undefined,
};
},
created() {
let path = "odrive0." + this.data.axis + ".controller.config.vel_ramp_rate";
this.vel_ramp_rate = parseFloat(getVal(path));
},
methods: {
sendConfig() {
console.log("emitting choice event from limits page");
let configStub = undefined;
if (this.data.axis == "axis0") {
configStub = {
axis0: {
controller: {
config: {
vel_ramp_rate: this.vel_ramp_rate,
input_mode: odriveEnums.INPUT_MODE_VEL_RAMP,
},
},
},
};
} else if (this.data.axis == "axis1") {
configStub = {
axis1: {
controller: {
config: {
vel_ramp_rate: this.vel_ramp_rate,
input_mode: odriveEnums.INPUT_MODE_VEL_RAMP,
},
},
},
};
}
this.$emit("choice", {
choice: "Velocity ramp " + this.data.axis,
configStub: configStub,
hooks: [],
});
},
setRampRate(e) {
this.vel_ramp_rate = parseFloat(e.target.value);
this.sendConfig();
},
},
};
</script>
<style scoped>
.wizard-choice {
display: flex;
flex-direction: column;
margin: 2rem;
}
.left {
margin-left: auto;
}
.name {
margin-top: auto;
}
input {
width: 5rem;
font-family: inherit;
border-style: none;
border-bottom: 1px solid grey;
text-align: center;
background-color: transparent;
}
input::-webkit-outer-spin-button,
input::-webkit-inner-spin-button {
/* display: none; <- Crashes Chrome on hover */
-webkit-appearance: none;
margin: 0; /* <-- Apparently some margin are still there even though it's hidden */
}
input[type="number"] {
-moz-appearance: textfield; /* Firefox */
}
.measure-button {
margin-top: 2rem;
}
.measure-button:active {
background-color: var(--bg-color);
}
.name {
text-align: center;
}
</style>
@@ -0,0 +1,166 @@
<template>
<div
class="card wizard-motor-custom wizard-choice"
:class="{ 'choice-inactive': !allowed }"
>
<div class="left">
<span>Motor Velocity Limit =</span>
<input
v-on:change="setVelocityLimit"
:placeholder="velocityLimit"
:value="vel_limit"
/>
<span class="unit">[{{ velUnit }}]</span>
</div>
<div class="left">
<span>Motor Current Limit =</span>
<input
v-on:change="setCurrentLimit"
:placeholder="currentLimit"
:value="current_lim"
/>
<span class="unit">[Amps]</span>
</div>
</div>
</template>
<script>
import { getVal, getUnit, parseMath } from "../../../lib/odrive_utils.js";
export default {
name: "wizardMisc",
props: {
data: Object,
allowed: Boolean,
},
data: function () {
return {
current_lim: undefined,
vel_limit: undefined,
vel_set: false,
current_set: false,
};
},
computed: {
velocityLimit: function () {
let path = "odrive0." + this.data.axis + ".controller.config.vel_limit";
return parseFloat(getVal(path));
},
currentLimit: function () {
let path = "odrive0." + this.data.axis + ".motor.config.current_lim";
return parseFloat(getVal(path));
},
velUnit() {
let path = "odrive0." + this.data.axis + ".controller.config.vel_limit";
return getUnit(this.$store.state.odrives.odrive0, path);
},
},
methods: {
sendConfig() {
if (this.vel_set == true && this.current_set == true) {
console.log("emitting choice event from limits page");
let configStub = undefined;
if (this.data.axis == "axis0") {
configStub = {
axis0: {
controller: {
config: {
vel_limit: this.vel_limit,
},
},
motor: {
config: {
current_lim: this.current_lim,
},
},
},
};
} else if (this.data.axis == "axis1") {
configStub = {
axis1: {
controller: {
config: {
vel_limit: this.vel_limit,
},
},
motor: {
config: {
current_lim: this.current_lim,
},
},
},
};
}
this.$emit("choice", {
choice: "Misc " + this.data.axis,
configStub: configStub,
hooks: [],
});
}
},
setVelocityLimit(e) {
let val = parseMath(e.target.value);
if (val !== false) {
this.vel_limit = parseFloat(e.target.value);
this.vel_set = true;
this.sendConfig();
}
},
setCurrentLimit(e) {
let val = parseMath(e.target.value);
if (val !== false) {
this.current_lim = parseFloat(e.target.value);
this.current_set = true;
this.sendConfig();
}
},
},
};
</script>
<style scoped>
.wizard-choice {
display: flex;
flex-direction: column;
margin: 2rem;
}
.left {
margin-right: auto;
}
input {
width: 5rem;
font-family: inherit;
border-style: none;
border-bottom: 1px solid grey;
text-align: center;
background-color: transparent;
}
input::-webkit-outer-spin-button,
input::-webkit-inner-spin-button {
/* display: none; <- Crashes Chrome on hover */
-webkit-appearance: none;
margin: 0; /* <-- Apparently some margin are still there even though it's hidden */
}
input[type="number"] {
-moz-appearance: textfield; /* Firefox */
}
.measure-button {
margin-top: 2rem;
}
.measure-button:active {
background-color: var(--bg-color);
}
.name {
text-align: center;
}
.unit {
font-family: "Roboto Mono", monospace;
}
</style>
@@ -0,0 +1,198 @@
<template>
<div class="card wizard-motor-custom wizard-choice" :class="{'choice-inactive': !allowed}">
<div class="left">
<span>KV =</span>
<input type="number" v-on:change="setKV" placeholder="Change Me!" />
</div>
<div class="left">
<span>Pole Pairs =</span>
<input type="number" v-on:change="setPP" placeholder="Change Me!" />
</div>
<div class="left">
<span>Phase Resistance =</span>
<span>{{" " + resistance}}</span>
<span class="unit">[Ohms]</span>
</div>
<div class="left">
<span>Phase Inductance =</span>
<span>{{" " + inductance}}</span>
<span class="unit">[Henries]</span>
</div>
<span class="name">Other Motor</span>
</div>
</template>
<script>
import odriveEnums from "../../../assets/odriveEnums.json";
import { getVal } from "../../../lib/odrive_utils.js"
export default {
name: "wizardMotor",
props: {
data: Object,
axis: String,
allowed: Boolean,
},
data: function () {
return {
kv_set: false,
pp_set: false,
r_set: false,
l_set: false,
pole_pairs: undefined,
phase_resistance: undefined,
phase_inductance: undefined,
torque_constant: undefined,
};
},
computed: {
resistance: function () {
let path = "odrive0." + this.data.axis + ".motor.config.phase_resistance";
return parseFloat(getVal(path)).toExponential(3);
},
inductance: function () {
let path = "odrive0." + this.data.axis + ".motor.config.phase_inductance";
return parseFloat(getVal(path)).toExponential(3);
},
},
watch: {
resistance: function (newVal) {
console.log("from resistance watcher: " + newVal);
let path = "odrive0." + this.data.axis + ".motor.config.phase_resistance";
this.phase_resistance = parseFloat(getVal(path));
},
inductance: function (newVal) {
console.log("from inductance watcher: " + newVal);
let path = "odrive0." + this.data.axis + ".motor.config.phase_inductance";
this.phase_inductance = parseFloat(getVal(path));
},
pp_set: function () {
console.log("pp_set: " + this.pp_set);
this.sendConfig();
},
kv_set: function () {
console.log("kv_set: " + this.kv_set);
this.sendConfig();
},
r_set: function () {
console.log("r_set: " + this.r_set);
this.sendConfig();
},
l_set: function () {
console.log("l_set: " + this.l_set);
this.sendConfig();
},
},
methods: {
sendConfig() {
// if all of the set flags are true, emit an event
if (this.phase_resistance != 0) {
this.r_set = true;
} else {
this.r_set = false;
}
if (this.phase_inductance != 0) {
this.l_set = true;
} else {
this.l_set = false;
}
if (
this.r_set == true &&
this.l_set == true &&
this.pp_set == true &&
this.kv_set == true
) {
console.log("emitting choice event from other motor");
let configStub = undefined;
if (this.axis == "axis0") {
configStub = {
axis0: {
motor: {
config: {
pole_pairs: this.pole_pairs,
torque_constant: this.torque_constant,
phase_resistance: this.phase_resistance,
phase_inductance: this.phase_inductance,
type: odriveEnums.MOTOR_TYPE_HIGH_CURRENT,
},
},
},
};
} else if (this.axis == "axis1") {
configStub = {
axis1: {
motor: {
config: {
pole_pairs: this.pole_pairs,
torque_constant: this.torque_constant,
phase_resistance: this.phase_resistance,
phase_inductance: this.phase_inductance,
type: odriveEnums.MOTOR_TYPE_HIGH_CURRENT,
},
},
},
};
}
this.$emit("choice", {
choice: "Other motor",
configStub: configStub,
hooks: [],
});
}
},
setKV(e) {
this.torque_constant = 8.27 / parseFloat(e.target.value);
this.kv_set = true;
console.log(this.torque_constant);
},
setPP(e) {
this.pole_pairs = parseFloat(e.target.value);
this.pp_set = true;
console.log(this.pole_pairs);
},
},
};
</script>
<style scoped>
.wizard-choice {
display: flex;
flex-direction: column;
margin: 2rem;
}
.left {
margin-right: auto;
}
.name {
margin-top: auto;
}
input {
width: 5rem;
font-family: inherit;
border-style: none;
border-bottom: 1px solid grey;
text-align: center;
background-color: transparent;
}
input::-webkit-outer-spin-button,
input::-webkit-inner-spin-button {
/* display: none; <- Crashes Chrome on hover */
-webkit-appearance: none;
margin: 0; /* <-- Apparently some margin are still there even though it's hidden */
}
input[type="number"] {
-moz-appearance: textfield; /* Firefox */
}
.name {
text-align: center;
}
.unit {
font-family: "Roboto Mono", monospace;
}
</style>
@@ -0,0 +1,38 @@
<template>
<div class="cal-message"
v-tooltip.top="{
content: data.tooltip,
class: 'tooltip-custom tooltip-other-custom fade-in',
delay: 0,
visible: !calStatus,
}"
>{{message}}</div>
</template>
<script>
export default {
name: "wizardCalStatus",
props: {
data: Object,
calStatus: Boolean,
},
computed: {
message() {
let retmsg = "";
if (this.calStatus == true) {
retmsg = this.data.success;
} else if (this.calStatus == false) {
retmsg = this.data.failure;
}
return retmsg;
},
},
};
</script>
<style scoped>
.cal-message {
margin-top: auto;
margin-bottom: auto;
}
</style>
@@ -0,0 +1,35 @@
<template>
<div>
<button class="measure-button card" @click="calibrate">{{text}}</button>
</div>
</template>
<script>
export default {
name: "wizardEncoderCal",
props: {
data: Object,
calibrating: Boolean,
},
computed: {
text() {
return this.calibrating ? "Calibrating..." : "Calibrate Encoder";
}
},
methods: {
calibrate() {
// ask ODrive to measure resistance and inductance
this.$emit('page-comp-event', {data: "encoder calibration", axis: this.data.axis});
},
},
};
</script>
<style scoped>
.measure-button:active {
background-color: var(--bg-color);
}
</style>
@@ -0,0 +1,36 @@
<template>
<div>
<button class="measure-button card" @click="calibrate">{{text}}</button>
</div>
</template>
<script>
//import odriveEnums from "../../../assets/odriveEnums.json";
//import { putVal } from "../../../lib/odrive_utils.js"
export default {
name: "wizardMotorCal",
props: {
data: Object,
calibrating: Boolean,
},
computed: {
text() {
return this.calibrating ? "Calibrating..." : "Calibrate Motor";
}
},
methods: {
calibrate() {
this.$emit('page-comp-event', {data: "motor calibration", axis: this.data.axis});
},
},
};
</script>
<style scoped>
.measure-button:active {
background-color: var(--bg-color);
}
</style>
@@ -0,0 +1,190 @@
<template>
<!-- render a list of choices horizontally in cards -->
<div class="wizard-page card">
<div class="wizard-title">{{title}}</div>
<div class="wizard-choices">
<!--
<wizardChoice
v-on:click.native="selectedChoice=choice.text;$emit('choice', {title: title, choice: choice.text, configStub: choice.configStub, hooks: choice.hooks})"
/>
-->
<template v-for="choice in choices">
<component
:class="{chosen: selectedChoice == choice.title, unchosen: selectedChoice != choice.title, inactive: !requirementsMet(choice.requirements, config)}"
:is="choice.component"
:data="choice.data"
:config="config"
:axis="axis"
:title="choice.title"
:selected="selectedChoice == choice.title"
:hooks="choice.hooks"
:allowed="requirementsMet(choice.requirements, config)"
v-tooltip.top="{
content: requirementsMet(choice.requirements, config) ? choice.tooltip : choice.altTooltip,
class: 'tooltip-custom tooltip-other-custom fade-in',
delay: 0,
visible: choice.tooltip!=null,
}"
:key="choice.title"
v-on:click.native="choiceHandler(choice)"
v-on:choice="handleCustomChoice"
v-on:undo-choice="undoChoice"
/>
</template>
</div>
<div class="page-components">
<template v-for="pageComponent in pageComponents">
<component
:is="pageComponent.component"
:data="pageComponent.data"
:key="pageComponent.id"
:calibrating="calibrating"
:calStatus="calStatus"
v-on:page-comp-event="pageCompEvent"
/>
</template>
</div>
</div>
</template>
<script>
import wizardChoice from "./choices/wizardChoice.vue";
import wizardMisc from "./choices/wizardMisc.vue";
import wizardMotor from "./choices/wizardMotor.vue";
import wizardEncoderIncremental from "./choices/wizardEncoderIncremental.vue";
import wizardEncoderIncrementalIndex from "./choices/wizardEncoderIncrementalIndex.vue";
import wizardEnd from "./choices/wizardEnd.vue";
import wizardMotorCal from "./page_components/wizardMotorCal.vue";
import wizardEncoderCal from "./page_components/wizardEncoderCal.vue";
import wizardCalStatus from "./page_components/wizardCalStatus.vue";
import wizardBrake from "./choices/wizardBrake.vue";
import wizardInputFiltered from "./choices/wizardInputFiltered.vue";
import wizardInputVelRamp from "./choices/wizardInputVelRamp.vue";
import wizardInputTrajectory from "./choices/wizardInputTrajectory.vue";
export default {
name: "wizardPage",
props: {
choices: Array,
title: String,
customComponents: Array,
axis: String,
config: Object,
pageComponents: Array,
calibrating: Boolean,
calStatus: Boolean,
},
components: {
wizardChoice,
wizardMisc,
wizardMotor,
wizardEncoderIncremental,
wizardEncoderIncrementalIndex,
wizardEnd,
wizardMotorCal,
wizardEncoderCal,
wizardCalStatus,
wizardBrake,
wizardInputFiltered,
wizardInputVelRamp,
wizardInputTrajectory,
},
data: function () {
return {
selectedChoice: undefined,
};
},
methods: {
handleCustomChoice(e) {
console.log(e);
this.$emit("choice", e);
},
undoChoice(e) {
this.$emit("undo-choice",e);
},
pageCompEvent(e) {
this.$emit("page-comp-event", e);
},
requirementsMet(reqs, wizardConfig){
let reqsMet = true;
for (const fn of reqs) {
reqsMet = reqsMet && fn(wizardConfig);
}
return reqsMet;
},
choiceHandler(choice) {
console.log(choice.title);
if (this.selectedChoice == choice.title) {
// undo selection
this.selectedChoice = undefined;
}
else if (this.requirementsMet(choice.requirements, this.config)){
this.selectedChoice = choice.title;
}
}
},
};
</script>
<style>
.wizard-page {
display: flex;
flex-direction: column;
text-align: center;
}
.wizard-choices {
display: flex;
flex-direction: row;
flex-wrap: wrap;
}
.page-components {
display: flex;
flex-direction: row;
justify-content: center;
}
.chosen {
border: 2px solid black;
}
.unchosen {
border: 2px solid transparent;
}
.inactive {
background-color: var(--bg-color);
}
.wizard-title {
font-weight: bold;
}
.vue-tooltip.tooltip-custom {
background-color: lightyellow; /* var(--fg-color); */
border-radius: 0px;
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.4);
font-family: "Roboto", sans-serif;
color: black;
}
.vue-tooltip.tooltip-custom .tooltip-arrow {
display: none;
}
.vue-tooltip.fade-in {
opacity: 0;
animation: fadeIn ease 0.25s;
animation-fill-mode: both;
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
</style>
@@ -0,0 +1,242 @@
import store from "../store.js";
import * as socketio from "../comms/socketio.js";
import {wait, waitFor} from "./utils.js";
import odriveEnums from "../assets/odriveEnums.json"
// helper functions and utilities for getting ODrive values
// given a path like "odrive0.axis0.config.blah", return the value
export function getParam(path) {
let keys = path.split('.');
if (store.state.ODrivesConnected[keys[0]]) {
let odriveObj = store.state.odrives;
for (const key of keys) {
odriveObj = odriveObj[key];
}
return odriveObj;
}
else {
console.log("getParam for " + path + " is for disconnected ODrive");
return undefined;
}
}
// wrapper for val field
export function getVal(path) {
return getParam(path + ".val");
}
// wrapper for readonly field
export function getReadonly(path) {
return getParam(path + '.readonly');
}
export function fetchParam(path) {
if (store.state.serverConnected){
socketio.sendEvent({
type: "getProperty",
data: {path: path},
});
}
}
export function parseMath(inString) {
// given an input string that is valid arithmetic, use eval() to evaluate it
//let allowedChars = "0123456789eE/*-+.()";
let send = true;
//for (const c of inString) {
// if (!allowedChars.includes(c)) {
// send = false;
// }
//}
if (send) {
return eval(inString);
}
else {
return false;
}
}
export function putVal(path, value) {
console.log("path: " + path + ", val: " + value + ", type: " + typeof value);
if (store.state.ODrivesConnected[path.split('.')[0]]) {
if (value == Number.POSITIVE_INFINITY) {
value = "Infinity";
}
else if (value == Number.NEGATIVE_INFINITY) {
value = "-Infinity";
}
socketio.sendEvent({
type: "setProperty",
data: {path: path, val: value, type: typeof value}
});
}
else {
console.log("requesting " + path + " from disconnected odrive")
}
}
// path is path to function, args is list of parameters
export function callFcn(path, args = []) {
console.log("calling function: " + path);
socketio.sendEvent({
type: "callFunction",
data: {path: path, args: args}
});
}
export function odriveMinorVersion(odrive) {
// given an odrive object, return the minor version
return odrive.fw_version_minor.val;
}
export function getUnit(odrive, path) {
let unit = undefined;
for ( const key of Object.keys(odriveUnits[odriveMinorVersion(odrive)]) ) {
if (path.includes(key)) {
unit = odriveUnits[odriveMinorVersion(odrive)][key];
}
}
return unit;
}
export function clearErrors(odrive, axis) {
// odrive is odrive path like 'odrive0'
// axis is 'axis0', etc
let paths = [];
[".error", ".motor.error", ".encoder.error", ".controller.error"].forEach((path) => {
paths.push(odrive + axis + path);
});
paths.forEach((path) => {
putVal(path, 0);
})
}
export async function motorCalibration(odrive, axis) {
// set up our continuous fetch
let state;
let motorError;
let updateVals = () => {
fetchParam(odrive + axis + ".current_state");
fetchParam(odrive + axis + ".motor.config.phase_resistance");
fetchParam(odrive + axis + ".motor.config.phase_inductance");
fetchParam(odrive + axis + ".motor.is_calibrated");
fetchParam(odrive + axis + ".motor.error")
state = getVal(odrive + axis + ".current_state");
motorError = getVal(odrive + axis + ".motor.error");
}
// set up our state watch function
let end = () => {
// return {done: boolean, data <whatever>}
if (state != odriveEnums.AXIS_STATE_MOTOR_CALIBRATION) {
return {done: true, data: motorError}
}
else {
return {done: false}
}
}
// start getting live updates
let cylicUpdate = setInterval(updateVals, 100);
// send motor calibration command to correct odrive and axis
putVal(odrive + axis + ".requested_state", odriveEnums.AXIS_STATE_MOTOR_CALIBRATION);
// give it some time to start
await wait(500);
// only two things can happen now, either we successfully calibrate or we error out for some reason
const result = await waitFor(end);
// stop our parameter updates
clearInterval(cylicUpdate);
return result;
}
export async function encoderCalibration(odrive,axis) {
// set up our continuous fetch
let state;
let encoderError;
let updateVals = () => {
fetchParam(odrive + axis + ".current_state");
fetchParam(odrive + axis + ".encoder.is_ready");
fetchParam(odrive + axis + ".encoder.error")
state = getVal(odrive + axis + ".current_state");
encoderError = getVal(odrive + axis + ".encoder.error");
}
// set up our state watch function
let end = () => {
// return {done: boolean, data <whatever>}
if (state != odriveEnums.AXIS_STATE_ENCODER_OFFSET_CALIBRATION) {
return {done: true, data: encoderError}
}
else {
return {done: false}
}
}
// start getting live updates
let cylicUpdate = setInterval(updateVals, 100);
// start encoder offset cal
putVal(odrive + axis + ".requested_state", odriveEnums.AXIS_STATE_ENCODER_OFFSET_CALIBRATION);
// give it some time to start
await wait(500);
// only two things can happen now, either we successfully calibrate or we error out for some reason
const result = await waitFor(end);
// stop our parameter updates
clearInterval(cylicUpdate);
return result;
}
// standins for wizard and gui unit displays
// TODO - convert between odrive api units and user-defined units like degrees and rpm
export let odriveUnits = {
"4": {
"config.brake_resistance": "Ohms",
"controller.pos_setpoint": "counts",
"controller.vel_setpoint": "counts/s",
"controller.current_setpoint": "Amps",
"controller.config.vel_limit": "counts/s",
"controller.config.vel_ramp_rate": "counts/s^2",
"encoder.pos_estimate": "counts",
"encoder.vel_estimate": "counts/s",
"trap_traj.config.vel_limit": "counts/s",
"trap_traj.config.accel_limit": "counts/s^2",
"trap_traj.config.decel_limit": "counts/s^2",
"motor.config.phase_resistance": "Ohms",
"motor.config.phase_inductance": "Henries",
"motor.config.current_lim": "Amps",
"vbus_voltage": "Volts",
},
"5": {
"config.brake_resistance": "Ohms",
"controller.input_pos": "turns",
"controller.input_vel": "turns/s",
"controller.torque_setpoint": "Amps",
"controller.input_torque": "N/m",
"controller.config.vel_limit": "turns/s",
"controller.config.vel_ramp_rate": "turns/s^2",
"encoder.pos_estimate": "turns",
"encoder.vel_estimate": "turns/s",
"trap_traj.config.vel_limit": "turns/s",
"trap_traj.config.accel_limit": "turns/s^2",
"trap_traj.config.decel_limit": "turns/s^2",
"motor.config.phase_resistance": "Ohms",
"motor.config.phase_inductance": "Henries",
"motor.config.current_lim": "Amps",
"motor.config.torque_lim": "N/m",
"vbus_voltage": "Volts",
}
}
+93
View File
@@ -0,0 +1,93 @@
// general utilities for use in the GUI
// given an object and a test function, return a new object where only
// the values where test(value) == true are kept, as well as the parents
// of those objects. Nested non-matching children are set to {}
// ex: obj = {parent: {child1: {child2: "hello"}, child1a: {child2a: "world"}}}, test = ((o) => o == "hello"),
// filterBy(obj, test) -> {parent: {child1: {child2: "hello"}, child1a: {}}}
export let filterBy = (obj, test) => {
let retobj = {};
Object.entries(obj).forEach(([key, val]) => {
if (test(val)) {
retobj[key] = val;
}
else if (typeof val == 'object' && val != null) {
retobj[key] = filterBy(val, test);
}
})
return retobj;
}
// given an object and test function, delete values where test(val) == true
// modifies obj in place
export let deleteBy = (obj, test) => {
Object.keys(obj).forEach((key) => {
if (typeof obj[key] == 'object' && obj[key] != null) {
deleteBy(obj[key], test);
}
if (test(obj[key])) {
delete obj[key];
}
})
}
// sets up a timeout to wait for fun() to evaluate to true;
// fun must return {done: boolean, data: <whatever>}
export let waitFor = (fun) => {
return new Promise(resolve => {
let check = () => {
let res = fun();
if (res.done) {
resolve(res.data);
}
else {
setTimeout(check,100);
}
}
check();
});
}
// utility function to allow time for things to happen
// async wait
export let wait = (time) => {
return new Promise(resolve => {
setTimeout(() => resolve(), time);
});
}
export let pathsFromTree = (tree) => {
let flatpaths = [];
let path = [];
let pathFromTree = (tree) => {
for (const key of Object.keys(tree)) {
path.push(key);
if (tree[key] != null && typeof tree[key] == "object") {
pathFromTree(tree[key]);
}
else {
flatpaths.push(path.join('.'));
}
path.pop();
}
}
// array path
pathFromTree(tree);
return flatpaths;
}
export let numberDisplay = (val) => {
// if a number can be represented with 3 decimals, return it in that form
// otherwise, return scientific notation
let retVal = '';
try {
retVal = parseFloat(val).toFixed(3);
if (retVal == '0.000' && val != 0 || retVal.length > 7) {
retVal = parseFloat(val).toExponential(3);
}
} catch (error) {
console.log(error);
}
return retVal;
}
+14
View File
@@ -0,0 +1,14 @@
import Vue from 'vue'
import App from './App.vue'
import store from './store'
import Tooltip from 'vue-directive-tooltip';
import 'vue-directive-tooltip/dist/vueDirectiveTooltip.css';
import 'typeface-roboto/index.css';
Vue.config.productionTip = false
Vue.use(Tooltip);
new Vue({
store,
render: h => h(App)
}).$mount('#app')
+2
View File
@@ -0,0 +1,2 @@
import { ipcRenderer } from 'electron'
window.ipcRenderer = ipcRenderer
+420
View File
@@ -0,0 +1,420 @@
import Vue from 'vue';
import Vuex from 'vuex';
import TuningDashOld from "./assets/dashboards/Tuning_0_4_12.json";
import TuningDashNew from "./assets/dashboards/Tuning_0_5_1.json";
import * as socketio from "./comms/socketio";
import {filterBy, deleteBy } from "./lib/utils.js";
//import { v4 as uuidv4 } from "uuid";
Vue.use(Vuex);
export default new Vuex.Store({
// state is the data for this app
state: {
odrives: Object,
odriveConfigs: Object,
odriveParameters: Object,
odriveFunctions: Object,
axes: Array,
odriveServerAddress: String,
serverConnected: Boolean,
ODrivesConnected: Object,
serverOutput: [],
dashboards: [
{
name: "Start",
component: "Start",
},
],
timeSampleStart: 0,
sampledProperties: [], // make this an object where the full path is a key and the value is the sampled var
propSamples: { time: [] }, // {time: [time values], ...path: [path var values]}
newData: false,
sampling: false,
currentDash: "Start",
firstConn: false,
wizardMotor: "odrive0",
},
// mutations are functions that change the data
mutations: {
setWizardMotor(state, odrive) {
// odrive is string, "odrive0" or "odriveN"
state.wizardMotor = odrive;
},
setDash(state, dashName) {
state.currentDash = dashName;
},
setOdrives(state, odrives) {
state.odrives = odrives;
if (state.firstConn == false) {
// first time we're getting odrive data, add correct config page
let TuningDash;
if (state.odrives.odrive0.fw_version_minor.val == "5") {
TuningDash = TuningDashNew;
}
else if (state.odrives.odrive0.fw_version_minor.val == "4") {
TuningDash = TuningDashOld;
}
else {
TuningDash = TuningDashNew;
}
state.dashboards.push({
name: "Wizard",
component: "Wizard",
});
state.dashboards.push(TuningDash);
// plots will have variables associated, add them to sampled variables list
for (const plot of TuningDash.plots) {
for (const plotVar of plot.vars) {
//addsampledprop(path);
let path = plotVar.path;
if (!(path in state.sampledProperties)) {
let newPath = path.split('.');
newPath.splice(0, 1);
state.sampledProperties.push(newPath.join('.'));
state.propSamples[newPath.join('.')] = [];
}
socketio.sendEvent({
type: 'sampledVarNames',
data: {
paths: state.sampledProperties
}
});
}
}
}
state.firstConn = true;
},
setOdriveConfigs(state, payload) {
state.odriveConfigs['full'] = payload.full;
state.odriveConfigs['functions'] = payload.functions;
state.odriveConfigs['params'] = payload.params;
state.odriveConfigs['writeAble'] = payload.writeAble;
state.odriveConfigs['writeAbleNumeric'] = payload.writeAbleNumeric;
},
setODrivesStatus(state, obj) {
// obj is {"odriveX": true/false}
for (const odrive of Object.keys(obj)){
state.ODrivesConnected[odrive] = obj[odrive];
console.log(state.ODrivesConnected);
}
},
setAxes(state, axes) {
state.axes = axes;
},
setServerAddress(state, address) {
state.odriveServerAddress = address;
},
updateOdriveProp(state, payload) {
// need to use Vue.set!!!
// payload is {path, value}
//
const createNestedObject = (odrive, path) => {
let ref = odrive;
let keys = path.split('.');
for (const key of keys) {
ref = ref[key];
}
return ref;
};
let val = payload.val
if (val == "Infinity") {
val = Number.POSITIVE_INFINITY;
console.log("val is infinity");
}
else if (val == "-Infinity") {
val = Number.NEGATIVE_INFINITY;
}
Vue.set(createNestedObject(state.odrives, payload.path), "val", val);
},
addSampledProperty(state, path) {
if (!(path in state.sampledProperties)) {
let newPath = path.split('.');
newPath.splice(0, 1);
state.sampledProperties.push(newPath.join('.'));
state.propSamples[newPath.join('.')] = [];
}
socketio.sendEvent({
type: 'sampledVarNames',
data: {
paths: state.sampledProperties
}
});
},
removeSampledProperty(state, path) {
let newPath = path.split('.');
newPath.splice(0, 1);
const index = state.sampledProperties.indexOf(newPath.join('.'));
if (index > -1) {
state.sampledProperties.splice(index, 1);
}
},
updateSampledProperty(state, payload) {
// payload is object of paths and values
for (const path of Object.keys(payload)) {
state.propSamples[path].push(payload[path]);
if (state.propSamples[path].length > 250) {
state.propSamples[path].splice(0, 1); // emulate circular buffer
}
}
state.propSamples["time"].push((Date.now() - state.timeSampleStart) / 1000);
if (state.propSamples["time"].length > 250) {
state.propSamples["time"].splice(0, 1);
}
state.newData = true;
},
logServerMessage(state, payload) {
// payload is string
state.serverOutput.push(payload);
// cut off to 1000 lines
if (state.serverOutput.length > 1000) {
state.serverOutput.splice(0,1);
}
},
setServerStatus(state, val) {
state.serverConnected = val;
},
removeCtrlFromDash(state, obj) {
// obj is {dash: dashID, path: control path}
for (const dash of state.dashboards) {
if (obj.dashID == dash.id) {
for (const control of dash.controls) {
if (obj.path == control.path) {
dash.controls.splice(dash.controls.indexOf(control), 1);
break;
}
}
break;
}
}
},
removeActionFromDash(state, obj) {
// obj is {dashID: dashID, actionID: action ID}
for (const dash of state.dashboards) {
if (obj.dashID == dash.id) {
for (const action of dash.actions) {
if (obj.actionID == action.id) {
dash.actions.splice(dash.actions.indexOf(action), 1);
break;
}
}
break;
}
}
},
removePlotFromDash(state, obj) {
// obj is {dashID: dash ID, plotID: plot ID}
for (const dash of state.dashboards) {
if (obj.dashID == dash.id) {
for (const plot of dash.plots) {
if (obj.plotID == plot.name) {
dash.plots.splice(dash.plots.indexOf(plot), 1);
break;
}
}
break;
}
}
},
setActionVal(state, obj) {
// obj is {dashID: dash ID, actionID: action ID, val: val}
for (const dash of state.dashboards) {
if (obj.dashID == dash.id) {
for (const action of dash.actions) {
if (obj.actionID == action.id) {
action.val = obj.val;
break;
}
}
break;
}
}
},
},
// actions trigger mutations
actions: {
getOdrives() {
socketio.sendEvent({
type: 'getODrives',
});
},
getOdriveConfigs(context) {
// transform ODrive JSON
function treeParse(odriveObj) {
let retObj = {};
for (const key of Object.keys(odriveObj)) {
if (typeof odriveObj[key] === 'object' && odriveObj[key] !== null) {
// check if "val" is a valid key
if (Object.prototype.hasOwnProperty.call(odriveObj[key], "val")) {
// parse from string to a type that we care about
switch (odriveObj[key]["type"]) {
case "str": {
// for handling infinity
let val;
if (odriveObj[key]["val"] == 'Infinity') {
val = Number.POSITIVE_INFINITY;
}
else if (odriveObj[key]["val"] == '-Infinity') {
val = Number.NEGATIVE_INFINITY;
}
retObj[key] = val;
break;
}
case "float":
retObj[key] = parseFloat(parseFloat(odriveObj[key]["val"]).toFixed(3));
break;
case "int8":
case "int16":
case "int32":
case "int64":
case "uint8":
case "uint16":
case "uint32":
case "uint64":
retObj[key] = parseInt(odriveObj[key]["val"]);
break;
case "bool":
retObj[key] = odriveObj[key]["val"] == 'True';
break;
default:
retObj[key] = odriveObj[key]["val"];
}
}
else {
retObj[key] = treeParse(odriveObj[key]);
}
}
else if (odriveObj[key] == "function") {
retObj[key] = "function";
}
else {
retObj[key] = odriveObj[key];
}
}
return retObj;
}
// full parameter tree
// create object containing only ODrive functions
let fullTree = treeParse(context.state.odrives);
let fcns = filterBy(fullTree, ((o) => o == "function"));
deleteBy(fcns, ((o) => o.constructor == Object && Object.keys(o).length == 0));
// create object containing only ODrive parameters
let params = filterBy(fullTree, ((o) => o != "function" && typeof o != "object"));
deleteBy(params, ((o) => o.constructor == Object && Object.keys(o).length == 0))
// create object containing only ODrive parameters with write access
let writeAble = filterBy(context.state.odrives, ((o) => o['readonly'] == false));
deleteBy(writeAble, ((o) => o.constructor == Object && Object.keys(o).length == 0))
// create object of only ODrive parameters that are writeable and numeric
let writeAbleNumeric = filterBy(context.state.odrives, ((o) => o['readonly'] == false && ['float', 'int', 'bool'].includes(o['type'])));
deleteBy(writeAbleNumeric, ((o) => o.constructor == Object && Object.keys(o).length == 0))
context.commit('setOdriveConfigs', {full: fullTree,
functions: fcns,
params: params,
writeAble: treeParse(writeAble),
writeAbleNumeric: treeParse(writeAbleNumeric)});
},
getAxes(context) {
let axes = [];
//for each connected odrive, collect axes and display them
for (const odrive of Object.keys(context.state.odrives)) {
if ('axis0' in context.state.odrives[odrive]) {
axes.push({
name: `${odrive}.axis0`,
});
}
if ('axis1' in context.state.odrives[odrive]) {
axes.push({
name: `${odrive}.axis1`,
});
}
}
context.commit('setAxes', axes);
},
setServerAddress(context, address) {
context.commit('setServerAddress', address);
socketio.setUrl(address);
socketio.addEventListener({
type: "connect",
callback: () => {
context.commit("setServerStatus", true);
console.log('connected to server');
//context.dispatch("getOdrives");
socketio.sendEvent({
type: "findODrives",
});
}
});
socketio.addEventListener({
type: "odrive-found",
callback: () => {
console.log("odrive-found recieved from server");
context.dispatch("getOdrives");
}
})
socketio.addEventListener({
type: "disconnect",
callback: () => {
context.commit("setServerStatus", false);
console.log('server disconnect');
socketio.closeSocket();
context.commit('setAxes', []);
}
});
socketio.addEventListener({
type: "sampledData",
callback: message => {
context.commit("updateSampledProperty", JSON.parse(message));
}
});
socketio.addEventListener({
type: "samplingEnabled",
callback: () => {
socketio.sendEvent({
type: "startSampling"
});
}
});
socketio.addEventListener({
type: "odrives",
callback: odrives => {
context.commit('setOdrives', JSON.parse(odrives));
context.dispatch('getOdriveConfigs');
context.dispatch('getAxes');
}
});
// getVal event gets sent to server, server emits ODriveProperty event with path and val of property
socketio.addEventListener({
type: "ODriveProperty",
callback: retmsg => {
// retmsg is {path, val}
context.commit('updateOdriveProp', JSON.parse(retmsg));
}
});
socketio.addEventListener({
type: "odrive-disconnected",
callback: (odrive_name) => {
console.log(odrive_name + " disconnected");
//console.log("restarting server...");
//window.ipcRenderer.send('kill-server');
//window.ipcRenderer.send('start-server');
//context.dispatch('setServerAddress', context.state.odriveServerAddress);
}
});
socketio.addEventListener({
type: "odrives-status",
callback: (odrives_status) => {
console.log("From odrives-status msg " + odrives_status);
context.commit('setODrivesStatus', JSON.parse(odrives_status));
}
});
}
}
})
@@ -0,0 +1,561 @@
<template>
<div class="dashboard">
<!-- PARAMETER DROPDOWN MENU -->
<div v-show="paramsVisible" class="dropdown">
<div class="card dropdown-content">
<div>
<button class="close-button" @click="hideTree">X</button>
Parameters
</div>
<div class="param-tree">
<json-view
:data="treeParams"
:rootKey="'odrives'"
v-on:selected="addVarToElement"
/>
</div>
</div>
</div>
<div class="dashboard_container">
<div class="controls">
<template v-for="(control, index) in dash.controls">
<component
:is="control.controlType"
:key="index + '-control'"
:path="control.path"
:name="control.name"
:options="control.options"
:odrives="odrives"
:dashID="dash.id"
/>
</template>
<div class="control-buttons">
<div class="add-button card" @click="addComponent('control')">Add Control</div>
<div class="add-button card" @click="addComponent('slider')">Add Slider</div>
<div class="add-button card" @click="addComponent('function')">Add Function</div>
</div>
<template v-for="(action, index) in dash.actions">
<component
:is="action.actionType"
:id="action.id"
:key="index + '-action'"
:path="action.path"
:odrives="odrives"
:initVal="action.val"
:dashID="dash.id"
:options="action.options"
/>
</template>
<div class="add-button card" @click="addComponent('action')">Add Action</div>
</div>
<div class="plots">
<template v-for="(plot, index) in dash.plots">
<plot
:plot="plot"
:key="index"
:name="plot.name"
:dashID="dash.id"
v-on:add-var="currentPlot=plot.name;addComponent('plot')"
/>
</template>
<div class="add-button card" @click="addPlot">Add Plot</div>
</div>
</div>
</div>
</template>
<script>
//this component will get passed a list of controls and plots
//display controls on the left and plots on the right?
//leave full names for plots
//collate controls into individual cards based on the deepest common level
import CtrlBoolean from "../components/controls/CtrlBoolean.vue";
import CtrlNumeric from "../components/controls/CtrlNumeric.vue";
import CtrlFunction from "../components/controls/CtrlFunction.vue";
import CtrlSlider from "../components/controls/CtrlSlider.vue";
import CtrlEnum from "../components/controls/CtrlEnum.vue";
import Plot from "../components/plots/Plot.vue";
import Action from "../components/actions/Action.vue";
import ActionEnum from "../components/actions/ActionEnum.vue"
import { JSONView } from "vue-json-component";
import { v4 as uuidv4 } from "uuid";
let plotColors = [
"#1f77b4", // blue
"#ff7f0e", // orange
"#2ca02c", // green
"#d62728", // red
"#9467bd", // purple
"#8c564b", // brown
"#e377c2", // pink
"#7f7f7f", // gray
"#bcbd22", // olive
"#17becf", // cyan
];
let odriveEnums = {
// axis requested state
requested_state: [
{
text: "Undefined",
value: 0,
},
{
text: "Idle",
value: 1,
},
{
text: "Startup Sequence",
value: 2,
},
{
text: "Full Calibration Sequence",
value: 3,
},
{
text: "Motor Calibration",
value: 4,
},
{
text: "Sensorless Control",
value: 5,
},
{
text: "Encoder Index Search",
value: 6,
},
{
text: "Encoder Offset Calibration",
value: 7,
},
{
text: "Closed Loop Control",
value: 8,
},
{
text: "Lockin Spin",
value: 9,
},
{
text: "Encoder Direction Find",
value: 10,
},
{
text: "Homing",
value: 11,
},
{
text: "Encoder Hall Polarity Calibration",
value: 12,
},
{
text: "Encoder Hall Phase Calibration",
value: 13,
}
],
current_state: [
{
text: "Undefined",
value: 0,
},
{
text: "Idle",
value: 1,
},
{
text: "Startup Sequence",
value: 2,
},
{
text: "Full Calibration Sequence",
value: 3,
},
{
text: "Motor Calibration",
value: 4,
},
{
text: "Sensorless Control",
value: 5,
},
{
text: "Encoder Index Search",
value: 6,
},
{
text: "Encoder Offset Calibration",
value: 7,
},
{
text: "Closed Loop Control",
value: 8,
},
{
text: "Lockin Spin",
value: 9,
},
{
text: "Encoder Direction Find",
value: 10,
},
{
text: "Homing",
value: 11,
},
{
text: "Encoder Hall Polarity Calibration",
value: 12,
},
{
text: "Encoder Hall Phase Calibration",
value: 13,
}
],
// encoder mode
mode: [
{
text: "Incremental",
value: 0,
},
{
text: "Hall Effect",
value: 1,
},
{
text: "Sine Cosine",
value: 2,
},
{
text: "CUI Absolute",
value: 256,
},
{
text: "AMS Absolute",
value: 257,
},
{
text: "AEAT Absolute",
value: 258,
},
{
text: "RLS Absolute",
value: 259,
},
],
// motor type
motor_type: [
{
text: "High Current",
value: 0,
},
{
text: "Gimbal",
value: 2,
},
{
text: "Induction",
value: 3,
},
],
// controller control mode
control_mode: [
{
text: "Voltage Control",
value: 0,
},
{
text: "Torque Control",
value: 1,
},
{
text: "Velocity Control",
value: 2,
},
{
text: "Position Control",
value: 3,
},
],
input_mode: [
{
text: "Inactive",
value: 0
},
{
text: "Passthrough",
value: 1
},
{
text: "Velocity Ramp",
value: 2
},
{
text: "Position Filter",
value: 3
},
{
text: "Mix Channels",
value: 4
},
{
text: "Trapezoidal Trajectory",
value: 5
},
{
text: "Torque Ramp",
value: 6
},
{
text: "Mirror",
value: 7
},
{
text: "Tuning",
value: 8
}
]
}
export default {
name: "Dashboard",
components: {
CtrlBoolean,
CtrlNumeric,
CtrlFunction,
CtrlSlider,
CtrlEnum,
Plot,
Action,
ActionEnum,
"json-view": JSONView,
},
props: ["dash", "odrives"],
data() {
return {
paramsVisible: false,
addCompType: undefined,
currentPlot: undefined,
treeParams: undefined,
};
},
methods: {
deleteAction(e) {
this.$emit("delete-action", e);
console.log(e);
},
deletePlot(e) {
this.$emit("delete-plot", e);
},
addVar(e) {
this.$emit("add-var", e);
},
showTree() {
//show the parameter tree
this.paramsVisible = true;
},
hideTree() {
this.paramsVisible = false;
this.addCompType = undefined;
},
addComponent(componentType) {
this.addCompType = componentType;
switch (this.addCompType) {
case "control":
this.treeParams = this.$store.state.odriveConfigs['params'];
console.log(this.treeParams);
break;
case "plot":
this.treeParams = this.$store.state.odriveConfigs['params'];
break;
case "slider":
this.treeParams = this.$store.state.odriveConfigs['writeAbleNumeric'];
break;
case "action":
this.treeParams = this.$store.state.odriveConfigs['writeAble'];
break;
case "function":
this.treeParams = this.$store.state.odriveConfigs['functions'];
break;
default:
break;
}
this.paramsVisible = true;
},
addVarToElement(e) {
//when the parameter tree is open and a parameter is clicked,
//add the clicked parameter to the list of controls for the
//current dashboard
console.log(e);
switch (this.addCompType) {
case "control":
switch (typeof e.value) {
case "boolean":
this.dash.controls.push({
controlType: "CtrlBoolean",
path: e.path,
});
//this.$store.commit("addSampledProperty", e.path);
break;
case "number":
if (Object.keys(odriveEnums).includes(e.key)){
this.dash.controls.push({
controlType: "CtrlEnum",
path: e.path,
options: odriveEnums[e.key],
})
}
else {
this.dash.controls.push({
controlType: "CtrlNumeric",
path: e.path,
});
}
//this.$store.commit("addSampledProperty", e.path);
break;
case "string":
this.dash.controls.push({
controlType: "CtrlFunction",
path: e.path,
});
break;
default:
break;
}
break;
case "function":
this.dash.controls.push({
controlType: "CtrlFunction",
path: e.path,
});
break;
case "plot":
// add the selected element to the plot var list
// add the selected element to the sampling var list
// find the plot, append path to plot.vars
console.log(e);
for (const plot of this.dash.plots) {
if (plot.name == this.currentPlot) {
plot.vars.push({
path: e.path,
color: plotColors[plot.vars.length % plotColors.length],
});
this.$store.commit("addSampledProperty", e.path);
console.log(plot);
break;
}
}
break;
case "action":
{
// add an action to the current dash
let id = uuidv4();
console.log("action, key = " + e.key);
if (Object.keys(odriveEnums).includes(e.key)){
this.dash.actions.push({
actionType: "ActionEnum",
id: id,
path: e.path,
options: odriveEnums[e.key],
})
}
else {
this.dash.actions.push({
actionType: "Action",
id: id,
path: e.path,
val: undefined,
});
}
}
break;
case "slider":
// add a slider to the list of controls if the selected item is valid (numeric)
switch (typeof e.value) {
case "number":
this.dash.controls.push({
controlType: "CtrlSlider",
path: e.path,
});
break;
default:
break;
}
}
},
addPlot() {
let plotId = uuidv4();
this.dash.plots.push({
name: plotId,
vars: [],
});
},
},
};
</script>
<style scoped>
.dashboard {
background-color: var(--bg-color);
height: 100vh;
max-height: 100vh;
width: 100vw;
padding-top: var(--top-height);
padding-bottom: var(--bottom-height);
}
.dashboard_container {
display: flex;
flex-direction: row;
height: 95vh;
}
.controls {
flex-grow: 1;
border-right: 1px solid lightgrey;
overflow-y: scroll;
}
.add-button {
background-color: lightcyan;
width: 120px;
margin: 10px auto;
margin-left: 10px;
margin-right: 10px;
text-align: center;
border-radius: 20px;
cursor: pointer;
user-select: none;
}
.add-button:active {
background-color: lightblue;
}
.plots {
flex-grow: 1;
max-height: 94vh;
overflow-y: scroll;
display: flex;
flex-direction: column;
}
.control-buttons {
display: flex;
flex-direction: row;
}
.dropdown {
position: absolute;
padding: 0 0;
display: inline-block;
}
.dropdown-content {
position: absolute;
z-index: 1;
}
.param-tree {
overflow-y: scroll;
max-height: 85vh;
}
</style>
+118
View File
@@ -0,0 +1,118 @@
<template>
<div class="home">
<div class="logo">
<img alt="Odrive Logo" src="../assets/odrive_logo.png">
</div>
<div class="home_text">
To set up your ODrive, connect it and power it up.
</div>
<div class="connected-container">
<input type="text" :class="{ notConnected: notConnected, connected: connected}" v-on:change="setUrl" :value="serverAddress">
</div>
<button class="show-msg-button" @click="showServerMessages = !showServerMessages;">Having trouble? Click here for debug output</button>
<div class="server-msgs" v-if="showServerMessages">
<div class="server-msg" v-for="(msg, index) in serverMessages" :key="msg + index">{{msg}}</div>
</div>
</div>
</template>
<script>
export default {
name: 'Home',
components: {
},
data() {
return {
showServerMessages: false,
}
},
computed: {
connected() {
return this.$store.state.serverConnected == true;
},
notConnected() {
return this.$store.state.serverConnected != true;
},
serverAddress() {
return this.$store.state.odriveServerAddress;
},
serverMessages() {
if (this.$store.state.serverOutput.length >= 10) {
let len = this.$store.state.serverOutput.length;
return this.$store.state.serverOutput.slice(len-10,len);
}
else {
return this.$store.state.serverOutput;
}
}
},
methods: {
setUrl(e) {
console.log(e.target.value);
this.$store.dispatch("setServerAddress", e.target.value);
},
}
}
</script>
<style>
.home {
padding: 10% 0;
margin: auto;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background-color: var(--bg-color);
height: 100vh;
}
img {
margin: auto;
display: block;
max-width: 50%;
width: auto;
height: auto;
}
.home_text {
font-size: 1.5rem;
padding: 20px;
}
.connected, .notConnected {
width: 40vw;
margin: 0;
padding: 10px;
border: none;
border-bottom: 1px solid lightgrey;
font-size: 1.5rem;
background-color: var(--bg-color);
text-align: center;
}
input:focus {
outline: none;
}
.connected {
border-bottom: 1px solid lightgreen;
background-color: lightgreen;
}
.notConnected {
border-bottom: 1px solid red;
}
.show-msg-button {
border: none;
background-color: var(--bg-color);
margin-top: 0.5rem;
cursor: pointer;
}
.server-msg {
font-family: "Roboto Mono", monospace;
}
</style>
+395
View File
@@ -0,0 +1,395 @@
<template>
<div class="wizard">
<div class="wizard-container">
<div class="wizard-nav card">
<span class="wizard-nav-title">Wizard Pages</span>
<span
v-for="page in wizardPages"
:key="page.title"
class="wizard-link"
:class="{
'active-link': currentStep == page,
'finished-page': page.choiceMade,
}"
>{{ page.link }}</span
>
</div>
<div class="wizard-page">
<wizardPage
:choices="currentStep.choices"
:customComponents="currentStep.customComponents"
:pageComponents="currentStep.pageComponents"
:title="currentStep.title"
:config="wizardConfig"
:calibrating="calibrating"
:calStatus="calStatus"
v-on:choice="choiceHandler"
v-on:undo-choice="undoChoice"
v-on:page-comp-event="pageEventHandler"
/>
<div class="wizard-controls">
<!-- show breadcrumbs, back, apply, next buttons -->
<button class="wizard-button card" @click="back">Back</button>
<button class="wizard-button card" @click="finish">Finish</button>
<button
class="wizard-button card"
:class="{ 'next-green': choiceMade }"
v-tooltip.right="{
content: currentStep.nextTooltip,
class: 'tooltip-custom tooltip-other-custom fade-in',
delay: 0,
visible: !choiceMade && currentStep.nextTooltip != null,
}"
@click="next"
>
Next
</button>
</div>
</div>
</div>
</div>
</template>
<script>
import configTemplate from "../assets/wizard/configTemplate.json";
import wizardPage from "../components/wizard/wizardPage.vue";
import odriveEnums from "../assets/odriveEnums.json";
import { pages } from "../assets/wizard/wizard.js";
import {
getVal,
putVal,
fetchParam,
clearErrors,
motorCalibration,
encoderCalibration
} from "../lib/odrive_utils.js";
import {wait} from "../lib/utils.js"
export default {
name: "Wizard",
components: {
wizardPage,
},
props: [],
data: function () {
return {
currentStep: pages.ODrive,
wizardConfig: configTemplate,
choiceMade: false,
calibrating: false, // for indicating that calibration is in progress
calStatus: undefined, // for indicating the status of a calibration attempt
odrive: "odrive0.",
};
},
computed: {
wizardPages() {
return pages;
},
},
methods: {
// certain actions, like starting motor or encoder calibration,
// require special handling. This function is used for those events
async pageEventHandler(e) {
this.choiceMade = false;
if (e.data == "motor calibration") {
let apply = () => {
let configStub = {};
configStub[e.axis] = {
motor: {
config: {
phase_resistance: getVal(this.odrive + e.axis + ".motor.config.phase_resistance"),
phase_inductance: getVal(this.odrive + e.axis + ".motor.config.phase_inductance"),
}
}
}
this.choiceHandler({
choice: "Motor Calibration",
configStub: configStub,
hooks: [],
});
this.choiceMade = true;
};
let updateCalInfo = () => {
// parse error code, update calibration information
let motorError = getVal(this.odrive + e.axis + ".motor.error");
if (
motorError == odriveEnums.MOTOR_ERROR_PHASE_RESISTANCE_OUT_OF_RANGE ||
motorError == odriveEnums.MOTOR_ERROR_PHASE_INDUCTANCE_OUT_OF_RANGE
) {
//we got the expected calibration error
console.log("Calibration failed.");
this.calStatus = false;
}
};
// result is motor.error
this.calibrating = true;
let result = await motorCalibration(this.odrive, e.axis);
this.calibrating = false;
fetchParam(this.odrive + e.axis + '.motor.is_calibrated');
// no error, we're good!
if (result == 0) {
wait(250);
apply();
this.calStatus = true;
} else {
console.log("motor cal error" + result.data);
this.calibrating = false;
this.choiceMade = false;
// clear errors, display info.
updateCalInfo();
clearErrors(this.odrive, e.axis);
}
} else if (e.data == "encoder calibration") {
// get old CPR
// apply CPR from this.wizardConfig
// start calibration
// wait for cal to finish
// set odrive cpr back to oldVal
let oldCPR = getVal(this.odrive + e.axis + ".encoder.config.cpr");
console.log("oldCPR = " + oldCPR);
console.log("axis = " + e.axis);
// set up the calib_scan_distance to a value that will work
let pp = this.wizardConfig[e.axis].motor.config.pole_pairs;
// smallest multiple of 4pi that is bigger than pole_pairs * 2pi
let scan_distance = pp % 0 ? pp * 2 * Math.PI : (pp + 1) * 2 * Math.PI;
putVal(this.odrive + e.axis + ".encoder.config.calib_scan_distance", scan_distance);
putVal(
this.odrive + e.axis + ".encoder.config.cpr",
this.wizardConfig[e.axis].encoder.config.cpr
);
await wait(250);
this.calibrating = true;
let result = await encoderCalibration(this.odrive, e.axis);
this.calibrating = false;
putVal(this.odrive + e.axis + ".encoder.config.cpr", oldCPR);
fetchParam(this.odrive + e.axis + '.encoder.is_ready');
if (result == 0){
// no encoder error, we're good
this.choiceMade = true;
this.currentStep.choiceMade = true;
this.calStatus = true;
}
else {
clearErrors(this.odrive, e.axis);
this.calStatus = false;
}
}
},
async choiceHandler(e) {
// apply static configStub
this.updateConfig(this.wizardConfig, e.configStub);
// run hooks
for (const fn of e.hooks) {
this.wizardConfig = fn(this.wizardConfig);
}
// ugly, but a special case.
// for motors, wait for calibration to finish before giving the green light unless motor.is_calibrated == true
// for encoders, wait for calibration to finish unless encoder.is_ready == true
if (
this.currentStep == pages.Motor_0 ||
this.currentStep == pages.Motor_1
) {
let axis;
if (this.currentStep == pages.Motor_0) axis = "axis0";
if (this.currentStep == pages.Motor_1) axis = "axis1";
fetchParam(this.odrive + axis + ".motor.is_calibrated");
await wait(100);
if (getVal(this.odrive + axis + ".motor.is_calibrated") == false) {
this.choiceMade = false;
}
else {
this.choiceMade = true;
}
}
else if (
this.currentStep == pages.Encoder_0 ||
this.currentStep == pages.Encoder_1
) {
let axis;
if (this.currentStep == pages.Encoder_0) axis = "axis0";
if (this.currentStep == pages.Encoder_1) axis = "axis1";
fetchParam(this.odrive + axis + ".encoder.is_ready");
await wait(100);
if (getVal(this.odrive + axis + ".encoder.is_ready") == false) {
this.choiceMade = false;
}
else {
this.choiceMade = true;
}
}
else {
this.choiceMade = true;
}
this.currentStep.choiceMade = this.choiceMade;
console.log(JSON.parse(JSON.stringify(this.wizardConfig)));
},
undoChoice(e) {
this.choiceMade = false;
this.currentStep.choiceMade = this.choiceMade;
this.nullConfig(this.wizardConfig, e.configStub);
console.log(JSON.parse(JSON.stringify(this.wizardConfig)));
},
updateConfig(config, configStub) {
// iterate over keys in configStub
if (configStub != null) {
Object.keys(configStub).forEach((key) => {
if (typeof configStub[key] == "object") {
this.updateConfig(config[key], configStub[key]);
} else {
config[key] = configStub[key];
}
});
}
},
nullConfig(config, configStub) {
// iterate over keys in configStub
if (configStub != null) {
console.log("NullConfig");
Object.keys(configStub).forEach((key) => {
console.log(key);
if (typeof configStub[key] == "object") {
this.nullConfig(config[key], configStub[key]);
} else {
config[key] = null;
}
});
}
},
next() {
console.log("next");
if (this.choiceMade == true) {
this.currentStep = pages[this.currentStep.next];
this.choiceMade = false;
}
this.calStatus = undefined;
},
finish() {
console.log("finish");
this.currentStep = pages.End;
},
back() {
console.log("back");
this.currentStep = pages[this.currentStep.back];
this.choiceMade = false;
},
},
beforeDestroy() {
for (const page of Object.keys(pages)) {
pages[page].choiceMade = false;
}
let erase = (obj) => {
Object.keys(obj).forEach((key) => {
if (typeof obj[key] == "object" && obj[key] != null) {
erase(obj[key]);
} else {
obj[key] = null;
}
});
};
erase(this.wizardConfig);
},
};
</script>
<style scoped>
.wizard {
background-color: var(--bg-color);
height: 100vh;
max-height: 100vh;
width: 100vw;
padding-top: var(--top-height);
padding-bottom: var(--bottom-height);
}
.wizard-nav {
display: flex;
flex-direction: column;
height: fit-content;
}
.wizard-nav-title {
text-align: center;
font-weight: bold;
margin-bottom: 0.4rem;
}
.wizard-link {
margin: 0.2rem;
}
.active-link {
text-decoration: underline;
}
.wizard-container {
display: flex;
flex-direction: row;
height: 95vh;
}
.wizard-page {
margin: auto;
}
.wizard-controls {
margin-top: 0;
display: flex;
justify-content: center;
}
.active {
background-color: var(--fg-color);
}
.inactive {
background-color: var(--bg-color);
color: white;
}
.wizard-button:active {
background-color: var(--bg-color);
}
.next-green {
color: green;
font-weight: bold;
}
.vue-tooltip.tooltip-custom {
background-color: lightyellow; /* var(--fg-color); */
border-radius: 0px;
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.4);
font-family: "Roboto", sans-serif;
color: black;
}
.vue-tooltip.tooltip-custom .tooltip-arrow {
display: none;
}
.vue-tooltip.fade-in {
opacity: 0;
animation: fadeIn ease 0.25s;
animation-fill-mode: both;
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.finished-page {
color: green;
}
</style>
+31
View File
@@ -0,0 +1,31 @@
// this file is used for configuring electron-builder
module.exports = {
pluginOptions: {
electronBuilder: {
preload: 'src/preload.js',
//nodeIntegration: true,
builderOptions: {
"productName": "ODriveGUI",
"asar": false,
"extraResources": "server",
"artifactName": "${name}.${ext}",
"win" : {
"target" : [
{
"target": "portable",
}
]
},
"linux" : {
"target" : [
{
"target": "AppImage",
}
]
}
},
//mainProcessArgs: ['C:/Users/pajoh/Desktop/ODrive_work/ODrive/tools', 'C:/Users/pajoh/Desktop/ODrive_work/ODrive/Firmware']
}
}
}