*
@@ -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
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2020 ODrive Robotics
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,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/).
|
||||
@@ -0,0 +1,5 @@
|
||||
module.exports = {
|
||||
presets: [
|
||||
'@vue/cli-plugin-babel/preset'
|
||||
]
|
||||
}
|
||||
@@ -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):
|
||||
|
||||

|
||||
|
||||
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).
|
||||
|
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>
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
|
After Width: | Height: | Size: 4.2 KiB |
|
After Width: | Height: | Size: 12 KiB |
@@ -0,0 +1,17 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
|
||||
<title><%= htmlWebpackPlugin.options.title %></title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>
|
||||
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
|
||||
</noscript>
|
||||
<div id="app"></div>
|
||||
<!-- built files will be auto injected -->
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,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)
|
||||
@@ -0,0 +1,571 @@
|
||||
<template>
|
||||
<div id="app">
|
||||
<!-- HEADER -->
|
||||
<div class="header" id="header">
|
||||
<button class="dash-button menu-button" @click="toggleMenu">Menu</button>
|
||||
<div class="card menu" id="menu" :style="{top: menuTop}" v-show="showMenu">
|
||||
<button class="dash-button menu-item" @click="exportDash">Export dash</button>
|
||||
<button class="dash-button menu-item" @click="importDashWrapper">
|
||||
Import dash
|
||||
<input
|
||||
type="file"
|
||||
id="inputDash"
|
||||
ref="fileInput"
|
||||
@change="
|
||||
importDashFile($event.target.files);
|
||||
$refs.fileInput.value = null;
|
||||
"
|
||||
value
|
||||
style="display: none"
|
||||
/>
|
||||
</button>
|
||||
<button class="dash-button menu-item" id="importButton" @click="calcImportLeft();calcImportTop();toggleImport()">Import ODrive config</button>
|
||||
<button class="dash-button menu-item" id="exportButton" @click="calcExportLeft();calcExportTop();toggleExport()">Export ODrive config</button>
|
||||
</div>
|
||||
<div class="card import-menu" :style="{top: importTop, left: importLeft}" v-show="showImport">
|
||||
<button v-for="odrive in Object.keys(odrives)" :key="odrive" class="dash-button" @click="importConfigWrapper">
|
||||
{{odrive}}
|
||||
<input
|
||||
type="file"
|
||||
id="inputConfig"
|
||||
ref="fileInputConfig"
|
||||
@change="
|
||||
importConfigFile($event.target, odrive);
|
||||
"
|
||||
onclick="this.value=null"
|
||||
value
|
||||
style="display: none"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<div class="card export-menu" :style="{top: exportTop, left: exportLeft}" v-show="showExport">
|
||||
<button v-for="odrive in Object.keys(odrives)" :key="odrive" class="dash-button" @click="exportConfig(odrive)">{{odrive}}</button>
|
||||
</div>
|
||||
<button
|
||||
v-for="dash in dashboards"
|
||||
:key="dash.id"
|
||||
:class="['dash-button', { active: currentDash === dash.name }]"
|
||||
v-on:click.self="changeDash(dash.name)"
|
||||
v-on:dblclick="changeDashName(dash.id)"
|
||||
>
|
||||
<button
|
||||
v-if="
|
||||
dash.name !== 'Start' &&
|
||||
dash.name !== 'Tuning' &&
|
||||
dash.name !== 'Wizard'
|
||||
"
|
||||
class="close-button"
|
||||
v-on:click="deleteDash(dash.id)"
|
||||
>
|
||||
X
|
||||
</button>
|
||||
{{ dash.name }}
|
||||
</button>
|
||||
<button class="dash-button dash-add" @click="addDash">+</button>
|
||||
<button
|
||||
class="dash-button sample-button"
|
||||
:class="[{ active: sampling === true }]"
|
||||
@click="sampleButton"
|
||||
>
|
||||
{{ samplingText }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- PAGE CONTENT -->
|
||||
<component
|
||||
:is="currentDashName"
|
||||
:odrives="odrives"
|
||||
:dash="dash"
|
||||
></component>
|
||||
|
||||
<!-- FOOTER -->
|
||||
<div class="footer">
|
||||
<Axis
|
||||
v-for="axis in axes"
|
||||
:key="axis.name"
|
||||
:axis="axis.name"
|
||||
:odrives="odrives"
|
||||
></Axis>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Start from "./views/Start.vue";
|
||||
import Dashboard from "./views/Dashboard.vue";
|
||||
import Axis from "./components/Axis.vue";
|
||||
import Wizard from "./views/Wizard.vue";
|
||||
import * as socketio from "./comms/socketio";
|
||||
import { saveAs } from "file-saver";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { pathsFromTree } from "./lib/utils.js";
|
||||
import { getVal, putVal } from "./lib/odrive_utils.js";
|
||||
|
||||
export default {
|
||||
name: "App",
|
||||
components: {
|
||||
Start,
|
||||
Dashboard,
|
||||
Axis,
|
||||
Wizard,
|
||||
},
|
||||
data: function () {
|
||||
return {
|
||||
//currentDash: "Start",
|
||||
showMenu: false,
|
||||
showImport: false,
|
||||
showExport: false,
|
||||
menuTop: '0px',
|
||||
importTop: '0px',
|
||||
importLeft: '0px',
|
||||
exportTop: '0px',
|
||||
exportLeft: '0px',
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
var elem = document.getElementById('header');
|
||||
this.menuTop = elem.offsetHeight + 'px';
|
||||
},
|
||||
computed: {
|
||||
currentDashName: function () {
|
||||
//get the appropriate component name from the currentDash variable
|
||||
let comp = {};
|
||||
for (const dash of this.dashboards) {
|
||||
if (dash.name === this.$store.state.currentDash) {
|
||||
comp = dash.component;
|
||||
}
|
||||
}
|
||||
return comp;
|
||||
},
|
||||
dash: function () {
|
||||
let comp = {};
|
||||
for (const dash of this.dashboards) {
|
||||
if (dash.name === this.$store.state.currentDash) {
|
||||
comp = dash;
|
||||
}
|
||||
}
|
||||
return comp;
|
||||
},
|
||||
currentCtrlList: function () {
|
||||
let comp = {};
|
||||
for (const dash of this.dashboards) {
|
||||
if (dash.name === this.$store.state.currentDash) {
|
||||
comp = dash.controls;
|
||||
}
|
||||
}
|
||||
return comp;
|
||||
},
|
||||
axes: function () {
|
||||
return this.$store.state.axes;
|
||||
},
|
||||
odrives: function () {
|
||||
return this.$store.state.odrives;
|
||||
},
|
||||
dashboards: function () {
|
||||
return this.$store.state.dashboards;
|
||||
},
|
||||
sampling: function () {
|
||||
return this.$store.state.sampling;
|
||||
},
|
||||
currentDash: function () {
|
||||
return this.$store.state.currentDash;
|
||||
},
|
||||
samplingText: function () {
|
||||
let ret;
|
||||
if (this.$store.state.sampling) {
|
||||
ret = "stop sampling";
|
||||
} else {
|
||||
ret = "start sampling";
|
||||
}
|
||||
return ret;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
toggleMenu() {
|
||||
this.showMenu = !this.showMenu;
|
||||
if (!this.showMenu) {
|
||||
this.showImport = false;
|
||||
this.showExport = false;
|
||||
}
|
||||
},
|
||||
toggleImport() {
|
||||
this.showImport = !this.showImport;
|
||||
this.showExport = false;
|
||||
},
|
||||
toggleExport() {
|
||||
this.showExport = !this.showExport;
|
||||
this.showImport= false;
|
||||
},
|
||||
importConfigWrapper() {
|
||||
const inputElem = document.getElementById("inputConfig");
|
||||
if (inputElem) {
|
||||
console.log("importing config");
|
||||
inputElem.click();
|
||||
}
|
||||
},
|
||||
importConfigFile(event, odrive) {
|
||||
console.log("Importing config to odrive " + odrive);
|
||||
let file = event.files[0];
|
||||
const reader = new FileReader();
|
||||
// this is ugly, but it gets around scoping problems the "load" callback
|
||||
let importConfig = (config) => {
|
||||
let paths = pathsFromTree(config);
|
||||
for (const path of paths) {
|
||||
let val = config;
|
||||
for(const key of path.split('.')) {
|
||||
val = val[key];
|
||||
}
|
||||
putVal(odrive + '.' + path, val);
|
||||
}
|
||||
console.log("Config imported to odrive " + odrive);
|
||||
};
|
||||
reader.addEventListener("load", function (e) {
|
||||
let importString = e.target.result;
|
||||
importString = importString.replace(/ Infinity/g, '" Infinity"');
|
||||
importString = importString.replace(/ -Infinity/g, '" -Infinity"');
|
||||
importConfig(JSON.parse(importString));
|
||||
});
|
||||
reader.readAsText(file);
|
||||
},
|
||||
exportConfig(odrive) {
|
||||
console.log("Exporting config from odrive " + odrive);
|
||||
let configPaths = [];
|
||||
let exportConfig = {};
|
||||
|
||||
let paramTree = this.$store.state.odriveConfigs.params[odrive];
|
||||
for (const path of pathsFromTree(paramTree)) {
|
||||
if (path.includes('config.') && !path.includes('mapping')) {
|
||||
configPaths.push(path);
|
||||
}
|
||||
}
|
||||
for (const path of configPaths) {
|
||||
let keys = path.split('.');
|
||||
let val = String(getVal(odrive + '.' + path));
|
||||
// super ugly! Need a better way to do this and handle tuples in odrive_utils
|
||||
if (val.includes('True')) {
|
||||
val = true;
|
||||
}
|
||||
else if (val.includes('False')) {
|
||||
val = false;
|
||||
}
|
||||
else if (val.includes('.')) {
|
||||
val = parseFloat(val);
|
||||
}
|
||||
else if (!val.includes(',') && !val.includes('Infinity')){
|
||||
val = parseInt(val);
|
||||
}
|
||||
let obj = exportConfig;
|
||||
for (const key of keys.slice(0,-1)) {
|
||||
if (!Object.keys(obj).includes(key)) {
|
||||
obj[key] = {}
|
||||
}
|
||||
obj = obj[key];
|
||||
}
|
||||
obj[keys.pop()] = val;
|
||||
}
|
||||
let exportString = JSON.stringify(exportConfig, null, 2);
|
||||
if (exportString.includes('"Infinity"')) {
|
||||
console.log("found infinity!");
|
||||
}
|
||||
console.log(typeof exportString);
|
||||
exportString = exportString.replace(/"Infinity"/g, 'Infinity');
|
||||
exportString = exportString.replace(/"-Infinity"/g, '-Infinity');
|
||||
const blob = new Blob([exportString], {
|
||||
type: "application/json",
|
||||
});
|
||||
saveAs(blob, odrive + '-config.json');
|
||||
},
|
||||
calcImportLeft() {
|
||||
let elem = document.getElementById('menu');
|
||||
this.importLeft = elem.offsetWidth + 'px';
|
||||
},
|
||||
calcImportTop() {
|
||||
this.importTop = document.getElementById('importButton').getBoundingClientRect().top + 'px';
|
||||
},
|
||||
calcExportLeft() {
|
||||
let elem = document.getElementById('menu')
|
||||
this.exportLeft = elem.offsetWidth + 'px';
|
||||
},
|
||||
calcExportTop() {
|
||||
this.exportTop = document.getElementById('exportButton').getBoundingClientRect().top + 'px';
|
||||
},
|
||||
changeDash(dashName) {
|
||||
console.log(dashName);
|
||||
this.$store.commit("setDash", dashName);
|
||||
},
|
||||
addDash() {
|
||||
let dashname = "Dashboard " + (this.dashboards.length - 2);
|
||||
this.dashboards.push({
|
||||
component: "Dashboard",
|
||||
name: dashname,
|
||||
id: uuidv4(),
|
||||
controls: [],
|
||||
actions: [],
|
||||
plots: [],
|
||||
});
|
||||
},
|
||||
deleteDash(dashID) {
|
||||
this.$store.commit("setDash", "Start");
|
||||
console.log("Deleting dash " + dashID);
|
||||
for (const dash of this.dashboards) {
|
||||
if (dashID === dash.id) {
|
||||
this.dashboards.splice(this.dashboards.indexOf(dash), 1);
|
||||
}
|
||||
}
|
||||
},
|
||||
exportDash() {
|
||||
console.log("exporting dashboard");
|
||||
const blob = new Blob([JSON.stringify(this.dash, null, 2)], {
|
||||
type: "application/json",
|
||||
});
|
||||
saveAs(blob, this.dash.name);
|
||||
},
|
||||
importDashWrapper() {
|
||||
const inputElem = document.getElementById("inputDash");
|
||||
if (inputElem) {
|
||||
console.log("importing dashboard");
|
||||
inputElem.click();
|
||||
}
|
||||
},
|
||||
importDashFile(files) {
|
||||
console.log("file handler callback");
|
||||
let file = files[0];
|
||||
const reader = new FileReader();
|
||||
// this is ugly, but it gets around scoping problems the "load" callback
|
||||
let dashes = this.dashboards;
|
||||
let addImportedDash = (dash) => {
|
||||
console.log(dash);
|
||||
dashes.push(dash);
|
||||
// plots will have variables associated, add them to sampled variables list
|
||||
for (const plot of dash.plots) {
|
||||
console.log(plot);
|
||||
for (const plotVar of plot.vars) {
|
||||
console.log(plotVar);
|
||||
//addsampledprop(path);
|
||||
this.$store.commit("addSampledProperty", plotVar.path);
|
||||
}
|
||||
}
|
||||
};
|
||||
reader.addEventListener("load", function (e) {
|
||||
addImportedDash(JSON.parse(e.target.result));
|
||||
});
|
||||
reader.readAsText(file);
|
||||
},
|
||||
changeDashName(e) {
|
||||
console.log(e);
|
||||
console.log("double clicked dashboard name");
|
||||
},
|
||||
sampleButton() {
|
||||
if (this.$store.state.sampling) {
|
||||
// sampling acttive, stop
|
||||
socketio.sendEvent({
|
||||
type: "stopSampling",
|
||||
});
|
||||
this.$store.state.sampling = false;
|
||||
} else {
|
||||
// sampling inactive, start sampling
|
||||
socketio.sendEvent({
|
||||
type: "sampledVarNames",
|
||||
data: {
|
||||
paths: this.$store.state.sampledProperties,
|
||||
},
|
||||
});
|
||||
socketio.sendEvent({
|
||||
type: "enableSampling",
|
||||
});
|
||||
this.$store.state.timeSampleStart = Date.now();
|
||||
this.$store.state.sampling = true;
|
||||
}
|
||||
},
|
||||
startsample() {
|
||||
socketio.sendEvent({
|
||||
type: "sampledVarNames",
|
||||
data: {
|
||||
paths: this.$store.state.sampledProperties,
|
||||
},
|
||||
});
|
||||
socketio.sendEvent({
|
||||
type: "enableSampling",
|
||||
});
|
||||
this.$store.state.timeSampleStart = Date.now();
|
||||
this.$store.state.sampling = true;
|
||||
},
|
||||
stopsample() {
|
||||
socketio.sendEvent({
|
||||
type: "stopSampling",
|
||||
});
|
||||
this.$store.state.sampling = false;
|
||||
},
|
||||
estop() {
|
||||
// send stop command to odrives
|
||||
// behavior on reset?
|
||||
},
|
||||
emitFindODrives() {
|
||||
socketio.sendEvent({
|
||||
type: "findODrives",
|
||||
data: {},
|
||||
});
|
||||
},
|
||||
},
|
||||
created() {
|
||||
// on app creation, set the address to the default
|
||||
this.$store.dispatch("setServerAddress", "http://127.0.0.1:5000");
|
||||
|
||||
// to allow running as web app
|
||||
if (window.ipcRenderer != undefined) {
|
||||
window.ipcRenderer.on("server-stdout", (event, arg) => {
|
||||
this.$store.commit("logServerMessage", arg);
|
||||
});
|
||||
window.ipcRenderer.on("server-stderr", (event, arg) => {
|
||||
this.$store.commit("logServerMessage", arg);
|
||||
});
|
||||
window.ipcRenderer.send("start-server");
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@import "./assets/styles/vars.css";
|
||||
@import "./assets/styles/style.css";
|
||||
|
||||
* {
|
||||
/* font-family: Arial, Helvetica, sans-serif; */
|
||||
font-family: "Roboto", sans-serif;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
#app {
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.header {
|
||||
/* want this fixed and full width */
|
||||
position: fixed;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
width: 100vw;
|
||||
display: flex;
|
||||
background-color: var(--fg-color);
|
||||
box-shadow: 0 0px 8px 0 rgba(0, 0, 0, 0.4);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
button {
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
color: black;
|
||||
text-decoration: none;
|
||||
padding: 10px;
|
||||
background-color: var(--fg-color);
|
||||
border-style: none;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.dash-button {
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
color: black;
|
||||
text-decoration: none;
|
||||
padding: 10px;
|
||||
background-color: var(--fg-color);
|
||||
border-style: none;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.dash-button:active {
|
||||
background-color: var(--bg-color);
|
||||
}
|
||||
|
||||
.dash-button:hover {
|
||||
background-color: var(--bg-color);
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.active {
|
||||
color: #000000;
|
||||
background-color: var(--bg-color);
|
||||
}
|
||||
|
||||
.footer {
|
||||
position: fixed;
|
||||
width: 100vw;
|
||||
left: 0px;
|
||||
bottom: 0px;
|
||||
display: flex;
|
||||
background-color: var(--fg-color);
|
||||
box-shadow: 0 0px 8px 0 rgba(0, 0, 0, 0.4);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.odrvSer,
|
||||
.errorState {
|
||||
font-weight: bold;
|
||||
font-family: "Roboto Mono", monospace;
|
||||
margin: auto 5px;
|
||||
background-color: var(--fg-color);
|
||||
}
|
||||
|
||||
.errorState {
|
||||
color: #13a100;
|
||||
}
|
||||
|
||||
.dash-add {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.emergency-stop {
|
||||
margin-left: auto;
|
||||
padding-left: 2rem;
|
||||
padding-right: 2rem;
|
||||
background-color: red;
|
||||
font-weight: bold;
|
||||
color: white;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.sample-button {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.odrive-status {
|
||||
margin-left: auto;
|
||||
font-family: "Roboto Mono", monospace;
|
||||
padding: 5px 10px;
|
||||
}
|
||||
|
||||
.menu-button {
|
||||
border-right: 1px solid grey;
|
||||
}
|
||||
|
||||
.menu {
|
||||
position: absolute;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
left: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.import-menu {
|
||||
position: absolute;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.export-menu {
|
||||
position: absolute;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,127 @@
|
||||
{
|
||||
"name": "Tuning",
|
||||
"component": "Dashboard",
|
||||
"id": "deadb33f",
|
||||
"controls": [
|
||||
{
|
||||
"controlType": "CtrlNumeric",
|
||||
"path": "odrives.odrive0.axis0.motor.config.current_lim"
|
||||
},
|
||||
{
|
||||
"controlType": "CtrlNumeric",
|
||||
"path": "odrives.odrive0.axis0.controller.config.vel_limit"
|
||||
},
|
||||
{
|
||||
"controlType": "CtrlSlider",
|
||||
"path": "odrives.odrive0.axis0.controller.config.pos_gain"
|
||||
},
|
||||
{
|
||||
"controlType": "CtrlSlider",
|
||||
"path": "odrives.odrive0.axis0.controller.config.vel_gain"
|
||||
},
|
||||
{
|
||||
"controlType": "CtrlSlider",
|
||||
"path": "odrives.odrive0.axis0.controller.config.vel_integrator_gain"
|
||||
},
|
||||
{
|
||||
"controlType": "CtrlEnum",
|
||||
"path": "odrives.odrive0.axis0.requested_state",
|
||||
"options": [
|
||||
{
|
||||
"text": "Undefined",
|
||||
"value": 0
|
||||
},
|
||||
{
|
||||
"text": "Idle",
|
||||
"value": 1
|
||||
},
|
||||
{
|
||||
"text": "Startup Sequence",
|
||||
"value": 2
|
||||
},
|
||||
{
|
||||
"text": "Full Calibration Sequence",
|
||||
"value": 3
|
||||
},
|
||||
{
|
||||
"text": "Motor Calibration",
|
||||
"value": 4
|
||||
},
|
||||
{
|
||||
"text": "Sensorless Control",
|
||||
"value": 5
|
||||
},
|
||||
{
|
||||
"text": "Encoder Index Search",
|
||||
"value": 6
|
||||
},
|
||||
{
|
||||
"text": "Encoder Offset Calibration",
|
||||
"value": 7
|
||||
},
|
||||
{
|
||||
"text": "Closed Loop Control",
|
||||
"value": 8
|
||||
},
|
||||
{
|
||||
"text": "Lockin Spin",
|
||||
"value": 9
|
||||
},
|
||||
{
|
||||
"text": "Encoder Direction Find",
|
||||
"value": 10
|
||||
},
|
||||
{
|
||||
"text": "Homing",
|
||||
"value": 11
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"controlType": "CtrlFunction",
|
||||
"path": "odrives.odrive0.save_configuration"
|
||||
}
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"actionType": "Action",
|
||||
"id": "a699c462-cf8d-4b4e-aedf-0b200e84f97a",
|
||||
"path": "odrives.odrive0.axis0.controller.pos_setpoint",
|
||||
"val": 0
|
||||
},
|
||||
{
|
||||
"actionType": "Action",
|
||||
"id": "db267ae4-fdf4-4770-a2d3-eb61de026659",
|
||||
"path": "odrives.odrive0.axis0.controller.pos_setpoint",
|
||||
"val": 50000
|
||||
}
|
||||
],
|
||||
"plots": [
|
||||
{
|
||||
"name": "65a2768d-e61e-4dd5-bf80-dba153838f74",
|
||||
"vars": [
|
||||
{
|
||||
"path": "odrives.odrive0.axis0.controller.pos_setpoint",
|
||||
"color": "#1f77b4"
|
||||
},
|
||||
{
|
||||
"path": "odrives.odrive0.axis0.encoder.pos_estimate",
|
||||
"color": "#ff7f0e"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "d9ee2474-3e53-408f-9eab-1656342eb531",
|
||||
"vars": [
|
||||
{
|
||||
"path": "odrives.odrive0.axis0.controller.vel_setpoint",
|
||||
"color": "#1f77b4"
|
||||
},
|
||||
{
|
||||
"path": "odrives.odrive0.axis0.encoder.vel_estimate",
|
||||
"color": "#ff7f0e"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
{
|
||||
"name": "Tuning",
|
||||
"component": "Dashboard",
|
||||
"id": "deadb33f",
|
||||
"controls": [
|
||||
{
|
||||
"controlType": "CtrlNumeric",
|
||||
"path": "odrives.odrive0.axis0.motor.config.current_lim"
|
||||
},
|
||||
{
|
||||
"controlType": "CtrlNumeric",
|
||||
"path": "odrives.odrive0.axis0.controller.config.vel_limit"
|
||||
},
|
||||
{
|
||||
"controlType": "CtrlSlider",
|
||||
"path": "odrives.odrive0.axis0.controller.config.pos_gain"
|
||||
},
|
||||
{
|
||||
"controlType": "CtrlSlider",
|
||||
"path": "odrives.odrive0.axis0.controller.config.vel_gain"
|
||||
},
|
||||
{
|
||||
"controlType": "CtrlSlider",
|
||||
"path": "odrives.odrive0.axis0.controller.config.vel_integrator_gain"
|
||||
},
|
||||
{
|
||||
"controlType": "CtrlEnum",
|
||||
"path": "odrives.odrive0.axis0.requested_state",
|
||||
"options": [
|
||||
{
|
||||
"text": "Undefined",
|
||||
"value": 0
|
||||
},
|
||||
{
|
||||
"text": "Idle",
|
||||
"value": 1
|
||||
},
|
||||
{
|
||||
"text": "Startup Sequence",
|
||||
"value": 2
|
||||
},
|
||||
{
|
||||
"text": "Full Calibration Sequence",
|
||||
"value": 3
|
||||
},
|
||||
{
|
||||
"text": "Motor Calibration",
|
||||
"value": 4
|
||||
},
|
||||
{
|
||||
"text": "Sensorless Control",
|
||||
"value": 5
|
||||
},
|
||||
{
|
||||
"text": "Encoder Index Search",
|
||||
"value": 6
|
||||
},
|
||||
{
|
||||
"text": "Encoder Offset Calibration",
|
||||
"value": 7
|
||||
},
|
||||
{
|
||||
"text": "Closed Loop Control",
|
||||
"value": 8
|
||||
},
|
||||
{
|
||||
"text": "Lockin Spin",
|
||||
"value": 9
|
||||
},
|
||||
{
|
||||
"text": "Encoder Direction Find",
|
||||
"value": 10
|
||||
},
|
||||
{
|
||||
"text": "Homing",
|
||||
"value": 11
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"controlType": "CtrlFunction",
|
||||
"path": "odrives.odrive0.save_configuration"
|
||||
}
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"actionType": "Action",
|
||||
"id": "a699c462-cf8d-4b4e-aedf-0b200e84f97a",
|
||||
"path": "odrives.odrive0.axis0.controller.input_pos",
|
||||
"val": 0
|
||||
},
|
||||
{
|
||||
"actionType": "Action",
|
||||
"id": "db267ae4-fdf4-4770-a2d3-eb61de026659",
|
||||
"path": "odrives.odrive0.axis0.controller.input_pos",
|
||||
"val": 10
|
||||
}
|
||||
],
|
||||
"plots": [
|
||||
{
|
||||
"name": "65a2768d-e61e-4dd5-bf80-dba153838f74",
|
||||
"vars": [
|
||||
{
|
||||
"path": "odrives.odrive0.axis0.controller.input_pos",
|
||||
"color": "#1f77b4"
|
||||
},
|
||||
{
|
||||
"path": "odrives.odrive0.axis0.encoder.pos_estimate",
|
||||
"color": "#ff7f0e"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "d9ee2474-3e53-408f-9eab-1656342eb531",
|
||||
"vars": [
|
||||
{
|
||||
"path": "odrives.odrive0.axis0.controller.vel_setpoint",
|
||||
"color": "#1f77b4"
|
||||
},
|
||||
{
|
||||
"path": "odrives.odrive0.axis0.encoder.vel_estimate",
|
||||
"color": "#ff7f0e"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
After Width: | Height: | Size: 169 KiB |
|
After Width: | Height: | Size: 171 KiB |
|
After Width: | Height: | Size: 109 KiB |
|
After Width: | Height: | Size: 122 KiB |
|
After Width: | Height: | Size: 130 KiB |
|
After Width: | Height: | Size: 166 KiB |
|
After Width: | Height: | Size: 165 KiB |
|
After Width: | Height: | Size: 112 KiB |
|
After Width: | Height: | Size: 46 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
@@ -0,0 +1,700 @@
|
||||
---
|
||||
version: 0.0.1
|
||||
ns: com.odriverobotics
|
||||
summary: ODrive Interface Definitions
|
||||
|
||||
dictionary: [ODrive] # Prevent the word 'ODrive' from being detected as two words 'O' and 'Drive'
|
||||
|
||||
interfaces:
|
||||
ODrive:
|
||||
c_is_class: True
|
||||
attributes:
|
||||
vbus_voltage: readonly float32
|
||||
ibus: readonly float32
|
||||
serial_number: readonly uint64
|
||||
hw_version_major: readonly uint8
|
||||
hw_version_minor: readonly uint8
|
||||
hw_version_variant: readonly uint8
|
||||
fw_version_major: readonly uint8
|
||||
fw_version_minor: readonly uint8
|
||||
fw_version_revision: readonly uint8
|
||||
fw_version_unreleased:
|
||||
type: readonly uint8
|
||||
doc: 0 for official releases, 1 otherwise
|
||||
brake_resistor_armed: readonly bool
|
||||
brake_resistor_saturated: bool
|
||||
system_stats:
|
||||
c_is_class: False
|
||||
attributes:
|
||||
uptime: readonly uint32
|
||||
min_heap_space: readonly uint32
|
||||
min_stack_space_axis0: readonly uint32
|
||||
min_stack_space_axis1: readonly uint32
|
||||
min_stack_space_comms: readonly uint32
|
||||
min_stack_space_usb: readonly uint32
|
||||
min_stack_space_uart: readonly uint32
|
||||
min_stack_space_can: readonly uint32
|
||||
min_stack_space_usb_irq: readonly uint32
|
||||
min_stack_space_startup: readonly uint32
|
||||
stack_usage_axis0: readonly uint32
|
||||
stack_usage_axis1: readonly uint32
|
||||
stack_usage_comms: readonly uint32
|
||||
stack_usage_usb: readonly uint32
|
||||
stack_usage_uart: readonly uint32
|
||||
stack_usage_usb_irq: readonly uint32
|
||||
stack_usage_startup: readonly uint32
|
||||
stack_usage_can: readonly uint32
|
||||
usb:
|
||||
c_is_class: False
|
||||
attributes:
|
||||
rx_cnt: readonly uint32
|
||||
tx_cnt: readonly uint32
|
||||
tx_overrun_cnt: readonly uint32
|
||||
i2c:
|
||||
c_is_class: False
|
||||
attributes:
|
||||
addr: readonly uint8
|
||||
addr_match_cnt: readonly uint32
|
||||
rx_cnt: readonly uint32
|
||||
error_cnt: readonly uint32
|
||||
config:
|
||||
c_is_class: False
|
||||
attributes:
|
||||
enable_uart:
|
||||
type: bool
|
||||
doc: 'TODO: changing this currently requires a reboot - fix this'
|
||||
uart_baudrate:
|
||||
type: uint32
|
||||
doc: "Defines the baudrate used on the UART interface.
|
||||
Some baudrates will have a small timing error due to hardware limitations.
|
||||
|
||||
Here's an (incomplete) list of baudrates for ODrive v3.x:
|
||||
|
||||
Configured | Actual | Error [%]
|
||||
-------------|---------------|-----------
|
||||
1.2 KBps | 1.2 KBps | 0
|
||||
2.4 KBps | 2.4 KBps | 0
|
||||
9.6 KBps | 9.6 KBps | 0
|
||||
19.2 KBps | 19.195 KBps | 0.02
|
||||
38.4 KBps | 38.391 KBps | 0.02
|
||||
57.6 KBps | 57.613 KBps | 0.02
|
||||
115.2 KBps | 115.068 KBps | 0.11
|
||||
230.4 KBps | 230.769 KBps | 0.16
|
||||
460.8 KBps | 461.538 KBps | 0.16
|
||||
921.6 KBps | 913.043 KBps | 0.93
|
||||
1.792 MBps | 1.826 MBps | 1.9
|
||||
1.8432 MBps | 1.826 MBps | 0.93
|
||||
|
||||
For more information refer to Section 30.3.4 and Table 142 (the column with f_PCLK = 42 MHz) in the STM datasheet:
|
||||
https://www.st.com/content/ccc/resource/technical/document/reference_manual/3d/6d/5a/66/b4/99/40/d4/DM00031020.pdf/files/DM00031020.pdf/jcr:content/translations/en.DM00031020.pdf"
|
||||
enable_i2c_instead_of_can:
|
||||
type: bool
|
||||
doc: 'Changing this requires a reboot'
|
||||
enable_ascii_protocol_on_usb: bool
|
||||
max_regen_current: float32
|
||||
brake_resistance:
|
||||
type: float32
|
||||
unit: Ohm
|
||||
doc: Value of the brake resistor connected to the ODrive. Set to 0 to disable.
|
||||
|
||||
dc_bus_undervoltage_trip_level:
|
||||
type: float32
|
||||
unit: V
|
||||
doc: Minimum voltage below which the motor stops operating.
|
||||
dc_bus_overvoltage_trip_level:
|
||||
type: float32
|
||||
unit: V
|
||||
doc: Maximum voltage above which the motor stops operating.
|
||||
This protects against cases in which the power supply fails to dissipate
|
||||
the brake power if the brake resistor is disabled.
|
||||
The default is 26V for the 24V board version and 52V for the 48V board version.
|
||||
|
||||
enable_dc_bus_overvoltage_ramp:
|
||||
type: bool
|
||||
doc: 'If enabled, if the measured DC voltage exceeds `dc_bus_overvoltage_ramp_start`,
|
||||
the ODrive will sink more power than usual into the the brake resistor
|
||||
in an attempt to bring the voltage down again.
|
||||
|
||||
The brake duty cycle is increased by the following amount:
|
||||
vbus_voltage == dc_bus_overvoltage_ramp_start => brake_duty_cycle += 0%
|
||||
vbus_voltage == dc_bus_overvoltage_ramp_end => brake_duty_cycle += 100%
|
||||
|
||||
Remarks:
|
||||
- This feature is active even when all motors are disarmed.
|
||||
- This feature is disabled if `brake_resistance` is non-positive.'
|
||||
dc_bus_overvoltage_ramp_start:
|
||||
type: float32
|
||||
doc: See `enable_dc_bus_overvoltage_ramp`.
|
||||
Do not set this lower than your usual vbus_voltage,
|
||||
unless you like fried brake resistors.
|
||||
dc_bus_overvoltage_ramp_end:
|
||||
type: float32
|
||||
doc: See `enable_dc_bus_overvoltage_ramp`.
|
||||
Must be larger than `dc_bus_overvoltage_ramp_start`,
|
||||
otherwise the ramp feature is disabled.
|
||||
|
||||
dc_max_positive_current:
|
||||
type: float32
|
||||
unit: A
|
||||
doc: Max current the power supply can source.
|
||||
dc_max_negative_current:
|
||||
type: float32
|
||||
unit: A
|
||||
doc: Max current the power supply can sink. You most likely want a non-positive value here. Set to -INFINITY to disable.
|
||||
|
||||
gpio1_pwm_mapping: {type: Endpoint, c_name: 'pwm_mappings[0]'} # TODO: disable for ODrive v3.2 and older
|
||||
gpio2_pwm_mapping: {type: Endpoint, c_name: 'pwm_mappings[1]'} # TODO: disable for ODrive v3.2 and older
|
||||
gpio3_pwm_mapping: {type: Endpoint, c_name: 'pwm_mappings[2]'} # TODO: disable for ODrive v3.2 and older
|
||||
gpio4_pwm_mapping: {type: Endpoint, c_name: 'pwm_mappings[3]'}
|
||||
gpio3_analog_mapping: {type: Endpoint, c_name: 'analog_mappings[2]'}
|
||||
gpio4_analog_mapping: {type: Endpoint, c_name: 'analog_mappings[3]'}
|
||||
user_config_loaded: readonly bool
|
||||
|
||||
axis0: {type: Axis, c_name: get_axis(0)}
|
||||
axis1: {type: Axis, c_name: get_axis(1)}
|
||||
can: {type: Can, c_name: get_can()}
|
||||
test_property: uint32
|
||||
|
||||
functions:
|
||||
test_function: {in: {delta: int32}, out: {cnt: int32}}
|
||||
get_oscilloscope_val: {in: {index: uint32}, out: {val: float32}}
|
||||
get_adc_voltage: {in: {gpio: uint32}, out: {voltage: float32}}
|
||||
save_configuration:
|
||||
erase_configuration:
|
||||
reboot:
|
||||
enter_dfu_mode:
|
||||
|
||||
ODrive.Can:
|
||||
c_is_class: True
|
||||
attributes:
|
||||
error:
|
||||
nullflag: None
|
||||
flags: {DuplicateCanIds: }
|
||||
config:
|
||||
c_is_class: False
|
||||
attributes:
|
||||
baud_rate: readonly uint32
|
||||
protocol: Protocol
|
||||
functions:
|
||||
set_baud_rate: {in: {baudRate: uint32}}
|
||||
|
||||
ODrive.Endpoint:
|
||||
c_is_class: False
|
||||
attributes:
|
||||
endpoint: endpoint_ref
|
||||
min: float32
|
||||
max: float32
|
||||
|
||||
ODrive.Axis:
|
||||
c_is_class: True
|
||||
attributes:
|
||||
error:
|
||||
nullflag: 'None'
|
||||
flags:
|
||||
InvalidState:
|
||||
doc: An invalid state was requested.
|
||||
DcBusUnderVoltage:
|
||||
DcBusOverVoltage:
|
||||
CurrentMeasurementTimeout:
|
||||
BrakeResistorDisarmed:
|
||||
doc: The brake resistor was unexpectedly disarmed.
|
||||
MotorDisarmed:
|
||||
doc: The motor was unexpectedly disarmed.
|
||||
MotorFailed:
|
||||
doc: Check `motor.error` for more information.
|
||||
SensorlessEstimatorFailed:
|
||||
EncoderFailed:
|
||||
doc: Check `encoder.error` for more information.
|
||||
ControllerFailed:
|
||||
PosCtrlDuringSensorless:
|
||||
doc: DEPRECATED
|
||||
WatchdogTimerExpired:
|
||||
MinEndstopPressed:
|
||||
MaxEndstopPressed:
|
||||
EstopRequested:
|
||||
HomingWithoutEndstop:
|
||||
bit: 17
|
||||
doc: the min endstop was not enabled during homing
|
||||
step_dir_active: readonly bool
|
||||
current_state: readonly AxisState
|
||||
requested_state: AxisState
|
||||
loop_counter: readonly uint32
|
||||
lockin_state:
|
||||
typeargs: {fibre.Property.mode: readonly}
|
||||
values:
|
||||
Inactive:
|
||||
Ramp:
|
||||
Accelerate:
|
||||
ConstVel:
|
||||
is_homed: {type: bool, c_name: homing_.is_homed}
|
||||
config:
|
||||
c_is_class: False
|
||||
attributes:
|
||||
startup_motor_calibration:
|
||||
type: bool
|
||||
doc: run motor calibration at startup, skip otherwise
|
||||
startup_encoder_index_search:
|
||||
type: bool
|
||||
doc: run encoder index search after startup, skip otherwise this only has an effect if encoder.config.use_index is also true
|
||||
startup_encoder_offset_calibration:
|
||||
type: bool
|
||||
doc: run encoder offset calibration after startup, skip otherwise
|
||||
startup_closed_loop_control:
|
||||
type: bool
|
||||
doc: enable closed loop control after calibration/startup
|
||||
startup_sensorless_control:
|
||||
type: bool
|
||||
doc: enable sensorless control after calibration/startup
|
||||
startup_homing:
|
||||
type: bool
|
||||
doc: enable homing after calibration/startup
|
||||
enable_step_dir:
|
||||
type: bool
|
||||
doc: Enable step/dir input after calibration.
|
||||
For M0 this has no effect if `enable_uart` is true.
|
||||
step_dir_always_on:
|
||||
type: bool
|
||||
doc: Keep step/dir enabled while the motor is disabled.
|
||||
This is ignored if enable_step_dir is false.
|
||||
This setting only takes effect on a state transition
|
||||
into idle or out of closed loop control.
|
||||
counts_per_step: float32
|
||||
watchdog_timeout:
|
||||
type: float32
|
||||
unit: s
|
||||
doc: 0 disables watchdog
|
||||
enable_watchdog: bool
|
||||
step_gpio_pin: {type: uint16, c_setter: 'set_step_gpio_pin'}
|
||||
dir_gpio_pin: {type: uint16, c_setter: 'set_dir_gpio_pin'}
|
||||
calibration_lockin: # TODO: this is a subset of lockin state
|
||||
c_is_class: False
|
||||
attributes:
|
||||
current: float32
|
||||
ramp_time: float32
|
||||
ramp_distance: float32
|
||||
accel: float32
|
||||
vel: float32
|
||||
sensorless_ramp: LockinConfig
|
||||
general_lockin: LockinConfig
|
||||
can_node_id:
|
||||
type: uint32
|
||||
doc: Both axes will have the same id to start
|
||||
can_node_id_extended: bool
|
||||
can_heartbeat_rate_ms: uint32
|
||||
motor: Motor
|
||||
controller: Controller
|
||||
encoder: Encoder
|
||||
sensorless_estimator: SensorlessEstimator
|
||||
trap_traj: TrapezoidalTrajectory
|
||||
min_endstop: Endstop
|
||||
max_endstop: Endstop
|
||||
functions:
|
||||
watchdog_feed:
|
||||
doc: Feed the watchdog to prevent watchdog timeouts.
|
||||
clear_errors:
|
||||
doc: Check the watchdog timer for expiration. Also sets the watchdog error bit if expired.
|
||||
|
||||
ODrive.Axis.LockinConfig:
|
||||
c_is_class: False
|
||||
attributes:
|
||||
current:
|
||||
type: float32
|
||||
unit: A
|
||||
ramp_time:
|
||||
type: float32
|
||||
unit: s
|
||||
ramp_distance:
|
||||
type: float32
|
||||
unit: rad
|
||||
accel:
|
||||
type: float32
|
||||
unit: rad/s^2
|
||||
vel:
|
||||
type: float32
|
||||
unit: rad/s
|
||||
finish_distance:
|
||||
type: float32
|
||||
unit: rad
|
||||
finish_on_vel: bool
|
||||
finish_on_distance: bool
|
||||
finish_on_enc_idx: bool
|
||||
|
||||
|
||||
ODrive.Motor:
|
||||
c_is_class: True
|
||||
attributes:
|
||||
error:
|
||||
nullflag: None
|
||||
flags:
|
||||
PhaseResistanceOutOfRange:
|
||||
PhaseInductanceOutOfRange:
|
||||
AdcFailed:
|
||||
DrvFault:
|
||||
ControlDeadlineMissed:
|
||||
NotImplementedMotorType:
|
||||
BrakeCurrentOutOfRange:
|
||||
ModulationMagnitude:
|
||||
BrakeDeadtimeViolation:
|
||||
UnexpectedTimerCallback:
|
||||
CurrentSenseSaturation:
|
||||
InverterOverTemp:
|
||||
CurrentLimitViolation:
|
||||
BrakeDutyCycleNan:
|
||||
DcBusOverRegenCurrent: {doc: too much current pushed into the power supply}
|
||||
DcBusOverCurrent: {doc: too much current pulled out of the power supply}
|
||||
armed_state:
|
||||
typeargs: {fibre.Property.mode: readonly}
|
||||
values:
|
||||
Disarmed:
|
||||
WaitingForTimings:
|
||||
WaitingForUpdate:
|
||||
Armed:
|
||||
is_calibrated: readonly bool
|
||||
current_meas_phB: {type: readonly float32, c_name: current_meas_.phB}
|
||||
current_meas_phC: {type: readonly float32, c_name: current_meas_.phC}
|
||||
DC_calib_phB: {type: float32, c_name: DC_calib_.phB}
|
||||
DC_calib_phC: {type: float32, c_name: DC_calib_.phC}
|
||||
phase_current_rev_gain: float32
|
||||
thermal_current_lim: readonly float32
|
||||
inverter_temp:
|
||||
type: readonly float32
|
||||
unit: °C
|
||||
doc: NaN while the ODrive is initializing.
|
||||
current_control:
|
||||
c_is_class: False
|
||||
attributes:
|
||||
p_gain: float32
|
||||
i_gain: float32
|
||||
v_current_control_integral_d: float32
|
||||
v_current_control_integral_q: float32
|
||||
Ibus: float32
|
||||
final_v_alpha: float32
|
||||
final_v_beta: float32
|
||||
Id_setpoint: float32
|
||||
Iq_setpoint: readonly float32
|
||||
Iq_measured: float32
|
||||
Id_measured: float32
|
||||
I_measured_report_filter_k: float32
|
||||
max_allowed_current: readonly float32
|
||||
overcurrent_trip_level: readonly float32
|
||||
acim_rotor_flux: float32
|
||||
async_phase_vel: readonly float32
|
||||
async_phase_offset: float32
|
||||
gate_driver:
|
||||
c_name: gate_driver_exported_
|
||||
c_is_class: False
|
||||
attributes:
|
||||
drv_fault:
|
||||
typeargs: {fibre.Property.mode: readonly}
|
||||
nullflag: NoFault
|
||||
flags:
|
||||
FetLowCOvercurrent: {bit: 0, doc: FET Low side, Phase C Over Current fault}
|
||||
FetHighCOvercurrent: {bit: 1, doc: FET High side, Phase C Over Current fault}
|
||||
FetLowBOvercurrent: {bit: 2, doc: FET Low side, Phase B Over Current fault}
|
||||
FetHighBOvercurrent: {bit: 3, doc: FET High side, Phase B Over Current fault}
|
||||
FetLowAOvercurrent: {bit: 4, doc: FET Low side, Phase A Over Current fault}
|
||||
FetHighAOvercurrent: {bit: 5, doc: FET High side, Phase A Over Current fault}
|
||||
OvertemperatureWarning: {bit: 6, doc: Over Temperature Warning fault}
|
||||
OvertemperatureShutdown: {bit: 7, doc: Over Temperature Shut Down fault}
|
||||
PVddUndervoltage: {bit: 8, doc: Power supply Vdd Under Voltage fault}
|
||||
GVddUndervoltage: {bit: 9, doc: DRV8301 Vdd Under Voltage fault}
|
||||
GVddOvervoltage: {bit: 10, doc: DRV8301 Vdd Over Voltage fault}
|
||||
# status_reg_1: readonly uint32
|
||||
# status_reg_2: readonly uint32
|
||||
# ctrl_reg_1: readonly uint32
|
||||
# ctrl_reg_2: readonly uint32
|
||||
timing_log:
|
||||
c_is_class: False
|
||||
attributes:
|
||||
general: {type: readonly uint16, c_name: 'get(TIMING_LOG_GENERAL)'}
|
||||
adc_cb_i: {type: readonly uint16, c_name: 'get(TIMING_LOG_ADC_CB_I)'}
|
||||
adc_cb_dc: {type: readonly uint16, c_name: 'get(TIMING_LOG_ADC_CB_DC)'}
|
||||
meas_r: {type: readonly uint16, c_name: 'get(TIMING_LOG_MEAS_R)'}
|
||||
meas_l: {type: readonly uint16, c_name: 'get(TIMING_LOG_MEAS_L)'}
|
||||
enc_calib: {type: readonly uint16, c_name: 'get(TIMING_LOG_ENC_CALIB)'}
|
||||
idx_search: {type: readonly uint16, c_name: 'get(TIMING_LOG_IDX_SEARCH)'}
|
||||
foc_voltage: {type: readonly uint16, c_name: 'get(TIMING_LOG_FOC_VOLTAGE)'}
|
||||
foc_current: {type: readonly uint16, c_name: 'get(TIMING_LOG_FOC_CURRENT)'}
|
||||
spi_start: {type: readonly uint16, c_name: 'get(TIMING_LOG_SPI_START)'}
|
||||
sample_now: {type: readonly uint16, c_name: 'get(TIMING_LOG_SAMPLE_NOW)'}
|
||||
spi_end: {type: readonly uint16, c_name: 'get(TIMING_LOG_SPI_END)'}
|
||||
config:
|
||||
c_is_class: False
|
||||
attributes:
|
||||
pre_calibrated: {type: bool, c_setter: set_pre_calibrated}
|
||||
pole_pairs: int32
|
||||
calibration_current: float32
|
||||
resistance_calib_max_voltage: float32
|
||||
phase_inductance: {type: float32, c_setter: set_phase_inductance}
|
||||
phase_resistance: {type: float32, c_setter: set_phase_resistance}
|
||||
direction: int32
|
||||
motor_type: MotorType
|
||||
current_lim: float32
|
||||
current_lim_margin: float32
|
||||
inverter_temp_limit_lower: float32
|
||||
inverter_temp_limit_upper: float32
|
||||
requested_current_range: float32
|
||||
current_control_bandwidth: {type: float32, c_setter: set_current_control_bandwidth}
|
||||
acim_slip_velocity: float32
|
||||
acim_gain_min_flux: float32
|
||||
acim_autoflux_min_Id: float32
|
||||
acim_autoflux_enable: bool
|
||||
acim_autoflux_attack_gain: float32
|
||||
acim_autoflux_decay_gain: float32
|
||||
|
||||
|
||||
ODrive.Controller:
|
||||
c_is_class: True
|
||||
attributes:
|
||||
error:
|
||||
nullflag: None
|
||||
flags:
|
||||
Overspeed:
|
||||
InvalidInputMode:
|
||||
UnstableGain:
|
||||
InvalidMirrorAxis:
|
||||
InvalidLoadEncoder:
|
||||
InvalidEstimate:
|
||||
input_pos: {type: float32, c_setter: set_input_pos}
|
||||
input_vel: float32
|
||||
input_current: float32
|
||||
pos_setpoint: readonly float32
|
||||
vel_setpoint: readonly float32
|
||||
current_setpoint: readonly float32
|
||||
trajectory_done: readonly bool
|
||||
vel_integrator_current: float32
|
||||
anticogging_valid: bool
|
||||
config:
|
||||
c_is_class: False
|
||||
attributes:
|
||||
gain_scheduling_width: float32
|
||||
enable_vel_limit: bool
|
||||
enable_current_mode_vel_limit:
|
||||
type: bool
|
||||
doc: Enable velocity limit in current control mode (requires a valid velocity estimator).
|
||||
enable_gain_scheduling: bool
|
||||
enable_overspeed_error: bool
|
||||
control_mode: ControlMode
|
||||
input_mode: InputMode
|
||||
pos_gain:
|
||||
type: float32
|
||||
unit: (counts/s) / counts
|
||||
vel_gain:
|
||||
type: float32
|
||||
unit: 'A/(counts/s) (or A/(rad/s) in sensorless mode'
|
||||
vel_integrator_gain:
|
||||
type: float32
|
||||
unit: A/(counts/s * s)
|
||||
vel_limit:
|
||||
type: float32
|
||||
unit: counts/s
|
||||
doc: Infinity to disable.
|
||||
vel_limit_tolerance:
|
||||
type: float32
|
||||
doc: Ratio to `vel_limit`. Infinity to disable.
|
||||
vel_ramp_rate: float32
|
||||
current_ramp_rate:
|
||||
type: float32
|
||||
unit: A / sec
|
||||
homing_speed:
|
||||
type: float32
|
||||
unit: counts/s
|
||||
inertia:
|
||||
type: float32
|
||||
unit: A/(count/s^2)
|
||||
axis_to_mirror: uint8
|
||||
mirror_ratio: float32
|
||||
load_encoder_axis:
|
||||
type: uint8
|
||||
# TODO: this is meaningless for a user. Should there be a separate developer note?
|
||||
doc: Default depends on Axis number and is set in load_configuration()
|
||||
input_filter_bandwidth:
|
||||
type: float32
|
||||
unit: 1/s
|
||||
c_setter: set_input_filter_bandwidth
|
||||
anticogging:
|
||||
c_is_class: False
|
||||
attributes:
|
||||
index: readonly uint32
|
||||
pre_calibrated: bool
|
||||
calib_anticogging: readonly bool
|
||||
calib_pos_threshold: float32
|
||||
calib_vel_threshold: float32
|
||||
cogging_ratio: readonly float32
|
||||
anticogging_enabled: bool
|
||||
functions:
|
||||
move_incremental: {in: {displacement: float32, from_input_pos: bool}}
|
||||
start_anticogging_calibration:
|
||||
|
||||
|
||||
ODrive.Encoder:
|
||||
c_is_class: True
|
||||
attributes:
|
||||
error:
|
||||
nullflag: None
|
||||
flags:
|
||||
UnstableGain:
|
||||
CprPolepairsMismatch:
|
||||
NoResponse:
|
||||
UnsupportedEncoderMode:
|
||||
IllegalHallState:
|
||||
IndexNotFoundYet:
|
||||
AbsSpiTimeout:
|
||||
AbsSpiComFail:
|
||||
AbsSpiNotReady:
|
||||
is_ready: readonly bool
|
||||
index_found: readonly bool
|
||||
shadow_count: readonly int32
|
||||
count_in_cpr: readonly int32
|
||||
interpolation: readonly float32
|
||||
phase: readonly float32
|
||||
pos_estimate: readonly float32
|
||||
pos_cpr: readonly float32
|
||||
hall_state: readonly uint8
|
||||
vel_estimate: readonly float32
|
||||
calib_scan_response: readonly float32
|
||||
pos_abs: int32
|
||||
spi_error_rate: readonly float32
|
||||
config:
|
||||
c_is_class: False
|
||||
attributes:
|
||||
mode: Mode
|
||||
use_index: {type: bool, c_setter: set_use_index}
|
||||
find_idx_on_lockin_only: {type: bool, c_setter: set_find_idx_on_lockin_only}
|
||||
abs_spi_cs_gpio_pin: {type: uint16, c_setter: set_abs_spi_cs_gpio_pin}
|
||||
zero_count_on_find_idx: bool
|
||||
cpr: int32
|
||||
offset: int32
|
||||
pre_calibrated: {type: bool, c_setter: set_pre_calibrated}
|
||||
offset_float: float32
|
||||
enable_phase_interpolation: bool
|
||||
bandwidth: {type: float32, c_setter: set_bandwidth}
|
||||
calib_range: float32
|
||||
calib_scan_distance: float32
|
||||
calib_scan_omega: float32
|
||||
idx_search_unidirectional: bool
|
||||
ignore_illegal_hall_state: bool
|
||||
sincos_gpio_pin_sin: uint16
|
||||
sincos_gpio_pin_cos: uint16
|
||||
functions:
|
||||
set_linear_count: {in: {count: int32}}
|
||||
|
||||
|
||||
ODrive.SensorlessEstimator:
|
||||
c_is_class: True
|
||||
attributes:
|
||||
error:
|
||||
nullflag: None
|
||||
flags:
|
||||
UnstableGain:
|
||||
phase: float32
|
||||
pll_pos: float32
|
||||
vel_estimate: float32
|
||||
# pll_kp: float32
|
||||
# pll_ki: float32
|
||||
config:
|
||||
c_is_class: False
|
||||
attributes:
|
||||
observer_gain: float32
|
||||
pll_bandwidth: float32
|
||||
pm_flux_linkage: float32
|
||||
|
||||
|
||||
ODrive.TrapezoidalTrajectory:
|
||||
c_is_class: True
|
||||
attributes:
|
||||
config:
|
||||
c_is_class: False
|
||||
attributes:
|
||||
vel_limit: float32
|
||||
accel_limit: float32
|
||||
decel_limit: float32
|
||||
|
||||
|
||||
ODrive.Endstop:
|
||||
c_is_class: True
|
||||
attributes:
|
||||
endstop_state: readonly bool
|
||||
config:
|
||||
c_is_class: False
|
||||
attributes:
|
||||
gpio_num: {type: uint16, c_setter: set_gpio_num}
|
||||
enabled: {type: bool, c_setter: set_enabled}
|
||||
offset: float32
|
||||
is_active_high: bool
|
||||
pullup: bool
|
||||
debounce_ms: {type: uint32, c_setter: set_debounce_ms}
|
||||
|
||||
|
||||
valuetypes:
|
||||
ODrive.Can.Protocol:
|
||||
values: {Simple: }
|
||||
|
||||
ODrive.Axis.AxisState: # TODO: remove redundant "Axis" in name
|
||||
values:
|
||||
Undefined:
|
||||
doc: will fall through to idle
|
||||
Idle:
|
||||
doc: disable PWM and do nothing
|
||||
StartupSequence:
|
||||
doc: the actual sequence is defined by the config.startup... flags
|
||||
FullCalibrationSequence:
|
||||
doc: run all calibration procedures, then idle
|
||||
MotorCalibration:
|
||||
doc: run motor calibration
|
||||
SensorlessControl:
|
||||
doc: run sensorless control
|
||||
EncoderIndexSearch:
|
||||
doc: run encoder index search
|
||||
EncoderOffsetCalibration:
|
||||
doc: run encoder offset calibration
|
||||
ClosedLoopControl:
|
||||
doc: run closed loop control
|
||||
LockinSpin:
|
||||
doc: run lockin spin
|
||||
EncoderDirFind:
|
||||
Homing:
|
||||
doc: run axis homing function
|
||||
|
||||
ODrive.Encoder.Mode:
|
||||
values:
|
||||
Incremental:
|
||||
Hall:
|
||||
Sincos:
|
||||
SpiAbsCui:
|
||||
value: 0x100
|
||||
doc: compatible with CUI AMT23xx
|
||||
SpiAbsAms:
|
||||
value: 0x101
|
||||
doc: compatible with AMS AS5047P, AS5048A/AS5048B (no daisy chain support)
|
||||
SpiAbsAeat:
|
||||
value: 0x102
|
||||
doc: not yet implemented
|
||||
|
||||
ODrive.Controller.ControlMode:
|
||||
values:
|
||||
# Note: these should be sorted from lowest level of control to
|
||||
# highest level of control, to allow "<" style comparisons.
|
||||
VoltageControl:
|
||||
CurrentControl:
|
||||
VelocityControl:
|
||||
PositionControl:
|
||||
|
||||
ODrive.Controller.InputMode:
|
||||
values:
|
||||
Inactive:
|
||||
Passthrough:
|
||||
VelRamp:
|
||||
PosFilter:
|
||||
MixChannels:
|
||||
TrapTraj:
|
||||
CurrentRamp:
|
||||
Mirror:
|
||||
|
||||
|
||||
ODrive.Motor.MotorType:
|
||||
values:
|
||||
HighCurrent:
|
||||
#LowCurrent: # not implemented
|
||||
Gimbal: {value: 2}
|
||||
Acim:
|
||||
@@ -0,0 +1,131 @@
|
||||
{
|
||||
"GPIO_MODE_DIGITAL": 0,
|
||||
"GPIO_MODE_DIGITAL_PULL_UP": 1,
|
||||
"GPIO_MODE_DIGITAL_PULL_DOWN": 2,
|
||||
"GPIO_MODE_ANALOG_IN": 3,
|
||||
"GPIO_MODE_UART_A": 4,
|
||||
"GPIO_MODE_UART_B": 5,
|
||||
"GPIO_MODE_UART_C": 6,
|
||||
"GPIO_MODE_CAN_A": 7,
|
||||
"GPIO_MODE_I2C_A": 8,
|
||||
"GPIO_MODE_SPI_A": 9,
|
||||
"GPIO_MODE_PWM": 10,
|
||||
"GPIO_MODE_ENC0": 11,
|
||||
"GPIO_MODE_ENC1": 12,
|
||||
"GPIO_MODE_ENC2": 13,
|
||||
"GPIO_MODE_MECH_BRAKE": 14,
|
||||
"GPIO_MODE_STATUS": 15,
|
||||
"STREAM_PROTOCOL_TYPE_FIBRE": 0,
|
||||
"STREAM_PROTOCOL_TYPE_ASCII": 1,
|
||||
"STREAM_PROTOCOL_TYPE_STDOUT": 2,
|
||||
"STREAM_PROTOCOL_TYPE_ASCII_AND_STDOUT": 3,
|
||||
"PROTOCOL_SIMPLE": 1,
|
||||
"AXIS_STATE_UNDEFINED": 0,
|
||||
"AXIS_STATE_IDLE": 1,
|
||||
"AXIS_STATE_STARTUP_SEQUENCE": 2,
|
||||
"AXIS_STATE_FULL_CALIBRATION_SEQUENCE": 3,
|
||||
"AXIS_STATE_MOTOR_CALIBRATION": 4,
|
||||
"AXIS_STATE_ENCODER_INDEX_SEARCH": 6,
|
||||
"AXIS_STATE_ENCODER_OFFSET_CALIBRATION": 7,
|
||||
"AXIS_STATE_CLOSED_LOOP_CONTROL": 8,
|
||||
"AXIS_STATE_LOCKIN_SPIN": 9,
|
||||
"AXIS_STATE_ENCODER_DIR_FIND": 10,
|
||||
"AXIS_STATE_HOMING": 11,
|
||||
"AXIS_STATE_ENCODER_HALL_POLARITY_CALIBRATION": 12,
|
||||
"AXIS_STATE_ENCODER_HALL_PHASE_CALIBRATION": 13,
|
||||
"ENCODER_MODE_INCREMENTAL": 0,
|
||||
"ENCODER_MODE_HALL": 1,
|
||||
"ENCODER_MODE_SINCOS": 2,
|
||||
"ENCODER_MODE_SPI_ABS_CUI": 256,
|
||||
"ENCODER_MODE_SPI_ABS_AMS": 257,
|
||||
"ENCODER_MODE_SPI_ABS_AEAT": 258,
|
||||
"ENCODER_MODE_SPI_ABS_RLS": 259,
|
||||
"ENCODER_MODE_SPI_ABS_MA732": 260,
|
||||
"CONTROL_MODE_VOLTAGE_CONTROL": 0,
|
||||
"CONTROL_MODE_TORQUE_CONTROL": 1,
|
||||
"CONTROL_MODE_VELOCITY_CONTROL": 2,
|
||||
"CONTROL_MODE_POSITION_CONTROL": 3,
|
||||
"INPUT_MODE_INACTIVE": 0,
|
||||
"INPUT_MODE_PASSTHROUGH": 1,
|
||||
"INPUT_MODE_VEL_RAMP": 2,
|
||||
"INPUT_MODE_POS_FILTER": 3,
|
||||
"INPUT_MODE_MIX_CHANNELS": 4,
|
||||
"INPUT_MODE_TRAP_TRAJ": 5,
|
||||
"INPUT_MODE_TORQUE_RAMP": 6,
|
||||
"INPUT_MODE_MIRROR": 7,
|
||||
"INPUT_MODE_TUNING": 8,
|
||||
"MOTOR_TYPE_HIGH_CURRENT": 0,
|
||||
"MOTOR_TYPE_GIMBAL": 2,
|
||||
"MOTOR_TYPE_ACIM": 3,
|
||||
"ODRIVE_ERROR_NONE": 0,
|
||||
"ODRIVE_ERROR_CONTROL_ITERATION_MISSED": 1,
|
||||
"ODRIVE_ERROR_DC_BUS_UNDER_VOLTAGE": 2,
|
||||
"ODRIVE_ERROR_DC_BUS_OVER_VOLTAGE": 4,
|
||||
"ODRIVE_ERROR_DC_BUS_OVER_REGEN_CURRENT": 8,
|
||||
"ODRIVE_ERROR_DC_BUS_OVER_CURRENT": 16,
|
||||
"ODRIVE_ERROR_BRAKE_DEADTIME_VIOLATION": 32,
|
||||
"ODRIVE_ERROR_BRAKE_DUTY_CYCLE_NAN": 64,
|
||||
"ODRIVE_ERROR_INVALID_BRAKE_RESISTANCE": 128,
|
||||
"CAN_ERROR_NONE": 0,
|
||||
"CAN_ERROR_DUPLICATE_CAN_IDS": 1,
|
||||
"AXIS_ERROR_NONE": 0,
|
||||
"AXIS_ERROR_INVALID_STATE": 1,
|
||||
"AXIS_ERROR_WATCHDOG_TIMER_EXPIRED": 2048,
|
||||
"AXIS_ERROR_MIN_ENDSTOP_PRESSED": 4096,
|
||||
"AXIS_ERROR_MAX_ENDSTOP_PRESSED": 8192,
|
||||
"AXIS_ERROR_ESTOP_REQUESTED": 16384,
|
||||
"AXIS_ERROR_HOMING_WITHOUT_ENDSTOP": 131072,
|
||||
"AXIS_ERROR_OVER_TEMP": 262144,
|
||||
"AXIS_ERROR_UNKNOWN_POSITION": 524288,
|
||||
"MOTOR_ERROR_NONE": 0,
|
||||
"MOTOR_ERROR_PHASE_RESISTANCE_OUT_OF_RANGE": 1,
|
||||
"MOTOR_ERROR_PHASE_INDUCTANCE_OUT_OF_RANGE": 2,
|
||||
"MOTOR_ERROR_DRV_FAULT": 8,
|
||||
"MOTOR_ERROR_CONTROL_DEADLINE_MISSED": 16,
|
||||
"MOTOR_ERROR_MODULATION_MAGNITUDE": 128,
|
||||
"MOTOR_ERROR_CURRENT_SENSE_SATURATION": 1024,
|
||||
"MOTOR_ERROR_CURRENT_LIMIT_VIOLATION": 4096,
|
||||
"MOTOR_ERROR_MODULATION_IS_NAN": 65536,
|
||||
"MOTOR_ERROR_MOTOR_THERMISTOR_OVER_TEMP": 131072,
|
||||
"MOTOR_ERROR_FET_THERMISTOR_OVER_TEMP": 262144,
|
||||
"MOTOR_ERROR_TIMER_UPDATE_MISSED": 524288,
|
||||
"MOTOR_ERROR_CURRENT_MEASUREMENT_UNAVAILABLE": 1048576,
|
||||
"MOTOR_ERROR_CONTROLLER_FAILED": 2097152,
|
||||
"MOTOR_ERROR_I_BUS_OUT_OF_RANGE": 4194304,
|
||||
"MOTOR_ERROR_BRAKE_RESISTOR_DISARMED": 8388608,
|
||||
"MOTOR_ERROR_SYSTEM_LEVEL": 16777216,
|
||||
"MOTOR_ERROR_BAD_TIMING": 33554432,
|
||||
"MOTOR_ERROR_UNKNOWN_PHASE_ESTIMATE": 67108864,
|
||||
"MOTOR_ERROR_UNKNOWN_PHASE_VEL": 134217728,
|
||||
"MOTOR_ERROR_UNKNOWN_TORQUE": 268435456,
|
||||
"MOTOR_ERROR_UNKNOWN_CURRENT_COMMAND": 536870912,
|
||||
"MOTOR_ERROR_UNKNOWN_CURRENT_MEASUREMENT": 1073741824,
|
||||
"MOTOR_ERROR_UNKNOWN_VBUS_VOLTAGE": 2147483648,
|
||||
"MOTOR_ERROR_UNKNOWN_VOLTAGE_COMMAND": 4294967296,
|
||||
"MOTOR_ERROR_UNKNOWN_GAINS": 8589934592,
|
||||
"MOTOR_ERROR_CONTROLLER_INITIALIZING": 17179869184,
|
||||
"MOTOR_ERROR_UNBALANCED_PHASES": 34359738368,
|
||||
"CONTROLLER_ERROR_NONE": 0,
|
||||
"CONTROLLER_ERROR_OVERSPEED": 1,
|
||||
"CONTROLLER_ERROR_INVALID_INPUT_MODE": 2,
|
||||
"CONTROLLER_ERROR_UNSTABLE_GAIN": 4,
|
||||
"CONTROLLER_ERROR_INVALID_MIRROR_AXIS": 8,
|
||||
"CONTROLLER_ERROR_INVALID_LOAD_ENCODER": 16,
|
||||
"CONTROLLER_ERROR_INVALID_ESTIMATE": 32,
|
||||
"CONTROLLER_ERROR_INVALID_CIRCULAR_RANGE": 64,
|
||||
"CONTROLLER_ERROR_SPINOUT_DETECTED": 128,
|
||||
"ENCODER_ERROR_NONE": 0,
|
||||
"ENCODER_ERROR_UNSTABLE_GAIN": 1,
|
||||
"ENCODER_ERROR_CPR_POLEPAIRS_MISMATCH": 2,
|
||||
"ENCODER_ERROR_NO_RESPONSE": 4,
|
||||
"ENCODER_ERROR_UNSUPPORTED_ENCODER_MODE": 8,
|
||||
"ENCODER_ERROR_ILLEGAL_HALL_STATE": 16,
|
||||
"ENCODER_ERROR_INDEX_NOT_FOUND_YET": 32,
|
||||
"ENCODER_ERROR_ABS_SPI_TIMEOUT": 64,
|
||||
"ENCODER_ERROR_ABS_SPI_COM_FAIL": 128,
|
||||
"ENCODER_ERROR_ABS_SPI_NOT_READY": 256,
|
||||
"ENCODER_ERROR_HALL_NOT_CALIBRATED_YET": 512,
|
||||
"SENSORLESS_ESTIMATOR_ERROR_NONE": 0,
|
||||
"SENSORLESS_ESTIMATOR_ERROR_UNSTABLE_GAIN": 1,
|
||||
"SENSORLESS_ESTIMATOR_ERROR_UNKNOWN_CURRENT_MEASUREMENT": 2
|
||||
}
|
||||
|
After Width: | Height: | Size: 52 KiB |
@@ -0,0 +1,23 @@
|
||||
.card {
|
||||
padding: 10px;
|
||||
margin: 10px;
|
||||
background-color: #fff;
|
||||
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.close-button {
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
padding: 0 5px;
|
||||
margin-right: 10px;
|
||||
border: 1px solid black;
|
||||
}
|
||||
|
||||
.close-button:active {
|
||||
background-color: var(--bg-color);
|
||||
}
|
||||
|
||||
.choice-inactive {
|
||||
background-color: var(--bg-color);
|
||||
color: grey;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
:root {
|
||||
--bg-color: rgb(235, 235, 235);
|
||||
--fg-color: #fff;
|
||||
--top-height: 39px;
|
||||
--bottom-height: 29px;
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
{
|
||||
"config": {
|
||||
"brake_resistance": null
|
||||
},
|
||||
"axis0": {
|
||||
"motor": {
|
||||
"config": {
|
||||
"pole_pairs": null,
|
||||
"phase_resistance": null,
|
||||
"phase_inductance": null,
|
||||
"motor_type": null,
|
||||
"current_lim": null,
|
||||
"torque_constant": null
|
||||
}
|
||||
},
|
||||
"encoder": {
|
||||
"config": {
|
||||
"mode": null,
|
||||
"use_index": null,
|
||||
"cpr": null,
|
||||
"calib_scan_distance": null,
|
||||
"abs_spi_cs_gpio_pin": null
|
||||
}
|
||||
},
|
||||
"controller": {
|
||||
"config": {
|
||||
"input_mode": null,
|
||||
"control_mode": null,
|
||||
"vel_limit": null,
|
||||
"inertia": null
|
||||
}
|
||||
},
|
||||
"trap_traj": {
|
||||
"config": {
|
||||
"vel_limit": null,
|
||||
"accel_limit": null,
|
||||
"decel_limit": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"axis1": {
|
||||
"motor": {
|
||||
"config": {
|
||||
"pole_pairs": null,
|
||||
"phase_resistance": null,
|
||||
"phase_inductance": null,
|
||||
"motor_type": null,
|
||||
"current_lim": null,
|
||||
"torque_constant": null
|
||||
}
|
||||
},
|
||||
"encoder": {
|
||||
"config": {
|
||||
"mode": null,
|
||||
"use_index": null,
|
||||
"cpr": null,
|
||||
"calib_scan_distance": null,
|
||||
"abs_spi_cs_gpio_pin": null
|
||||
}
|
||||
},
|
||||
"controller": {
|
||||
"config": {
|
||||
"input_mode": null,
|
||||
"control_mode": null,
|
||||
"vel_limit": null,
|
||||
"inertia": null
|
||||
}
|
||||
},
|
||||
"trap_traj": {
|
||||
"config": {
|
||||
"vel_limit": null,
|
||||
"accel_limit": null,
|
||||
"decel_limit": null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,929 @@
|
||||
import odriveEnums from "../odriveEnums.json";
|
||||
|
||||
// wizard is an object of pages.
|
||||
// each page has a number, title, vue component name, next destination, back destination, and array of choices
|
||||
// certain choices require a hook - for example, hall effect encoder cpr is 6 * motor.config.pole_pairs
|
||||
// each hook takes a config object, modifies it, and returns it after running appropriate calculations.
|
||||
// see choiceHandler() in Wizard.vue for how this happens
|
||||
|
||||
// Pages can also have custom components displayed below the choice tiles
|
||||
// Example - on motor pages, there are buttons to run motor calibration and clear errors.
|
||||
export let pages = {
|
||||
ODrive: {
|
||||
title: "Which ODrive do you have?",
|
||||
link: "ODrive Version",
|
||||
component: "wizardPage",
|
||||
next: "Brake",
|
||||
back: "ODrive",
|
||||
nextTooltip: "Make a choice!",
|
||||
choiceMade: false,
|
||||
choices: [
|
||||
{
|
||||
component: "wizardChoice",
|
||||
data: {
|
||||
imageURL: require("../images/24v_300x300.png"),
|
||||
},
|
||||
title: "ODrive v3.6 24V",
|
||||
requirements: [],
|
||||
hooks: [],
|
||||
tooltip: null,
|
||||
altTooltip: null,
|
||||
},
|
||||
{
|
||||
component: "wizardChoice",
|
||||
data: {
|
||||
imageURL: require("../images/56v_300x300.png"),
|
||||
},
|
||||
title: "ODrive v3.6 56V",
|
||||
requirements: [],
|
||||
hooks: [],
|
||||
tooltip: null,
|
||||
altTooltip: null,
|
||||
},
|
||||
],
|
||||
customComponents: [],
|
||||
pageComponents: [],
|
||||
},
|
||||
Brake: {
|
||||
title: "Brake Resistor",
|
||||
link: "Brake Resistor",
|
||||
component: "wizardPage",
|
||||
next: "Motor_0",
|
||||
back: "ODrive",
|
||||
nextTooltip: "Make a choice!",
|
||||
choiceMade: false,
|
||||
choices: [
|
||||
{
|
||||
component: "wizardBrake",
|
||||
data: {},
|
||||
title: "Brake Resistor",
|
||||
requirements: [],
|
||||
hooks: [],
|
||||
tooltip: "When you slow down a motor, the energy has to go somewhere.\n\r \
|
||||
It either goes to the brake resistor or back to your power supply.",
|
||||
altTooltip: null,
|
||||
},
|
||||
{
|
||||
component: "wizardChoice",
|
||||
data: {
|
||||
imageURL: null,
|
||||
configStub: {
|
||||
config: {
|
||||
brake_resistance: 0,
|
||||
}
|
||||
},
|
||||
},
|
||||
title: "I don't have a brake resistor",
|
||||
hooks: [],
|
||||
requirements: [],
|
||||
tooltip: "Only operate without a brake resistor if your power source can handle reverse current, like a battery."
|
||||
}
|
||||
],
|
||||
},
|
||||
Motor_0: {
|
||||
title: "Which motor are you using on Axis 0?",
|
||||
link: "Motor 0",
|
||||
component: "wizardPage",
|
||||
next: "Encoder_0",
|
||||
back: "Brake",
|
||||
nextTooltip: "ODrive needs to know the inductance and resistance of your motor in order to control it.\
|
||||
Click the Calibrate Motor button to start the measurement process. The motor will beep\
|
||||
during calibration.",
|
||||
choiceMade: false,
|
||||
choices: [
|
||||
{
|
||||
component: "wizardChoice",
|
||||
data: {
|
||||
imageURL: require("../images/D5065_300x300.png"),
|
||||
configStub: {
|
||||
axis0: {
|
||||
motor: {
|
||||
config: {
|
||||
pole_pairs: 7,
|
||||
torque_constant: 8.27 / 270,
|
||||
// R and L come from "Calibrate Motor" button on page
|
||||
//phase_resistance: 0.039, // from test rig
|
||||
//phase_inductance: 1.57e-5,
|
||||
motor_type: odriveEnums.MOTOR_TYPE_HIGH_CURRENT, // MOTOR_TYPE_HIGH_CURRENT
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
title: "ODrive D5065",
|
||||
requirements: [],
|
||||
hooks: [],
|
||||
tooltip: "D5065 motor from the ODrive shop",
|
||||
altTooltip: null,
|
||||
},
|
||||
{
|
||||
component: "wizardChoice",
|
||||
data: {
|
||||
imageURL: require("../images/D6374_300x300.png"),
|
||||
configStub: {
|
||||
axis0: {
|
||||
motor: {
|
||||
config: {
|
||||
pole_pairs: 7,
|
||||
torque_constant: 8.27 / 150,
|
||||
// R and L come from "Calibrate Motor" button on page
|
||||
//phase_resistance: 0.041, // measurement from PJ
|
||||
//phase_inductance: 2.23e-5,
|
||||
motor_type: odriveEnums.MOTOR_TYPE_HIGH_CURRENT, // MOTOR_TYPE_HIGH_CURRENT,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
title: "ODrive D6374",
|
||||
requirements: [],
|
||||
hooks: [],
|
||||
tooltip: "D6374 motor from the ODrive shop",
|
||||
altTooltip: null,
|
||||
},
|
||||
{
|
||||
component: "wizardMotor",
|
||||
data: {
|
||||
axis: "axis0",
|
||||
},
|
||||
title: "Other Motor",
|
||||
requirements: [],
|
||||
hooks: [],
|
||||
tooltip: "Bring your own motor!",
|
||||
altTooltip: null,
|
||||
},
|
||||
],
|
||||
pageComponents: [
|
||||
{
|
||||
component: "wizardMotorCal",
|
||||
id: 0,
|
||||
data: {
|
||||
axis: "axis0",
|
||||
}
|
||||
},
|
||||
{
|
||||
component: "wizardCalStatus",
|
||||
id: 1,
|
||||
data: {
|
||||
success: "Calibration succeeded!",
|
||||
failure: "Calibration failed",
|
||||
tooltip: "Double check your motor connections. If the connections are good, it is \
|
||||
possible that your motor has a higher resistance or lower inductance than ODrive expects."
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
Encoder_0: {
|
||||
title: "Which encoder are you using for Axis 0?",
|
||||
link: "Encoder 0",
|
||||
component: "wizardPage",
|
||||
next: "Control_0",
|
||||
back: "Motor_0",
|
||||
nextTooltip: "ODrive needs to know the relation between the encoder position and motor position in order to function. \
|
||||
Click the Calibrate Encoder button to start the measurement process. The motor will slowly rotate back \
|
||||
and forth during calibration.",
|
||||
choiceMade: false,
|
||||
choices: [
|
||||
{
|
||||
component: "wizardChoice",
|
||||
data: {
|
||||
imageURL: require("../images/amt102-v_300x300.png"),
|
||||
configStub: {
|
||||
axis0: {
|
||||
encoder: {
|
||||
config: {
|
||||
cpr: 8192,
|
||||
use_index: true,
|
||||
mode: odriveEnums.ENCODER_MODE_INCREMENTAL, // ENCODER_MODE_INCREMENTAL
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
title: "CUI AMT102V",
|
||||
requirements: [],
|
||||
hooks: [],
|
||||
tooltip: "Incremental 8192cpr encoder from the ODrive shop",
|
||||
altTooltip: null,
|
||||
},
|
||||
{
|
||||
component: "wizardChoice",
|
||||
data: {
|
||||
imageURL: require("../images/hall_effect_300x300.png"),
|
||||
configStub: {
|
||||
axis0: {
|
||||
encoder: {
|
||||
config: {
|
||||
use_index: false,
|
||||
mode: odriveEnums.ENCODER_MODE_HALL,
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
title: "Hall Effect",
|
||||
requirements: [],
|
||||
hooks: [
|
||||
// hook takes a config object (normally this.wizardConfig in Wizard.vue), creates and returns a copy
|
||||
(configObj) => {
|
||||
let newConfig = configObj;
|
||||
newConfig.axis0.encoder.config.cpr = 6 * newConfig.axis0.motor.config.pole_pairs;
|
||||
return newConfig;
|
||||
},
|
||||
],
|
||||
tooltip: "If your motor is 'sensored' and you don't have another encoder, select this option",
|
||||
altTooltip: null,
|
||||
},
|
||||
{
|
||||
component: "wizardEncoderIncremental",
|
||||
data: {
|
||||
axis: "axis0",
|
||||
},
|
||||
title: "Incremental Without Index",
|
||||
requirements: [],
|
||||
hooks: [],
|
||||
tooltip: "Generic incremental encoder without index. Set cpr to 4 * PPR (pulses per revolution).",
|
||||
altTooltip: null,
|
||||
},
|
||||
{
|
||||
component: "wizardEncoderIncrementalIndex",
|
||||
data: {
|
||||
axis: "axis0",
|
||||
},
|
||||
title: "Incremental With Index",
|
||||
requirements: [],
|
||||
hooks: [],
|
||||
tooltip: "Generic incremental encoder with index. Set cpr to 4 * PPR (pulses per revolution).",
|
||||
altTooltip: null,
|
||||
},
|
||||
],
|
||||
pageComponents: [
|
||||
{
|
||||
component: "wizardEncoderCal",
|
||||
id: 0,
|
||||
data: {
|
||||
axis: "axis0",
|
||||
}
|
||||
},
|
||||
{
|
||||
component: "wizardCalStatus",
|
||||
id: 1,
|
||||
data: {
|
||||
success: "Calibration succeeded!",
|
||||
failure: "Calibration failed",
|
||||
tooltip: "Double check your encoder connections. If the connections are good, you \
|
||||
have a wrong setting for motor pole pairs or encoder cpr."
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
Control_0: {
|
||||
title: "Control mode for Axis 0",
|
||||
link: "Control Modes 0",
|
||||
component: "wizardPage",
|
||||
next: "Misc_0",
|
||||
back: "Encoder_0",
|
||||
nextTooltip: "Make a choice!",
|
||||
choiceMade: false,
|
||||
choices: [
|
||||
{
|
||||
component: "wizardChoice",
|
||||
data: {
|
||||
imageURL: require("../images/temp.png"),
|
||||
configStub: {
|
||||
axis0: {
|
||||
controller: {
|
||||
config: {
|
||||
control_mode: odriveEnums.CONTROL_MODE_POSITION_CONTROL,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
title: "Position Control",
|
||||
requirements: [],
|
||||
hooks: [],
|
||||
tooltip: "Use this mode to control the position of the motor",
|
||||
altTooltip: null,
|
||||
},
|
||||
{
|
||||
component: "wizardChoice",
|
||||
data: {
|
||||
imageURL: require("../images/temp.png"),
|
||||
configStub: {
|
||||
axis0: {
|
||||
controller: {
|
||||
config: {
|
||||
control_mode: odriveEnums.CONTROL_MODE_VELOCITY_CONTROL,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
title: "Velocity Control",
|
||||
requirements: [],
|
||||
hooks: [],
|
||||
tooltip: "Use this mode to control the velocity of the motor",
|
||||
altTooltip: null,
|
||||
},
|
||||
{
|
||||
component: "wizardChoice",
|
||||
data: {
|
||||
imageURL: require("../images/temp.png"),
|
||||
configStub: {
|
||||
axis0: {
|
||||
controller: {
|
||||
config: {
|
||||
control_mode: odriveEnums.CONTROL_MODE_TORQUE_CONTROL,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
title: "Torque Control",
|
||||
requirements: [],
|
||||
hooks: [],
|
||||
tooltip: "Use this mode to control the torque output of the motor",
|
||||
altTooltip: null,
|
||||
},
|
||||
{
|
||||
component: "wizardChoice",
|
||||
data: {
|
||||
imageURL: require("../images/temp.png"),
|
||||
configStub: {
|
||||
axis0: {
|
||||
controller: {
|
||||
config: {
|
||||
control_mode: odriveEnums.CONTROL_MODE_VOLTAGE_CONTROL,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
title: "Voltage Control",
|
||||
requirements: [],
|
||||
hooks: [],
|
||||
tooltip: "This mode is used for gimbal motors",
|
||||
altTooltip: null,
|
||||
},
|
||||
],
|
||||
customComponents: [],
|
||||
pageComponents: [],
|
||||
},
|
||||
/*
|
||||
Input_0: {
|
||||
title: "Input mode selection for Axis 0",
|
||||
link: "Input Modes 0",
|
||||
component: "wizardPage",
|
||||
next: "Misc_0",
|
||||
back: "Control_0",
|
||||
nextTooltip: "Make a choice!",
|
||||
choiceMade: false,
|
||||
choices: [
|
||||
{
|
||||
component: "wizardChoice",
|
||||
data: {
|
||||
imageURL: require("../images/temp.png"),
|
||||
configStub: {
|
||||
axis0: {
|
||||
controller: {
|
||||
config: {
|
||||
input_mode: odriveEnums.INPUT_MODE_PASSTHROUGH,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
title: "Input Passthrough",
|
||||
requirements: [],
|
||||
hooks: [],
|
||||
tooltip: "This is the default input mode. Input commands are passed to the controller without modification.",
|
||||
altTooltip: null,
|
||||
},
|
||||
{
|
||||
component: "wizardInputFiltered",
|
||||
data: {
|
||||
axis: "axis0",
|
||||
},
|
||||
title: "Position Input Filtering",
|
||||
requirements: [
|
||||
(configObj) => {
|
||||
return configObj.axis0.controller.config.control_mode == odriveEnums.CONTROL_MODE_POSITION_CONTROL;
|
||||
},
|
||||
],
|
||||
hooks: [],
|
||||
tooltip: "Filtered position mode. This provides smoother motion than the passthrough mode. \
|
||||
Input position commands are sent through a 2nd order filter. A higher bandwidth will cause quicker position changes.",
|
||||
altTooltip: "Control mode must be Position Control for this input mode",
|
||||
},
|
||||
{
|
||||
component: "wizardInputVelRamp",
|
||||
data: {
|
||||
axis: "axis0",
|
||||
},
|
||||
title: "Velocity Input Ramping",
|
||||
requirements: [
|
||||
(configObj) => {
|
||||
return configObj.axis0.controller.config.control_mode == odriveEnums.CONTROL_MODE_VELOCITY_CONTROL;
|
||||
},
|
||||
],
|
||||
hooks: [],
|
||||
tooltip: "For velocity control mode, you can choose to have the input velocities ramped between values. vel_ramp_rate is the \
|
||||
acceleration between velocities.",
|
||||
altTooltip: "Control mode must be Velocity Control for this input mode",
|
||||
},
|
||||
{
|
||||
component: "wizardInputTrajectory",
|
||||
data: {
|
||||
axis: "axis0",
|
||||
},
|
||||
title: "Trajectory Planning",
|
||||
requirements: [
|
||||
(configObj) => {
|
||||
return configObj.axis0.controller.config.control_mode == odriveEnums.CONTROL_MODE_POSITION_CONTROL;
|
||||
},
|
||||
],
|
||||
hooks: [],
|
||||
tooltip: "This mode lets you smoothly accelerate, coast, and decelerate from one position to another. vel_limit is your coasting speed, \
|
||||
accel_limit is the maximum acceleration in turns/s^2, decel_limit is the maximum decelaration in turns/s^2 and inertia is an \
|
||||
optional value to correlate acceleration with motor torque.",
|
||||
altTooltip: "Control mode must be Position Control for this input mode",
|
||||
}
|
||||
],
|
||||
pageComponents: [],
|
||||
},
|
||||
*/
|
||||
Misc_0: {
|
||||
title: "Finishing touches for Axis 0",
|
||||
link: "Misc 0",
|
||||
component: "wizardPage",
|
||||
next: "Motor_1",
|
||||
back: "Control_0",
|
||||
nextTooltip: "Make a choice!",
|
||||
choiceMade: false,
|
||||
choices: [
|
||||
{
|
||||
component: "wizardMisc",
|
||||
data: {
|
||||
axis: "axis0",
|
||||
},
|
||||
title: "Miscellaneous Parameters",
|
||||
requirements: [],
|
||||
hooks: [],
|
||||
tooltip: null,
|
||||
altTooltip: null,
|
||||
},
|
||||
],
|
||||
pageComponents: [],
|
||||
},
|
||||
Motor_1: {
|
||||
title: "Which motor are you using for Axis 1?",
|
||||
link: "Motor 1",
|
||||
component: "wizardPage",
|
||||
next: "Encoder_1",
|
||||
back: "Misc_0",
|
||||
nextTooltip: "ODrive needs to know the inductance and resistance of your motor in order to control it.\
|
||||
Click the Calibrate Motor button to start the measurement process. The motor will beep\
|
||||
during calibration.",
|
||||
choiceMade: false,
|
||||
choices: [
|
||||
{
|
||||
component: "wizardChoice",
|
||||
data: {
|
||||
imageURL: require("../images/D5065_300x300.png"),
|
||||
configStub: {
|
||||
axis1: {
|
||||
motor: {
|
||||
config: {
|
||||
pole_pairs: 7,
|
||||
torque_constant: 8.27 / 270,
|
||||
// R and L come from "Calibrate Motor" button on page
|
||||
//phase_resistance: 0.039, // from test rig
|
||||
//phase_inductance: 1.57e-5,
|
||||
motor_type: odriveEnums.MOTOR_TYPE_HIGH_CURRENT, // MOTOR_TYPE_HIGH_CURRENT
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
title: "ODrive D5065",
|
||||
requirements: [],
|
||||
hooks: [],
|
||||
tooltip: "D5065 motor from the ODrive shop",
|
||||
altTooltip: null,
|
||||
},
|
||||
{
|
||||
component: "wizardChoice",
|
||||
data: {
|
||||
imageURL: require("../images/D6374_300x300.png"),
|
||||
configStub: {
|
||||
axis1: {
|
||||
motor: {
|
||||
config: {
|
||||
pole_pairs: 7,
|
||||
torque_constant: 8.27 / 150,
|
||||
// R and L come from "Calibrate Motor" button on page
|
||||
//phase_resistance: 0.041, // measurement from PJ
|
||||
//phase_inductance: 2.23e-5,
|
||||
motor_type: odriveEnums.MOTOR_TYPE_HIGH_CURRENT, // MOTOR_TYPE_HIGH_CURRENT,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
title: "ODrive D6374",
|
||||
requirements: [],
|
||||
hooks: [],
|
||||
tooltip: "D6374 motor from the ODrive shop",
|
||||
altTooltip: null,
|
||||
},
|
||||
{
|
||||
component: "wizardMotor",
|
||||
data: {
|
||||
axis: "axis1",
|
||||
},
|
||||
title: "Other Motor",
|
||||
requirements: [],
|
||||
hooks: [],
|
||||
tooltip: "Bring your own motor!",
|
||||
altTooltip: null,
|
||||
},
|
||||
],
|
||||
pageComponents: [
|
||||
{
|
||||
component: "wizardMotorCal",
|
||||
id: 0,
|
||||
data: {
|
||||
axis: "axis1",
|
||||
}
|
||||
},
|
||||
{
|
||||
component: "wizardCalStatus",
|
||||
id: 1,
|
||||
data: {
|
||||
success: "Calibration succeeded!",
|
||||
failure: "Calibration failed",
|
||||
tooltip: "Double check your motor connections. If the connections are good, it is \
|
||||
possible that your motor has a higher resistance or lower inductance than ODrive expects."
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
Encoder_1: {
|
||||
title: "Which encoder are you using for Axis 1?",
|
||||
link: "Encoder 1",
|
||||
component: "wizardPage",
|
||||
next: "Control_1",
|
||||
back: "Motor_1",
|
||||
nextTooltip: "ODrive needs to know the relation between the encoder position and motor position in order to function. \
|
||||
Click the Calibrate Encoder button to start the measurement process. The motor will slowly rotate back \
|
||||
and forth during calibration.",
|
||||
choiceMade: false,
|
||||
choices: [
|
||||
{
|
||||
component: "wizardChoice",
|
||||
data: {
|
||||
imageURL: require("../images/amt102-v_300x300.png"),
|
||||
configStub: {
|
||||
axis1: {
|
||||
encoder: {
|
||||
config: {
|
||||
cpr: 8192,
|
||||
use_index: true,
|
||||
mode: odriveEnums.ENCODER_MODE_INCREMENTAL, // ENCODER_MODE_INCREMENTAL
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
title: "CUI AMT102V",
|
||||
requirements: [],
|
||||
hooks: [],
|
||||
tooltip: "Incremental 8192cpr encoder from the ODrive shop",
|
||||
altTooltip: null,
|
||||
},
|
||||
{
|
||||
component: "wizardChoice",
|
||||
data: {
|
||||
imageURL: require("../images/hall_effect_300x300.png"),
|
||||
configStub: {
|
||||
axis1: {
|
||||
encoder: {
|
||||
config: {
|
||||
use_index: false,
|
||||
mode: odriveEnums.ENCODER_MODE_HALL,
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
title: "Hall Effect",
|
||||
requirements: [],
|
||||
hooks: [
|
||||
// hook takes a config object (normally this.wizardConfig in Wizard.vue), creates and returns a copy
|
||||
(configObj) => {
|
||||
let newConfig = configObj;
|
||||
newConfig.axis1.encoder.config.cpr = 6 * newConfig.axis1.motor.config.pole_pairs;
|
||||
return newConfig;
|
||||
},
|
||||
],
|
||||
tooltip: "If your motor is 'sensored' and you don't have another encoder, select this option",
|
||||
altTooltip: null,
|
||||
},
|
||||
{
|
||||
component: "wizardEncoderIncremental",
|
||||
data: {
|
||||
axis: "axis1",
|
||||
},
|
||||
title: "Incremental Without Index",
|
||||
requirements: [],
|
||||
hooks: [],
|
||||
tooltip: "Generic incremental encoder without index. Set cpr to 4 * PPR (pulses per revolution).",
|
||||
altTooltip: null,
|
||||
},
|
||||
{
|
||||
component: "wizardEncoderIncrementalIndex",
|
||||
data: {
|
||||
axis: "axis1",
|
||||
},
|
||||
title: "Incremental With Index",
|
||||
requirements: [],
|
||||
hooks: [],
|
||||
tooltip: "Generic incremental encoder with index. Set cpr to 4 * PPR (pulses per revolution).",
|
||||
altTooltip: null,
|
||||
},
|
||||
],
|
||||
pageComponents: [
|
||||
{
|
||||
component: "wizardEncoderCal",
|
||||
id: 0,
|
||||
data: {
|
||||
axis: "axis1",
|
||||
}
|
||||
},
|
||||
{
|
||||
component: "wizardCalStatus",
|
||||
id: 1,
|
||||
data: {
|
||||
success: "Calibration succeeded!",
|
||||
failure: "Calibration failed",
|
||||
tooltip: "Double check your encoder connections. If the connections are good, you either \
|
||||
have a wrong setting for motor pole pairs or encoder cpr."
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
Control_1: {
|
||||
title: "Control mode for Axis 1",
|
||||
link: "Control Modes 1",
|
||||
component: "wizardPage",
|
||||
next: "Misc_1",
|
||||
back: "Encoder_1",
|
||||
nextTooltip: "Make a choice!",
|
||||
choiceMade: false,
|
||||
choices: [
|
||||
{
|
||||
component: "wizardChoice",
|
||||
data: {
|
||||
imageURL: require("../images/temp.png"),
|
||||
configStub: {
|
||||
axis1: {
|
||||
controller: {
|
||||
config: {
|
||||
control_mode: odriveEnums.CONTROL_MODE_POSITION_CONTROL,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
title: "Position Control",
|
||||
requirements: [],
|
||||
hooks: [],
|
||||
tooltip: "Use this mode to control the position of the motor",
|
||||
altTooltip: null,
|
||||
},
|
||||
{
|
||||
component: "wizardChoice",
|
||||
data: {
|
||||
imageURL: require("../images/temp.png"),
|
||||
configStub: {
|
||||
axis1: {
|
||||
controller: {
|
||||
config: {
|
||||
control_mode: odriveEnums.CONTROL_MODE_VELOCITY_CONTROL,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
title: "Velocity Control",
|
||||
requirements: [],
|
||||
hooks: [],
|
||||
tooltip: "Use this mode to control the velocity of the motor",
|
||||
altTooltip: null,
|
||||
},
|
||||
{
|
||||
component: "wizardChoice",
|
||||
data: {
|
||||
imageURL: require("../images/temp.png"),
|
||||
configStub: {
|
||||
axis1: {
|
||||
controller: {
|
||||
config: {
|
||||
control_mode: odriveEnums.CONTROL_MODE_TORQUE_CONTROL,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
title: "Torque Control",
|
||||
requirements: [],
|
||||
hooks: [],
|
||||
tooltip: "Use this mode to control the torque output of the motor",
|
||||
altTooltip: null,
|
||||
},
|
||||
{
|
||||
component: "wizardChoice",
|
||||
data: {
|
||||
imageURL: require("../images/temp.png"),
|
||||
configStub: {
|
||||
axis1: {
|
||||
controller: {
|
||||
config: {
|
||||
control_mode: odriveEnums.CONTROL_MODE_VOLTAGE_CONTROL,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
title: "Voltage Control",
|
||||
requirements: [],
|
||||
hooks: [],
|
||||
tooltip: "This mode is used for gimbal motors",
|
||||
altTooltip: null,
|
||||
},
|
||||
],
|
||||
customComponents: [],
|
||||
pageComponents: [],
|
||||
},
|
||||
/*
|
||||
Input_1: {
|
||||
title: "Input mode selection for Axis 1",
|
||||
link: "Input Modes 1",
|
||||
component: "wizardPage",
|
||||
next: "Misc_1",
|
||||
back: "Control_1",
|
||||
nextTooltip: "Make a choice!",
|
||||
choiceMade: false,
|
||||
choices: [
|
||||
{
|
||||
component: "wizardChoice",
|
||||
data: {
|
||||
imageURL: require("../images/temp.png"),
|
||||
configStub: {
|
||||
axis1: {
|
||||
controller: {
|
||||
config: {
|
||||
input_mode: odriveEnums.INPUT_MODE_PASSTHROUGH,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
title: "Input Passthrough",
|
||||
requirements: [],
|
||||
hooks: [],
|
||||
tooltip: "This is the default input mode. Input commands are passed to the controller without modification.",
|
||||
altTooltip: null,
|
||||
},
|
||||
{
|
||||
component: "wizardInputFiltered",
|
||||
data: {
|
||||
axis: "axis1",
|
||||
},
|
||||
title: "Position Input Filtering",
|
||||
requirements: [
|
||||
(configObj) => {
|
||||
return configObj.axis1.controller.config.control_mode == odriveEnums.CONTROL_MODE_POSITION_CONTROL;
|
||||
},
|
||||
],
|
||||
hooks: [],
|
||||
tooltip: "Filtered position mode. This provides smoother motion than the passthrough mode. \
|
||||
Input position commands are sent through a 2nd order filter. A higher bandwidth will cause quicker position changes.",
|
||||
altTooltip: "Control mode must be Position Control for this input mode",
|
||||
},
|
||||
{
|
||||
component: "wizardInputVelRamp",
|
||||
data: {
|
||||
axis: "axis1",
|
||||
},
|
||||
title: "Velocity Input Ramping",
|
||||
requirements: [
|
||||
(configObj) => {
|
||||
return configObj.axis1.controller.config.control_mode == odriveEnums.CONTROL_MODE_VELOCITY_CONTROL;
|
||||
},
|
||||
],
|
||||
hooks: [],
|
||||
tooltip: "For velocity control mode, you can choose to have the input velocities ramped between values. vel_ramp_rate is the \
|
||||
acceleration between velocities.",
|
||||
altTooltip: "Control mode must be Velocity Control for this input mode",
|
||||
},
|
||||
{
|
||||
component: "wizardInputTrajectory",
|
||||
data: {
|
||||
axis: "axis1",
|
||||
},
|
||||
title: "Trajectory Planning",
|
||||
requirements: [
|
||||
(configObj) => {
|
||||
return configObj.axis1.controller.config.control_mode == odriveEnums.CONTROL_MODE_POSITION_CONTROL;
|
||||
},
|
||||
],
|
||||
hooks: [],
|
||||
tooltip: "This mode lets you smoothly accelerate, coast, and decelerate from one position to another. vel_limit is your coasting speed, \
|
||||
accel_limit is the maximum acceleration in turns/s^2, decel_limit is the maximum decelaration in turns/s^2 and inertia is an \
|
||||
optional value to correlate acceleration with motor torque.",
|
||||
altTooltip: "Control most must be Position Control for this input mode",
|
||||
}
|
||||
],
|
||||
pageComponents: [],
|
||||
},
|
||||
*/
|
||||
Misc_1: {
|
||||
title: "Finishing touches for Axis 1",
|
||||
link: "Misc 1",
|
||||
component: "wizardPage",
|
||||
next: "End",
|
||||
back: "Control_1",
|
||||
nextTooltip: "Make a choice!",
|
||||
choiceMade: false,
|
||||
choices: [
|
||||
{
|
||||
component: "wizardMisc",
|
||||
data: {
|
||||
axis: "axis1",
|
||||
},
|
||||
title: "Miscellaneous Parameters",
|
||||
requirements: [],
|
||||
hooks: [],
|
||||
tooltip: null,
|
||||
altTooltip: null,
|
||||
},
|
||||
],
|
||||
pageComponents: [],
|
||||
},
|
||||
End: {
|
||||
title: "Review choices",
|
||||
link: "Review and Apply",
|
||||
component: "wizardPage",
|
||||
next: "End",
|
||||
back: "Misc_1",
|
||||
choiceMade: false,
|
||||
choices: [
|
||||
{
|
||||
component: "wizardEnd",
|
||||
data: {},
|
||||
title: null,
|
||||
requirements: [],
|
||||
hooks: [],
|
||||
tooltip: "Old config values are on the left, new config values are on the right in green",
|
||||
altTooltip: null,
|
||||
}
|
||||
],
|
||||
pageComponents: [],
|
||||
}
|
||||
}
|
||||
|
||||
export let enumVars = {
|
||||
// for wizardEnd.vue
|
||||
motor_type: {
|
||||
0: "MOTOR_TYPE_HIGH_CURRENT",
|
||||
2: "MOTOR_TYPE_GIMBAL",
|
||||
3: "MOTOR_TYPE_ACIM",
|
||||
},
|
||||
mode: {
|
||||
0: "ENCODER_MODE_INCREMENTAL",
|
||||
1: "ENCODER_MODE_HALL",
|
||||
2: "ENCODER_MODE_SINCOS",
|
||||
256: "ENCODER_MODE_SPI_ABS_CUI",
|
||||
257: "ENCODER_MODE_SPI_ABS_AMS",
|
||||
258: "ENCODER_MODE_SPI_ABS_AEAT",
|
||||
259: "ENCODER_MODE_SPI_ABS_RLS",
|
||||
},
|
||||
input_mode: {
|
||||
0: "INPUT_MODE_INACTIVE",
|
||||
1: "INPUT_MODE_PASSTHROUGH",
|
||||
2: "INPUT_MODE_VEL_RAMP",
|
||||
3: "INPUT_MODE_POS_FILTER",
|
||||
4: "INPUT_MODE_MIX_CHANNELS",
|
||||
5: "INPUT_MODE_TRAP_TRAJ",
|
||||
6: "INPUT_MODE_TORQUE_RAMP",
|
||||
7: "INPUT_MODE_MIRROR",
|
||||
},
|
||||
control_mode: {
|
||||
0: "CONTROL_MODE_VOLTAGE_CONTROL",
|
||||
1: "CONTROL_MODE_TORQUE_CONTROL",
|
||||
2: "CONTROL_MODE_VELOCITY_CONTROL",
|
||||
3: "CONTROL_MODE_POSITION_CONTROL",
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
'use strict'
|
||||
|
||||
import { app, protocol, BrowserWindow, ipcMain } from 'electron'
|
||||
import { createProtocol } from 'vue-cli-plugin-electron-builder/lib'
|
||||
import installExtension, { VUEJS_DEVTOOLS } from 'electron-devtools-installer'
|
||||
const isDevelopment = process.env.NODE_ENV !== 'production'
|
||||
|
||||
const { spawnSync, execSync } = require('child_process');
|
||||
const spawn = require('child_process').spawn;
|
||||
const path = require('path');
|
||||
|
||||
// Keep a global reference of the window object, if you don't, the window will
|
||||
// be closed automatically when the JavaScript object is garbage collected.
|
||||
let win;
|
||||
|
||||
// var for python server
|
||||
let server;
|
||||
|
||||
// Scheme must be registered before the app is ready
|
||||
protocol.registerSchemesAsPrivileged([
|
||||
{ scheme: 'app', privileges: { secure: true, standard: true } }
|
||||
])
|
||||
|
||||
// function to get determine correct command for python
|
||||
function getPyCmd() {
|
||||
// call both 'python' and 'python3' to figure out the correct command
|
||||
let spawnRet = spawnSync('python', ['-V']);
|
||||
let success = spawnRet.status != null;
|
||||
let outputString;
|
||||
let cmd = '';
|
||||
if (success) {
|
||||
if (spawnRet.stdout.toString().length > 1) {
|
||||
outputString = spawnRet.stdout.toString();
|
||||
}
|
||||
else {
|
||||
outputString = spawnRet.stderr.toString();
|
||||
}
|
||||
if (outputString.includes("Python 3")) {
|
||||
cmd = 'python';
|
||||
}
|
||||
}
|
||||
if (cmd == '') {
|
||||
spawnRet = spawnSync('python3', ['-V']);
|
||||
success = spawnRet.status != null;
|
||||
if (success) {
|
||||
if (spawnRet.stdout.toString().length > 1) {
|
||||
outputString = spawnRet.stdout.toString();
|
||||
}
|
||||
else {
|
||||
outputString = spawnRet.stderr.toString();
|
||||
}
|
||||
if (outputString.includes("Python 3")) {
|
||||
cmd = 'python3';
|
||||
}
|
||||
}
|
||||
}
|
||||
return cmd;
|
||||
}
|
||||
|
||||
function createWindow() {
|
||||
|
||||
// Create the browser window.
|
||||
win = new BrowserWindow({
|
||||
width: 800,
|
||||
height: 600,
|
||||
webPreferences: {
|
||||
// Use pluginOptions.nodeIntegration, leave this alone
|
||||
// See nklayman.github.io/vue-cli-plugin-electron-builder/guide/security.html#node-integration for more info
|
||||
nodeIntegration: process.env.ELECTRON_NODE_INTEGRATION,
|
||||
preload: path.join(__dirname, 'preload.js')
|
||||
},
|
||||
icon: path.join(__static, 'icon.png')
|
||||
})
|
||||
win.maximize();
|
||||
win.setMenu(null);
|
||||
|
||||
if (process.env.WEBPACK_DEV_SERVER_URL) {
|
||||
// Load the url of the dev server if in development mode
|
||||
win.loadURL(process.env.WEBPACK_DEV_SERVER_URL)
|
||||
if (!process.env.IS_TEST) win.webContents.openDevTools()
|
||||
} else {
|
||||
createProtocol('app')
|
||||
// Load the index.html when not in development
|
||||
win.loadURL('app://./index.html')
|
||||
}
|
||||
|
||||
win.on('closed', () => {
|
||||
win = null
|
||||
})
|
||||
}
|
||||
|
||||
// Quit when all windows are closed.
|
||||
app.on('window-all-closed', () => {
|
||||
// kill flask server
|
||||
if (process.platform !== 'win32') {
|
||||
execSync('kill $(ps aux | grep \'[o]drive_server.py\' | awk \'{print $2}\')');
|
||||
console.log("killed flask server");
|
||||
}
|
||||
// On macOS it is common for applications and their menu bar
|
||||
// to stay active until the user quits explicitly with Cmd + Q
|
||||
if (process.platform !== 'darwin') {
|
||||
app.quit()
|
||||
}
|
||||
})
|
||||
|
||||
app.on('activate', () => {
|
||||
// On macOS it's common to re-create a window in the app when the
|
||||
// dock icon is clicked and there are no other windows open.
|
||||
if (win === null) {
|
||||
createWindow()
|
||||
}
|
||||
})
|
||||
|
||||
// This method will be called when Electron has finished
|
||||
// initialization and is ready to create browser windows.
|
||||
// Some APIs can only be used after this event occurs.
|
||||
app.on('ready', async () => {
|
||||
if (isDevelopment && !process.env.IS_TEST) {
|
||||
// Install Vue Devtools
|
||||
try {
|
||||
await installExtension(VUEJS_DEVTOOLS)
|
||||
} catch (e) {
|
||||
console.error('Vue Devtools failed to install:', e.toString())
|
||||
}
|
||||
}
|
||||
createWindow()
|
||||
// Figure out correct path and arguments to launch python server
|
||||
let scriptFilename = path.join(app.getAppPath(), '../server', 'odrive_server.py');
|
||||
const args = process.argv;
|
||||
let effectiveCommand = [];
|
||||
effectiveCommand.push(scriptFilename);
|
||||
if (app.isPackaged === true) {
|
||||
for (const arg of args.slice(1)) {
|
||||
effectiveCommand.push(arg);
|
||||
}
|
||||
}
|
||||
else {
|
||||
for (const arg of args.slice(2)) {
|
||||
effectiveCommand.push(arg);
|
||||
}
|
||||
}
|
||||
// launch python server on event from renderer process (gui) and pipe stdout/stderr to it
|
||||
ipcMain.on('start-server', () => {
|
||||
server = spawn(getPyCmd(), effectiveCommand);
|
||||
server.stdout.on('data', function (data) {
|
||||
try {
|
||||
console.log(data.toString('utf8'));
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
try {
|
||||
win.webContents.send('server-stdout', String(data.toString('utf8')));
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
});
|
||||
server.stderr.on('data', function (data) {
|
||||
console.log(data.toString('utf8'));
|
||||
try {
|
||||
win.webContents.send('server-stderr', String(data.toString('utf8')));
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
});
|
||||
})
|
||||
ipcMain.on('kill-server', () => {
|
||||
server.kill('SIGINT');
|
||||
})
|
||||
})
|
||||
|
||||
// Exit cleanly on request from parent process in development mode.
|
||||
if (isDevelopment) {
|
||||
if (process.platform === 'win32') {
|
||||
process.on('message', (data) => {
|
||||
if (data === 'graceful-exit') {
|
||||
server.kill('SIGINT');
|
||||
app.quit()
|
||||
}
|
||||
})
|
||||
} else {
|
||||
process.on('SIGTERM', () => {
|
||||
server.kill('SIGINT');
|
||||
app.quit()
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import io from 'socket.io-client';
|
||||
|
||||
let socket = undefined;
|
||||
let url = 'https://0.0.0.0:8080';
|
||||
|
||||
function initSocket(url) {
|
||||
socket = io(url);
|
||||
}
|
||||
|
||||
export function setUrl(path) {
|
||||
if (socket) {
|
||||
socket.close();
|
||||
socket = undefined;
|
||||
}
|
||||
url = path;
|
||||
initSocket(url);
|
||||
}
|
||||
|
||||
export function closeSocket() {
|
||||
socket.close();
|
||||
socket = undefined;
|
||||
}
|
||||
|
||||
export function addEventListener(event) {
|
||||
if (!socket) {
|
||||
initSocket(url);
|
||||
}
|
||||
socket.on(event.type, event.callback);
|
||||
}
|
||||
|
||||
export function sendEvent(event) {
|
||||
socket.emit(event.type, event.data);
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
<template>
|
||||
<div
|
||||
class="axis"
|
||||
@click.self="showError = !showError"
|
||||
:class="{ inactive: !connected, noError: !error, error: error }"
|
||||
>
|
||||
{{ axis }}
|
||||
<div
|
||||
v-show="showError"
|
||||
class="error-popup card"
|
||||
@click.self="showError = !showError"
|
||||
>
|
||||
<clear-errors :data="{ axis: axis }" />
|
||||
axis:
|
||||
<span :class="{ noError: !axisError, error: axisError }">{{
|
||||
axisErrorMsg
|
||||
}}</span>
|
||||
<br />motor:
|
||||
<span :class="{ noError: !motorError, error: motorError }">{{
|
||||
motorErrorMsg
|
||||
}}</span>
|
||||
<br />encoder:
|
||||
<span :class="{ noError: !encoderError, error: encoderError }">{{
|
||||
encoderErrorMsg
|
||||
}}</span>
|
||||
<br />controller:
|
||||
<span :class="{ noError: !controllerError, error: controllerError }">{{
|
||||
controllerErrorMsg
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import odriveEnums from "../assets/odriveEnums.json";
|
||||
import clearErrors from "./clearErrors.vue";
|
||||
import { getVal, fetchParam } from "../lib/odrive_utils";
|
||||
|
||||
export default {
|
||||
name: "Axis",
|
||||
components: {
|
||||
clearErrors,
|
||||
},
|
||||
props: ["axis", "odrives"],
|
||||
data() {
|
||||
return {
|
||||
showError: false,
|
||||
axisErr: undefined,
|
||||
motorErr: undefined,
|
||||
controllerErr: undefined,
|
||||
encoderErr: undefined,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
getErrorString(errCode, errType) {
|
||||
let retMsg = "none";
|
||||
|
||||
if (errCode != 0) {
|
||||
// we got an error!
|
||||
// mask errors depending on which page is active
|
||||
let errs = [];
|
||||
for (const [name, value] of Object.entries(odriveEnums)) {
|
||||
if (name.includes(errType) && errCode & value) {
|
||||
errs.push(name);
|
||||
}
|
||||
}
|
||||
retMsg = errs.join(", ");
|
||||
}
|
||||
|
||||
return retMsg;
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
connected() {
|
||||
return this.$store.state.ODrivesConnected[this.axis.split(".")[0]];
|
||||
},
|
||||
axisErrorMsg() {
|
||||
let errCode = this.axisErr;
|
||||
|
||||
if (this.$store.state.currentDash == "Wizard") {
|
||||
errCode &= ~odriveEnums.AXIS_ERROR_MOTOR_FAILED;
|
||||
errCode &= ~odriveEnums.AXIS_ERROR_ENCODER_FAILED;
|
||||
}
|
||||
|
||||
return this.getErrorString(errCode, 'AXIS_ERROR');
|
||||
},
|
||||
motorErrorMsg() {
|
||||
let errCode = this.motorErr;
|
||||
|
||||
if (this.$store.state.currentDash == "Wizard") {
|
||||
errCode &= ~odriveEnums.MOTOR_ERROR_PHASE_RESISTANCE_OUT_OF_RANGE;
|
||||
errCode &= ~odriveEnums.MOTOR_ERROR_PHASE_INDUCTANCE_OUT_OF_RANGE;
|
||||
}
|
||||
|
||||
return this.getErrorString(errCode, 'MOTOR_ERROR');
|
||||
},
|
||||
encoderErrorMsg() {
|
||||
let errCode = this.encoderErr;
|
||||
|
||||
if (this.$store.state.currentDash == "Wizard") {
|
||||
errCode &= ~odriveEnums.ENCODER_ERROR_CPR_POLEPAIRS_MISMATCH;
|
||||
errCode &= ~odriveEnums.ENCODER_ERROR_NO_RESPONSE;
|
||||
}
|
||||
|
||||
return this.getErrorString(errCode, 'ENCODER_ERROR');
|
||||
},
|
||||
controllerErrorMsg() {
|
||||
let errCode = this.controllerErr;
|
||||
|
||||
return this.getErrorString(errCode, 'CONTROLLER_ERROR');
|
||||
},
|
||||
axisError() {
|
||||
let errCode = this.axisErr;
|
||||
if (this.$store.state.currentDash == "Wizard") {
|
||||
errCode &= ~odriveEnums.AXIS_ERROR_MOTOR_FAILED;
|
||||
errCode &= ~odriveEnums.AXIS_ERROR_ENCODER_FAILED;
|
||||
}
|
||||
return errCode != 0;
|
||||
},
|
||||
motorError() {
|
||||
let errCode = this.motorErr;
|
||||
if (this.$store.state.currentDash == "Wizard") {
|
||||
errCode &= ~odriveEnums.MOTOR_ERROR_PHASE_RESISTANCE_OUT_OF_RANGE;
|
||||
errCode &= ~odriveEnums.MOTOR_ERROR_PHASE_INDUCTANCE_OUT_OF_RANGE;
|
||||
}
|
||||
return errCode != 0;
|
||||
},
|
||||
encoderError() {
|
||||
let errCode = this.encoderErr;
|
||||
if (this.$store.state.currentDash == "Wizard") {
|
||||
errCode &= ~odriveEnums.ENCODER_ERROR_CPR_POLEPAIRS_MISMATCH;
|
||||
errCode &= ~odriveEnums.ENCODER_ERROR_NO_RESPONSE;
|
||||
}
|
||||
return errCode != 0;
|
||||
},
|
||||
controllerError() {
|
||||
let errCode = this.controllerErr;
|
||||
return errCode != 0;
|
||||
},
|
||||
error() {
|
||||
return (
|
||||
this.axisError ||
|
||||
this.motorError ||
|
||||
this.encoderError ||
|
||||
this.controllerError
|
||||
);
|
||||
},
|
||||
},
|
||||
created() {
|
||||
// set up timeout loop for grabbing axis error values
|
||||
let update = () => {
|
||||
// Do we have an active connection to the ODrive that contains this axis?
|
||||
if (this.$store.state.ODrivesConnected[this.axis.split(".")[0]]) {
|
||||
fetchParam(this.axis + ".error");
|
||||
fetchParam(this.axis + ".motor.error");
|
||||
fetchParam(this.axis + ".controller.error");
|
||||
fetchParam(this.axis + ".encoder.error");
|
||||
this.axisErr = getVal(this.axis + ".error");
|
||||
this.motorErr = getVal(this.axis + ".motor.error");
|
||||
this.controllerErr = getVal(this.axis + ".controller.error");
|
||||
this.encoderErr = getVal(this.axis + ".encoder.error");
|
||||
}
|
||||
};
|
||||
this.cyclicUpdate = setInterval(update, 1000);
|
||||
},
|
||||
beforeDestroy() {
|
||||
clearInterval(this.cyclicUpdate);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.axis {
|
||||
padding: 5px 10px;
|
||||
border: 2px black;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.axis:active {
|
||||
background-color: var(--bg-color);
|
||||
}
|
||||
|
||||
.noError {
|
||||
color: green;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: red;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.error-popup {
|
||||
position: absolute;
|
||||
bottom: 2rem;
|
||||
color: black;
|
||||
margin-left: 0px;
|
||||
}
|
||||
|
||||
.inactive {
|
||||
color: grey;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,101 @@
|
||||
<template>
|
||||
<div class="card action-card">
|
||||
<button class="close-button" @click=deleteAction>X</button>
|
||||
<span class="ctrlName">{{shortPath}}:</span>
|
||||
<div class="right">
|
||||
<input v-on:change="newVal" :placeholder="initVal" :value="valueDisplay" spellcheck="false"/>
|
||||
<button class="action-button close-button" @click="putVal">Send</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { putVal, parseMath } from "../../lib/odrive_utils.js";
|
||||
import { numberDisplay } from "../../lib/utils.js"
|
||||
|
||||
export default {
|
||||
name: "Action",
|
||||
props: {
|
||||
id: String,
|
||||
path: String,
|
||||
initVal: Number,
|
||||
dashID: String
|
||||
},
|
||||
data: function () {
|
||||
return {
|
||||
value: 0,
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
if (this.initVal !== undefined) {
|
||||
this.value = this.initVal;
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
shortPath() {
|
||||
let keys = this.path.split('.');
|
||||
keys.shift();
|
||||
return keys.join('.');
|
||||
},
|
||||
valueDisplay() {
|
||||
return numberDisplay(this.value);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
newVal: function (e) {
|
||||
let val = parseMath(e.target.value);
|
||||
if (val !== false) {
|
||||
this.value = val;
|
||||
console.log("input = " + e.target.value + ", val = " + this.value);
|
||||
this.$store.commit("setActionVal", {dashID: this.dashID, actionID: this.id, val: this.value});
|
||||
}
|
||||
},
|
||||
putVal: function () {
|
||||
let keys = this.path.split(".");
|
||||
keys.shift();
|
||||
console.log('path = ' + keys.join('.') + ", val = " + this.value);
|
||||
putVal(keys.join('.'), this.value);
|
||||
},
|
||||
deleteAction: function() {
|
||||
// commit a mutation to remove this action from the dashboard
|
||||
this.$store.commit("removeActionFromDash", {dashID: this.dashID, actionID: this.id});
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
input {
|
||||
border-style: none;
|
||||
border-bottom: 1px solid grey;
|
||||
width: 5rem;
|
||||
margin-left: 0.5rem;
|
||||
margin-right: 0.5rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.action-card {
|
||||
display: flex;
|
||||
/* border: 1px solid lightcoral; */
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.action-button {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.right {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
input::-webkit-outer-spin-button,
|
||||
input::-webkit-inner-spin-button {
|
||||
/* display: none; <- Crashes Chrome on hover */
|
||||
-webkit-appearance: none;
|
||||
margin: 0; /* <-- Apparently some margin are still there even though it's hidden */
|
||||
}
|
||||
|
||||
input[type=number] {
|
||||
-moz-appearance:textfield; /* Firefox */
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,95 @@
|
||||
<template>
|
||||
<div class="card action-card">
|
||||
<button class="close-button" @click=deleteAction>X</button>
|
||||
<span class="ctrlName">{{shortPath}}:</span>
|
||||
<div class="right">
|
||||
<select class="action-enum" v-model="value" @change="newVal">
|
||||
<option v-for="option in options" :key="option.value" :value="option.value">
|
||||
{{ option.text }}
|
||||
</option>
|
||||
</select>
|
||||
<button class="action-button close-button" @click="putVal">Send</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { putVal } from "../../lib/odrive_utils.js";
|
||||
|
||||
export default {
|
||||
name: "ActionEnum",
|
||||
props: {
|
||||
id: String,
|
||||
path: String,
|
||||
initVal: Number,
|
||||
dashID: String,
|
||||
options: Array,
|
||||
},
|
||||
data: function () {
|
||||
return {
|
||||
value: 0,
|
||||
};
|
||||
},
|
||||
created() {
|
||||
if (this.initVal !== undefined) {
|
||||
this.value = this.initVal;
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
shortPath() {
|
||||
let keys = this.path.split('.');
|
||||
keys.shift();
|
||||
return keys.join('.');
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
newVal: function (e) {
|
||||
console.log(this.selected);
|
||||
this.value = parseFloat(e.target.value);
|
||||
this.$store.commit("setActionVal", {dashID: this.dashID, actionID: this.id, val: this.value});
|
||||
},
|
||||
putVal: function () {
|
||||
let keys = this.path.split(".");
|
||||
keys.shift();
|
||||
console.log('path = ' + keys.join('.') + ", val = " + this.value);
|
||||
putVal(keys.join('.'), this.value);
|
||||
},
|
||||
deleteAction: function() {
|
||||
// commit a mutation to remove this action from the dashboard
|
||||
this.$store.commit("removeActionFromDash", {dashID: this.dashID, actionID: this.id});
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
input {
|
||||
border-style: none;
|
||||
border-bottom: 1px solid grey;
|
||||
width: 5rem;
|
||||
margin-left: 0.5rem;
|
||||
margin-right: 0.5rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.action-card {
|
||||
display: flex;
|
||||
/* border: 1px solid lightcoral; */
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.action-button {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.right {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.action-enum {
|
||||
border: none;
|
||||
border-bottom: 1px solid grey;
|
||||
background-color: var(--fg-color);
|
||||
text-align-last: right;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,38 @@
|
||||
<template>
|
||||
<div>
|
||||
<button class="measure-button card" @click="clear">Clear Errors</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { putVal } from "../lib/odrive_utils.js";
|
||||
|
||||
export default {
|
||||
name: "clearErrors.vue",
|
||||
props: {
|
||||
data: Object,
|
||||
},
|
||||
methods: {
|
||||
clear() {
|
||||
// clear odrive errors
|
||||
// set them to 0?
|
||||
console.log(this.data);
|
||||
let paths = [
|
||||
this.data.axis + ".error",
|
||||
this.data.axis + ".motor.error",
|
||||
this.data.axis + ".encoder.error",
|
||||
this.data.axis + ".controller.error",
|
||||
];
|
||||
for (const path of paths) {
|
||||
putVal(path, 0);
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.measure-button:active {
|
||||
background-color: var(--bg-color);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,90 @@
|
||||
<template>
|
||||
<div class="card">
|
||||
<button class="close-button" @click=deleteCtrl>X</button>
|
||||
<span class="ctrlName">{{name}}:</span>
|
||||
<div class="right">
|
||||
<span class="ctrlVal">{{value}}</span>
|
||||
<input
|
||||
class="ctrlInput"
|
||||
v-if="writeAccess"
|
||||
type="checkbox"
|
||||
:value="value"
|
||||
:checked="value"
|
||||
@click="putVal"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getVal, getReadonly, putVal, fetchParam } from "../../lib/odrive_utils.js";
|
||||
|
||||
export default {
|
||||
name: "CtrlBoolean",
|
||||
//type checking here for properties
|
||||
props: {
|
||||
path: String,
|
||||
odrives: Object,
|
||||
dashID: String,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
value: function () {
|
||||
let keys = this.path.split(".");
|
||||
keys.shift(); // don't need first key here
|
||||
return getVal(keys.join('.'));
|
||||
},
|
||||
name: function () {
|
||||
let keys = this.path.split(".");
|
||||
keys.shift();
|
||||
return keys.join(".");
|
||||
},
|
||||
writeAccess: function () {
|
||||
let keys = this.path.split(".");
|
||||
keys.shift(); // don't need first key here
|
||||
return getReadonly(keys.join('.')) == false;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
putVal: function (e) {
|
||||
let keys = this.path.split('.');
|
||||
keys.shift();
|
||||
putVal(keys.join('.'), e.target.checked);
|
||||
fetchParam(keys.join('.'));
|
||||
},
|
||||
deleteCtrl: function() {
|
||||
// commit a mutation in the store with the relevant information
|
||||
this.$store.commit("removeCtrlFromDash", {dashID: this.dashID, path: this.path});
|
||||
}
|
||||
},
|
||||
created() {
|
||||
// update parameter value on component creation
|
||||
let keys = this.path.split('.');
|
||||
keys.shift();
|
||||
fetchParam(keys.join('.'));
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.ctrlVal {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.ctrlInput {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.right {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.card {
|
||||
display: flex;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,107 @@
|
||||
<template>
|
||||
<div class="card">
|
||||
<button class="close-button" @click=deleteCtrl>X</button>
|
||||
<span class="ctrlName">{{name}}:</span>
|
||||
<div class="right">
|
||||
<select class="ctrl-enum" v-model="selected" @change="putVal">
|
||||
<option v-for="option in options" :key="option.value" :value="option.value">
|
||||
{{ option.text }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getVal, getReadonly, putVal, fetchParam } from "../../lib/odrive_utils.js";
|
||||
|
||||
export default {
|
||||
name: "CtrlNumeric",
|
||||
//type checking here for properties
|
||||
props: {
|
||||
path: String,
|
||||
odrives: Object,
|
||||
dashID: String,
|
||||
options: Array,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
selected: undefined,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
value: function () {
|
||||
let keys = this.path.split('.');
|
||||
keys.shift();
|
||||
return parseFloat(getVal(keys.join('.')));
|
||||
},
|
||||
name: function () {
|
||||
let keys = this.path.split(".");
|
||||
keys.shift();
|
||||
return keys.join(".");
|
||||
},
|
||||
writeAccess: function () {
|
||||
let keys = this.path.split(".");
|
||||
keys.shift(); // don't need first key here
|
||||
return getReadonly(keys.join('.')) === false;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
value: function(newVal) {
|
||||
this.selected = newVal;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
putVal: function (e) {
|
||||
let keys = this.path.split('.');
|
||||
keys.shift();
|
||||
console.log("Calling putVal from CtrlEnum");
|
||||
putVal(keys.join('.'), parseFloat(e.target.value));
|
||||
},
|
||||
deleteCtrl: function() {
|
||||
// commit a mutation in the store with the relevant information
|
||||
this.$store.commit("removeCtrlFromDash", {dashID: this.dashID, path: this.path});
|
||||
},
|
||||
},
|
||||
created() {
|
||||
// update parameter value on component creation
|
||||
let keys = this.path.split('.');
|
||||
keys.shift();
|
||||
fetchParam(keys.join('.'));
|
||||
|
||||
this.selected = this.value;
|
||||
console.log(this.value);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.ctrlVal {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
input {
|
||||
width: 5rem;
|
||||
font-family: inherit;
|
||||
border-style: none;
|
||||
border-bottom: 1px solid grey;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.right {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.card {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.ctrl-enum {
|
||||
border: none;
|
||||
border-bottom: 1px solid grey;
|
||||
background-color: var(--fg-color);
|
||||
text-align-last: right;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,57 @@
|
||||
<template>
|
||||
<div class="card" @click.self="executeFunction">
|
||||
<button class="close-button" @click=deleteCtrl>X</button>
|
||||
<button class="execute" @click="executeFunction">{{name}}()</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { callFcn } from "../../lib/odrive_utils.js";
|
||||
|
||||
export default {
|
||||
name: "CtrlFunction",
|
||||
//type checking here for properties
|
||||
props: {
|
||||
path: String,
|
||||
odrives: Object,
|
||||
dashID: String,
|
||||
},
|
||||
computed: {
|
||||
name: function() {
|
||||
let keys = this.path.split(".");
|
||||
keys.shift();
|
||||
return keys.join(".");
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
executeFunction: function() {
|
||||
//execute this function on the odrive
|
||||
let keys = this.path.split(".");
|
||||
keys.shift();
|
||||
callFcn(keys.join('.'));
|
||||
},
|
||||
deleteCtrl: function() {
|
||||
// commit a mutation in the store with the relevant information
|
||||
this.$store.commit("removeCtrlFromDash", {dashID: this.dashID, path: this.path});
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.card {
|
||||
background-color: lightcyan;
|
||||
}
|
||||
|
||||
.execute {
|
||||
color: black;
|
||||
font-family: "Roboto Mono", monospace;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background-color: rgba(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
.card:active {
|
||||
background-color: lightblue;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,109 @@
|
||||
<template>
|
||||
<div class="card">
|
||||
<button class="close-button" @click=deleteCtrl>X</button>
|
||||
<span class="ctrlName">{{name}}:</span>
|
||||
<div class="right">
|
||||
<span v-if="!writeAccess" class="ctrlVal">{{value}}</span>
|
||||
<input v-if="writeAccess" :placeholder="value" v-on:change="putVal" :value="value" spellcheck="false"/>
|
||||
<!-- <span class="unit">[{{unit}}]</span> -->
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getVal, getReadonly, putVal, fetchParam, getUnit, parseMath } from "../../lib/odrive_utils.js";
|
||||
import { numberDisplay } from "../../lib/utils.js";
|
||||
|
||||
export default {
|
||||
name: "CtrlNumeric",
|
||||
//type checking here for properties
|
||||
props: {
|
||||
path: String,
|
||||
odrives: Object,
|
||||
dashID: String
|
||||
},
|
||||
computed: {
|
||||
value: function () {
|
||||
let keys = this.path.split('.');
|
||||
keys.shift();
|
||||
let val = getVal(keys.join('.'));
|
||||
return numberDisplay(val);
|
||||
},
|
||||
name: function () {
|
||||
let keys = this.path.split(".");
|
||||
keys.shift();
|
||||
return keys.join(".");
|
||||
},
|
||||
writeAccess: function () {
|
||||
let keys = this.path.split(".");
|
||||
keys.shift(); // don't need first key here
|
||||
return getReadonly(keys.join('.')) === false;
|
||||
},
|
||||
unit() {
|
||||
let keys = this.path.split(".");
|
||||
keys.shift();
|
||||
return getUnit(this.$store.state.odrives.odrive0,keys.join('.'));
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
putVal: function (e) {
|
||||
let keys = this.path.split('.');
|
||||
keys.shift();
|
||||
console.log("input recieved: " + e.target.value);
|
||||
let val = parseMath(e.target.value);
|
||||
if (val !== false) {
|
||||
putVal(keys.join('.'), val);
|
||||
}
|
||||
},
|
||||
deleteCtrl: function() {
|
||||
// commit a mutation in the store with the relevant information
|
||||
this.$store.commit("removeCtrlFromDash", {dashID: this.dashID, path: this.path});
|
||||
}
|
||||
},
|
||||
created() {
|
||||
// update parameter value on component creation
|
||||
let keys = this.path.split('.');
|
||||
keys.shift();
|
||||
fetchParam(keys.join('.'));
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.ctrlVal {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
input {
|
||||
width: 5rem;
|
||||
font-family: inherit;
|
||||
border-style: none;
|
||||
border-bottom: 1px solid grey;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.right {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.card {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
input::-webkit-outer-spin-button,
|
||||
input::-webkit-inner-spin-button {
|
||||
/* display: none; <- Crashes Chrome on hover */
|
||||
-webkit-appearance: none;
|
||||
margin: 0; /* <-- Apparently some margin are still there even though it's hidden */
|
||||
}
|
||||
|
||||
input[type=number] {
|
||||
-moz-appearance:textfield; /* Firefox */
|
||||
}
|
||||
|
||||
.unit {
|
||||
font-family: "Roboto Mono", monospace;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,151 @@
|
||||
<template>
|
||||
<div class="card">
|
||||
<div>
|
||||
<button class="close-button" @click=deleteCtrl>X</button>
|
||||
<span class="ctrlName">{{name}}</span>
|
||||
</div>
|
||||
<div class="slider-container">
|
||||
<input :value="displayMin" :placeholder="displayMin" v-on:change="setMin"/>
|
||||
<!-- <vue-slider v-model="value" :min="min" :max="max" :interval="interval" /> -->
|
||||
<vue-slider v-model="value" :data="data" :adsorb="true" @change="putVal"/>
|
||||
<input :value="displayMax" :placeholder="displayMax" v-on:change="setMax" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import VueSlider from "vue-slider-component";
|
||||
import "vue-slider-component/theme/default.css";
|
||||
import { getVal, putVal, parseMath } from "../../lib/odrive_utils.js";
|
||||
import { numberDisplay } from "../../lib/utils.js";
|
||||
|
||||
export default {
|
||||
name: "CtrlSlider",
|
||||
components: {
|
||||
VueSlider,
|
||||
},
|
||||
//type checking here for properties
|
||||
props: {
|
||||
path: String,
|
||||
odrives: Object,
|
||||
dashID: String,
|
||||
},
|
||||
data: function () {
|
||||
return {
|
||||
value: 0,
|
||||
min: 0,
|
||||
max: 1,
|
||||
data: []
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
name: function () {
|
||||
let keys = this.path.split(".");
|
||||
keys.shift();
|
||||
return keys.join(".");
|
||||
},
|
||||
interval: function () {
|
||||
return (this.max - this.min) / 100;
|
||||
},
|
||||
sliderData: function () {
|
||||
let interval = (this.max - this.min) / 100;
|
||||
return Array.from(Array(101), (_, i) => this.min + interval * i);
|
||||
},
|
||||
displayMin() {
|
||||
return numberDisplay(this.min);
|
||||
},
|
||||
displayMax() {
|
||||
return numberDisplay(this.max);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
putVal: function (value) {
|
||||
console.log(value);
|
||||
let keys = this.path.split(".");
|
||||
keys.shift();
|
||||
putVal(keys.join('.'), value);
|
||||
},
|
||||
setMin: function (e) {
|
||||
let min = parseMath(e.target.value);
|
||||
if (min < this.max) {
|
||||
this.min = min;
|
||||
this.data = Array.from(Array(101), (_, i) => this.min + (this.max-this.min) / 100 * i);
|
||||
this.value = this.findNearest(this.data, this.value);
|
||||
}
|
||||
},
|
||||
setMax: function (e) {
|
||||
let max = parseMath(e.target.value);
|
||||
if (max > this.min) {
|
||||
this.max = max;
|
||||
this.data = Array.from(Array(101), (_, i) => this.min + (this.max-this.min) / 100 * i);
|
||||
this.value = this.findNearest(this.data, this.value);
|
||||
}
|
||||
},
|
||||
deleteCtrl: function() {
|
||||
// commit a mutation in the store with the relevant information
|
||||
this.$store.commit("removeCtrlFromDash", {dashID: this.dashID, path: this.path});
|
||||
},
|
||||
findNearest(data, value) {
|
||||
// find item in data closest to value
|
||||
let diff = Number.POSITIVE_INFINITY;
|
||||
let retVal = value;
|
||||
for (const val of data) {
|
||||
if (Math.abs(val - value) < diff) {
|
||||
diff = Math.abs(val - value);
|
||||
retVal = val;
|
||||
}
|
||||
}
|
||||
return retVal;
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
let initVal = () => {
|
||||
let keys = this.path.split(".");
|
||||
keys.shift(); // don't need first key here
|
||||
return parseFloat(getVal(keys.join('.')));
|
||||
};
|
||||
this.value = initVal();
|
||||
this.max = this.value * 4;
|
||||
this.min = this.value / 4;
|
||||
this.data = Array.from(Array(101), (_, i) => this.min + (this.max-this.min) / 100 * i);
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.ctrlVal {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.slider-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin-top: 0.4rem;
|
||||
}
|
||||
|
||||
.vue-slider {
|
||||
flex-grow: 3;
|
||||
margin-left: 0.4rem;
|
||||
margin-right: 0.4rem;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
input {
|
||||
width: 5rem;
|
||||
font-family: inherit;
|
||||
border-style: none;
|
||||
border-bottom: 1px solid grey;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
input::-webkit-outer-spin-button,
|
||||
input::-webkit-inner-spin-button {
|
||||
/* display: none; <- Crashes Chrome on hover */
|
||||
-webkit-appearance: none;
|
||||
margin: 0; /* <-- Apparently some margin are still there even though it's hidden */
|
||||
}
|
||||
|
||||
input[type=number] {
|
||||
-moz-appearance:textfield; /* Firefox */
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,14 @@
|
||||
import { Line, mixins } from 'vue-chartjs';
|
||||
import 'chartjs-plugin-streaming';
|
||||
const { reactiveProp } = mixins;
|
||||
|
||||
export default {
|
||||
extends: Line,
|
||||
mixins: [reactiveProp],
|
||||
props: ['options'],
|
||||
mounted () {
|
||||
// this.chartData is created in the mixin.
|
||||
// If you want to pass options please create a local options object
|
||||
this.renderChart(this.chartData, this.options)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
<template>
|
||||
<div class="card plot">
|
||||
<div class="plot-header">
|
||||
<button class="close-button" @click="deletePlot">X</button>
|
||||
<button class="close-button" @click="exportCSV">Export</button>
|
||||
<button class="close-button" @click="$emit('add-var', name)">+</button>
|
||||
</div>
|
||||
<line-chart v-if="loaded" :chart-data="datacollection" :options="dataOptions"></line-chart>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import LineChart from "./LineChart.js";
|
||||
import { saveAs } from "file-saver";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
LineChart,
|
||||
},
|
||||
props: ["name", "plot", "dashID"],
|
||||
data() {
|
||||
return {
|
||||
datacollection: null,
|
||||
timeStart: null,
|
||||
loaded: false,
|
||||
dataOptions: {
|
||||
animation: {
|
||||
duration: 0, // general animation time
|
||||
},
|
||||
events: [],
|
||||
hover: {
|
||||
animationDuration: 0, // duration of animations when hovering an item
|
||||
},
|
||||
responsiveAnimationDuration: 0, // animation duration after a resize
|
||||
elements: {
|
||||
point: {
|
||||
radius: 0,
|
||||
},
|
||||
line: {
|
||||
// borderColor: "rbga(0,0,0,0)",
|
||||
fill: false,
|
||||
borderWidth: 0,
|
||||
tension: 0,
|
||||
},
|
||||
},
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
},
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.timeStart = Date.now();
|
||||
//this.initData(); //set label for dataset and color
|
||||
this.cyclicUpdate = setInterval(() => this.fillData(), 50);
|
||||
},
|
||||
beforeDestroy() {
|
||||
clearInterval(this.cyclicUpdate);
|
||||
},
|
||||
methods: {
|
||||
fillData() {
|
||||
let newData = {
|
||||
labels: this.$store.state.propSamples["time"],
|
||||
datasets: [],
|
||||
};
|
||||
for (const plotVar of this.plot.vars) {
|
||||
let newPath = plotVar.path.split(".");
|
||||
newPath.splice(0, 1);
|
||||
newPath = newPath.join(".");
|
||||
newData.datasets.push({
|
||||
label: newPath,
|
||||
borderColor: plotVar.color, //"rgba(0,0,0,0)",
|
||||
data: this.$store.state.propSamples[newPath],
|
||||
});
|
||||
}
|
||||
this.datacollection = newData;
|
||||
this.loaded = true;
|
||||
},
|
||||
initData() {
|
||||
this.datacollection = {
|
||||
labels: [0],
|
||||
datasets: [
|
||||
{
|
||||
label: "Sine wave",
|
||||
backgroundColor: "rgba(0,0,0,0)", //"#f87979",
|
||||
data: [0],
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
deletePlot: function () {
|
||||
// commit a mutation in the store with the relevant information
|
||||
this.$store.commit("removePlotFromDash", {
|
||||
dashID: this.dashID,
|
||||
plotID: this.name,
|
||||
});
|
||||
},
|
||||
exportCSV: function () {
|
||||
// make a sensible data structure
|
||||
let csvData = {};
|
||||
csvData["time"] = this.$store.state.propSamples["time"];
|
||||
for (const dataset of this.datacollection.datasets) {
|
||||
csvData[dataset.label] = dataset.data;
|
||||
}
|
||||
let csvString = "";
|
||||
let dataKeys = Object.keys(csvData);
|
||||
// turn data structure into a string that's suitable for exporting
|
||||
for (const label of dataKeys) {
|
||||
csvString += label;
|
||||
if (label != dataKeys.slice(-1)) {
|
||||
csvString += ",";
|
||||
} else {
|
||||
csvString += "\n";
|
||||
}
|
||||
}
|
||||
for (let idx = 0; idx < csvData["time"].length; idx = idx + 1) {
|
||||
for (const label of dataKeys) {
|
||||
csvString += csvData[label][idx];
|
||||
if (label != dataKeys.slice(-1)) {
|
||||
csvString += ",";
|
||||
} else {
|
||||
csvString += "\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
var blob = new Blob([csvString], { type: "text/plain;charset=utf-8" });
|
||||
saveAs(blob, "plot.csv");
|
||||
|
||||
console.log("exporting plot");
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
div {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.plot {
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.plot-header {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.plotname {
|
||||
flex-grow: 10;
|
||||
margin: auto 0;
|
||||
}
|
||||
|
||||
.delete {
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
padding: 0 5px;
|
||||
margin-right: 10px;
|
||||
border: 1px solid black;
|
||||
}
|
||||
|
||||
.add-var {
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
padding: 0 5px;
|
||||
margin-right: 10px;
|
||||
border: 1px solid black;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,91 @@
|
||||
<template>
|
||||
<div class="card wizard-choice" :class="{ 'choice-inactive': !allowed }">
|
||||
<div class="left">
|
||||
<span>Brake resistor value = </span>
|
||||
<input v-on:change="setBR" placeholder="Change Me!" :value="brake_resistance"/>
|
||||
<span class="unit"> [{{ unit }}] </span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getUnit, parseMath, getVal } from "../../../lib/odrive_utils.js";
|
||||
|
||||
export default {
|
||||
name: "wizardBrake",
|
||||
props: {
|
||||
data: Object,
|
||||
allowed: Boolean,
|
||||
},
|
||||
data: function () {
|
||||
return {
|
||||
brake_resistance: undefined,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
unit() {
|
||||
// goal is to return "Ohms" from odriveUnits
|
||||
let path = "odrive0.config.brake_resistance";
|
||||
return getUnit(this.$store.state.odrives.odrive0, path);
|
||||
},
|
||||
value() {
|
||||
let path = "odrive0.config.brake_resistance";
|
||||
return getVal(path);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
setBR(e) {
|
||||
console.log("from setBR " + e.target.value);
|
||||
let val = parseMath(e.target.value);
|
||||
if (val !== false) {
|
||||
this.brake_resistance = val;
|
||||
let configStub = undefined;
|
||||
configStub = {
|
||||
config: {
|
||||
brake_resistance: this.brake_resistance,
|
||||
},
|
||||
};
|
||||
this.$emit("choice", {
|
||||
choice: "Brake Resistor",
|
||||
configStub: configStub,
|
||||
hooks: [],
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.wizard-choice {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin: 2rem;
|
||||
}
|
||||
.left {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
input {
|
||||
width: 5rem;
|
||||
font-family: inherit;
|
||||
border-style: none;
|
||||
border-bottom: 1px solid grey;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
input::-webkit-outer-spin-button,
|
||||
input::-webkit-inner-spin-button {
|
||||
/* display: none; <- Crashes Chrome on hover */
|
||||
-webkit-appearance: none;
|
||||
margin: 0; /* <-- Apparently some margin are still there even though it's hidden */
|
||||
}
|
||||
|
||||
input[type="number"] {
|
||||
-moz-appearance: textfield; /* Firefox */
|
||||
}
|
||||
|
||||
.unit {
|
||||
font-family: "Roboto Mono", monospace;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,56 @@
|
||||
<template>
|
||||
<!-- single choice for a wizard page -->
|
||||
<div class="wizard-choice card" :class="{'choice-inactive': !allowed}">
|
||||
<img v-if="data.imageURL != null" :alt="title" :src="data.imageURL" />
|
||||
{{ title }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "wizardChoice",
|
||||
props: {
|
||||
data: Object,
|
||||
selected: Boolean,
|
||||
title: String,
|
||||
hooks: Array,
|
||||
allowed: Boolean,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
oldSelected: undefined,
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
selected(newVal) {
|
||||
if (newVal == true) {
|
||||
// emit choice event
|
||||
this.$emit('choice', {choice: this.title, configStub: this.data.configStub, hooks: this.hooks});
|
||||
}
|
||||
if (this.oldSelected == true && newVal == false){
|
||||
console.log("emitting undo-choice");
|
||||
this.$emit('undo-choice', {choice: this.title, configStub: this.data.configStub, hooks: this.hooks})
|
||||
}
|
||||
this.oldSelected = newVal;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.wizard-choice {
|
||||
text-align: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin: 2rem;
|
||||
}
|
||||
|
||||
.inactive {
|
||||
background-color: var(--bg-color);
|
||||
}
|
||||
|
||||
img {
|
||||
padding: 0px;
|
||||
margin: auto;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,100 @@
|
||||
<template>
|
||||
<div class="card wizard-choice" :class="{ 'choice-inactive': !allowed }">
|
||||
<div class="left">
|
||||
<span>counts per revolution = </span>
|
||||
<input v-on:change="setCPR" placeholder="Change Me!" :value="cpr" />
|
||||
</div>
|
||||
<span class="name">Incremental Encoder without Index</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import odriveEnums from "../../../assets/odriveEnums.json";
|
||||
import { parseMath } from "../../../lib/odrive_utils.js";
|
||||
export default {
|
||||
name: "wizardEncoderIncremental",
|
||||
props: {
|
||||
data: Object,
|
||||
allowed: Boolean,
|
||||
},
|
||||
data: function () {
|
||||
return {
|
||||
cpr_set: false,
|
||||
cpr: undefined,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
setCPR(e) {
|
||||
let val = parseMath(e.target.value);
|
||||
if (val !== false) {
|
||||
this.cpr = val;
|
||||
let configStub = undefined;
|
||||
if (this.data.axis == "axis0") {
|
||||
configStub = {
|
||||
axis0: {
|
||||
encoder: {
|
||||
config: {
|
||||
cpr: this.cpr,
|
||||
use_index: false,
|
||||
mode: odriveEnums.ENCODER_MODE_INCREMENTAL,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
} else if (this.data.axis == "axis1") {
|
||||
configStub = {
|
||||
axis1: {
|
||||
encoder: {
|
||||
config: {
|
||||
cpr: this.cpr,
|
||||
use_index: false,
|
||||
mode: odriveEnums.ENCODER_MODE_INCREMENTAL,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
this.$emit("choice", {
|
||||
choice: "Incremental",
|
||||
configStub: configStub,
|
||||
hooks: [],
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.wizard-choice {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin: 2rem;
|
||||
}
|
||||
.left {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.name {
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
input {
|
||||
width: 5rem;
|
||||
font-family: inherit;
|
||||
border-style: none;
|
||||
border-bottom: 1px solid grey;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
input::-webkit-outer-spin-button,
|
||||
input::-webkit-inner-spin-button {
|
||||
/* display: none; <- Crashes Chrome on hover */
|
||||
-webkit-appearance: none;
|
||||
margin: 0; /* <-- Apparently some margin are still there even though it's hidden */
|
||||
}
|
||||
|
||||
input[type="number"] {
|
||||
-moz-appearance: textfield; /* Firefox */
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,101 @@
|
||||
<template>
|
||||
<div class="card wizard-choice" :class="{ 'choice-inactive': !allowed }">
|
||||
<div class="left">
|
||||
<span>counts per revolution = </span>
|
||||
<input v-on:change="setCPR" placeholder="Change Me!" :value="cpr" />
|
||||
</div>
|
||||
<span class="name">Incremental Encoder with Index</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import odriveEnums from "../../../assets/odriveEnums.json";
|
||||
import { parseMath } from "../../../lib/odrive_utils.js";
|
||||
|
||||
export default {
|
||||
name: "wizardEncoderIncremental",
|
||||
props: {
|
||||
data: Object,
|
||||
allowed: Boolean,
|
||||
},
|
||||
data: function () {
|
||||
return {
|
||||
cpr_set: false,
|
||||
cpr: undefined,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
setCPR(e) {
|
||||
let val = parseMath(e.target.value);
|
||||
if (val !== false) {
|
||||
this.cpr = val;
|
||||
let configStub = undefined;
|
||||
if (this.data.axis == "axis0") {
|
||||
configStub = {
|
||||
axis0: {
|
||||
encoder: {
|
||||
config: {
|
||||
cpr: this.cpr,
|
||||
use_index: true,
|
||||
mode: odriveEnums.ENCODER_MODE_INCREMENTAL,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
} else if (this.data.axis == "axis1") {
|
||||
configStub = {
|
||||
axis1: {
|
||||
encoder: {
|
||||
config: {
|
||||
cpr: this.cpr,
|
||||
use_index: true,
|
||||
mode: odriveEnums.ENCODER_MODE_INCREMENTAL,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
this.$emit("choice", {
|
||||
choice: "IncrementalIndex",
|
||||
configStub: configStub,
|
||||
hooks: [],
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.wizard-choice {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin: 2rem;
|
||||
}
|
||||
.left {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.name {
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
input {
|
||||
width: 5rem;
|
||||
font-family: inherit;
|
||||
border-style: none;
|
||||
border-bottom: 1px solid grey;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
input::-webkit-outer-spin-button,
|
||||
input::-webkit-inner-spin-button {
|
||||
/* display: none; <- Crashes Chrome on hover */
|
||||
-webkit-appearance: none;
|
||||
margin: 0; /* <-- Apparently some margin are still there even though it's hidden */
|
||||
}
|
||||
|
||||
input[type="number"] {
|
||||
-moz-appearance: textfield; /* Firefox */
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,127 @@
|
||||
<template>
|
||||
<div class="card" :class="{'choice-inactive': !allowed}">
|
||||
<div class="diff" v-for="diff in configDiffs" :key="diff.path">
|
||||
<span class="diff-path">{{diff.path}}: </span>
|
||||
<span class="diff-old">{{diffRepresentation(diff.path,diff.oldVal)}}</span>
|
||||
<span class="diff-seperator"> ⮕ </span>
|
||||
<span :class="{'diff-new': diff.oldVal != diff.newVal, 'diff-same': diff.oldVal == diff.newVal}">{{diffRepresentation(diff.path,diff.newVal)}}</span>
|
||||
</div>
|
||||
<button class="wizard-button card" @click="applyConfig">Apply</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { enumVars } from "../../../assets/wizard/wizard.js";
|
||||
import { putVal, fetchParam } from "../../../lib/odrive_utils.js"
|
||||
|
||||
export default {
|
||||
name: "wizardEnd",
|
||||
props: {
|
||||
config: Object,
|
||||
axis: String,
|
||||
allowed: Boolean,
|
||||
// actual odrive config is in this.$store (Vuex store)
|
||||
},
|
||||
data: function() {
|
||||
return {
|
||||
configDiffs: [], // array of objects {path, oldval, newval}
|
||||
flatpaths: [],
|
||||
path: [],
|
||||
}
|
||||
},
|
||||
created() {
|
||||
// flatten config tree into array of full variable paths
|
||||
this.pathFromTree(this.config);
|
||||
//for (const path of this.flatpaths){
|
||||
// console.log(path);
|
||||
//}
|
||||
for (const path of this.flatpaths) {
|
||||
let odrvObj = this.$store.state.odrives.odrive0;
|
||||
let configObj = this.config;
|
||||
for (const key of path.split('.')){
|
||||
odrvObj = odrvObj[key];
|
||||
configObj = configObj[key];
|
||||
}
|
||||
if (configObj != null) {
|
||||
console.log("oldVal is " + odrvObj["val"] + " path is " + path);
|
||||
if (Number.isInteger(parseFloat(odrvObj["val"]))){
|
||||
this.configDiffs.push({path: path, oldVal: parseFloat(odrvObj["val"]), newVal: configObj});
|
||||
}
|
||||
else if (typeof configObj == 'boolean'){
|
||||
this.configDiffs.push({path: path, oldVal: odrvObj["val"] == "True" || odrvObj["val"] == true, newVal: configObj});
|
||||
}
|
||||
else {
|
||||
this.configDiffs.push({path: path, oldVal: parseFloat(odrvObj["val"]), newVal: configObj});
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
pathFromTree(tree){
|
||||
// array path
|
||||
for (const key of Object.keys(tree)) {
|
||||
this.path.push(key);
|
||||
if (tree[key] != null && typeof tree[key] == "object") {
|
||||
this.pathFromTree(tree[key]);
|
||||
}
|
||||
else {
|
||||
this.flatpaths.push(this.path.join('.'));
|
||||
}
|
||||
this.path.pop();
|
||||
}
|
||||
},
|
||||
applyConfig(){
|
||||
// flatpaths will already be populated at this point
|
||||
for (const diff of this.configDiffs) {
|
||||
putVal("odrive0." + diff.path, diff.newVal);
|
||||
fetchParam("odrive0." + diff.path);
|
||||
console.log("applying " + diff.newVal + " to " + diff.path);
|
||||
}
|
||||
},
|
||||
diffRepresentation(path, val) {
|
||||
// for the diff view, check if the path indicates that a value is an Enum and return enum string
|
||||
// otherwise, just the numeric value
|
||||
let keys = path.split('.');
|
||||
let retval;
|
||||
if (Object.keys(enumVars).includes(keys[keys.length - 1])) {
|
||||
retval = enumVars[keys[keys.length-1]][val];
|
||||
}
|
||||
else if (!Number.isInteger(val) && typeof val != 'boolean') {
|
||||
retval = parseFloat(val).toExponential(3);
|
||||
}
|
||||
else {
|
||||
retval = val;
|
||||
}
|
||||
return retval;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.diff {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.diff-path {
|
||||
font-family: "Roboto Mono", monospace;
|
||||
}
|
||||
|
||||
.diff-old {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.diff-new {
|
||||
font-weight: bold;
|
||||
color: green;
|
||||
}
|
||||
|
||||
.diff-same {
|
||||
font-weight: bold;
|
||||
color: blue;
|
||||
}
|
||||
|
||||
.wizard-button:active {
|
||||
background-color: var(--bg-color);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,117 @@
|
||||
<template>
|
||||
<div class="card wizard-motor-custom wizard-choice" :class="{'choice-inactive': !allowed}">
|
||||
<div class="left">
|
||||
<span>Input filter bandwidth =</span>
|
||||
<input type="number" v-on:change="setBandwidth" :placeholder="bandwidth" />
|
||||
<span>Hz</span>
|
||||
</div>
|
||||
<span class="name">Filtered Position Input Mode</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import odriveEnums from "../../../assets/odriveEnums.json";
|
||||
import { getVal } from "../../../lib/odrive_utils.js"
|
||||
|
||||
export default {
|
||||
name: "wizardInputFiltered",
|
||||
props: {
|
||||
data: Object,
|
||||
allowed: Boolean,
|
||||
},
|
||||
data: function () {
|
||||
return {
|
||||
bandwidth: undefined,
|
||||
};
|
||||
},
|
||||
created() {
|
||||
let path = "odrive0." + this.data.axis + ".controller.config.input_filter_bandwidth";
|
||||
this.bandwidth = parseFloat(getVal(path));
|
||||
},
|
||||
methods: {
|
||||
sendConfig() {
|
||||
console.log("emitting choice event from limits page");
|
||||
let configStub = undefined;
|
||||
if (this.data.axis == "axis0") {
|
||||
configStub = {
|
||||
axis0: {
|
||||
controller: {
|
||||
config: {
|
||||
input_filter_bandwidth: this.bandwidth,
|
||||
input_mode: odriveEnums.INPUT_MODE_POS_FILTER,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
} else if (this.data.axis == "axis1") {
|
||||
configStub = {
|
||||
axis1: {
|
||||
controller: {
|
||||
config: {
|
||||
input_filter_bandwidth: this.bandwidth,
|
||||
input_mode: odriveEnums.INPUT_MODE_POS_FILTER,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
this.$emit("choice", {
|
||||
choice: "Input Filter " + this.data.axis,
|
||||
configStub: configStub,
|
||||
hooks: [],
|
||||
});
|
||||
},
|
||||
setBandwidth(e) {
|
||||
this.bandwidth = parseFloat(e.target.value);
|
||||
this.sendConfig();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.wizard-choice {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin: 2rem;
|
||||
}
|
||||
.left {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.name {
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
input {
|
||||
width: 5rem;
|
||||
font-family: inherit;
|
||||
border-style: none;
|
||||
border-bottom: 1px solid grey;
|
||||
text-align: center;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
input::-webkit-outer-spin-button,
|
||||
input::-webkit-inner-spin-button {
|
||||
/* display: none; <- Crashes Chrome on hover */
|
||||
-webkit-appearance: none;
|
||||
margin: 0; /* <-- Apparently some margin are still there even though it's hidden */
|
||||
}
|
||||
|
||||
input[type="number"] {
|
||||
-moz-appearance: textfield; /* Firefox */
|
||||
}
|
||||
|
||||
.measure-button {
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.measure-button:active {
|
||||
background-color: var(--bg-color);
|
||||
}
|
||||
|
||||
.name {
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,160 @@
|
||||
<template>
|
||||
<div class="card wizard-motor-custom wizard-choice" :class="{'choice-inactive': !allowed}">
|
||||
<div class="left">
|
||||
<span>Velocity limit =</span>
|
||||
<input type="number" v-on:change="setVelLimit" :placeholder="vel_limit" />
|
||||
<span>turns / s</span>
|
||||
</div>
|
||||
<div class="left">
|
||||
<span>Acceleration limit =</span>
|
||||
<input type="number" v-on:change="setAccelLimit" :placeholder="accel_limit" />
|
||||
<span>turns / s^2</span>
|
||||
</div>
|
||||
<div class="left">
|
||||
<span>Deceleration limit =</span>
|
||||
<input type="number" v-on:change="setDecelLimit" :placeholder="decel_limit" />
|
||||
<span>turns / s^2</span>
|
||||
</div>
|
||||
<div class="left">
|
||||
<span>Inertia =</span>
|
||||
<input type="number" v-on:change="setInertia" :placeholder="inertia" />
|
||||
</div>
|
||||
<span class="name">Trajectory Planner</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getVal } from "../../../lib/odrive_utils.js"
|
||||
|
||||
export default {
|
||||
name: "wizardInputTrajectory",
|
||||
props: {
|
||||
data: Object,
|
||||
allowed: Boolean,
|
||||
},
|
||||
data: function () {
|
||||
return {
|
||||
vel_limit: undefined,
|
||||
accel_limit: undefined,
|
||||
decel_limit: undefined,
|
||||
inertia: undefined,
|
||||
};
|
||||
},
|
||||
created() {
|
||||
// get pre-existing values for trap_traj vel limit, accel, devel, and config.inertia
|
||||
this.vel_limit = getVal("odrive0." + this.data.axis + ".trap_traj.config.vel_limit");
|
||||
this.accel_limit = getVal("odrive0." + this.data.axis + ".trap_traj.config.accel_limit");
|
||||
this.decel_limit = getVal("odrive0." + this.data.axis + ".trap_traj.config.decel_limit");
|
||||
this.inertia = getVal("odrive0." + this.data.axis + ".controller.config.inertia");
|
||||
},
|
||||
methods: {
|
||||
sendConfig() {
|
||||
console.log("emitting choice event from limits page");
|
||||
let configStub = undefined;
|
||||
if (this.data.axis == "axis0") {
|
||||
configStub = {
|
||||
axis0: {
|
||||
controller: {
|
||||
config: {
|
||||
inertia: this.inertia,
|
||||
},
|
||||
},
|
||||
trap_traj: {
|
||||
config: {
|
||||
vel_limit: this.vel_limit,
|
||||
accel_limit: this.accel_limit,
|
||||
decel_limit: this.decel_limit,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
} else if (this.data.axis == "axis1") {
|
||||
configStub = {
|
||||
axis1: {
|
||||
controller: {
|
||||
config: {
|
||||
inertia: this.inertia,
|
||||
},
|
||||
},
|
||||
trap_traj: {
|
||||
config: {
|
||||
vel_limit: this.vel_limit,
|
||||
accel_limit: this.accel_limit,
|
||||
decel_limit: this.decel_limit,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
this.$emit("choice", {
|
||||
choice: "Trapezoidal trajectory " + this.data.axis,
|
||||
configStub: configStub,
|
||||
hooks: [],
|
||||
});
|
||||
},
|
||||
setVelLimit(e) {
|
||||
this.vel_limit = parseFloat(e.target.value);
|
||||
this.sendConfig();
|
||||
},
|
||||
setAccelLimit(e) {
|
||||
this.accel_limit = parseFloat(e.target.value);
|
||||
this.sendConfig();
|
||||
},
|
||||
setDecelLimit(e) {
|
||||
this.decel_limit = parseFloat(e.target.value);
|
||||
this.sendConfig();
|
||||
},
|
||||
setInertia(e) {
|
||||
this.inertia = parseFloat(e.target.value);
|
||||
this.sendConfig();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.wizard-choice {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin: 2rem;
|
||||
}
|
||||
.left {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.name {
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
input {
|
||||
width: 5rem;
|
||||
font-family: inherit;
|
||||
border-style: none;
|
||||
border-bottom: 1px solid grey;
|
||||
text-align: center;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
input::-webkit-outer-spin-button,
|
||||
input::-webkit-inner-spin-button {
|
||||
/* display: none; <- Crashes Chrome on hover */
|
||||
-webkit-appearance: none;
|
||||
margin: 0; /* <-- Apparently some margin are still there even though it's hidden */
|
||||
}
|
||||
|
||||
input[type="number"] {
|
||||
-moz-appearance: textfield; /* Firefox */
|
||||
}
|
||||
|
||||
.measure-button {
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.measure-button:active {
|
||||
background-color: var(--bg-color);
|
||||
}
|
||||
|
||||
.name {
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,117 @@
|
||||
<template>
|
||||
<div class="card wizard-motor-custom wizard-choice" :class="{'choice-inactive': !allowed}">
|
||||
<div class="left">
|
||||
<span>Velocity ramp rate =</span>
|
||||
<input type="number" v-on:change="setRampRate" :placeholder="vel_ramp_rate" />
|
||||
<span>turns / s^2</span>
|
||||
</div>
|
||||
<span class="name">Ramped Velocity Input Mode</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import odriveEnums from "../../../assets/odriveEnums.json";
|
||||
import { getVal } from "../../../lib/odrive_utils.js"
|
||||
|
||||
export default {
|
||||
name: "wizardInputVelRamp",
|
||||
props: {
|
||||
data: Object,
|
||||
allowed: Boolean,
|
||||
},
|
||||
data: function () {
|
||||
return {
|
||||
vel_ramp_rate: undefined,
|
||||
};
|
||||
},
|
||||
created() {
|
||||
let path = "odrive0." + this.data.axis + ".controller.config.vel_ramp_rate";
|
||||
this.vel_ramp_rate = parseFloat(getVal(path));
|
||||
},
|
||||
methods: {
|
||||
sendConfig() {
|
||||
console.log("emitting choice event from limits page");
|
||||
let configStub = undefined;
|
||||
if (this.data.axis == "axis0") {
|
||||
configStub = {
|
||||
axis0: {
|
||||
controller: {
|
||||
config: {
|
||||
vel_ramp_rate: this.vel_ramp_rate,
|
||||
input_mode: odriveEnums.INPUT_MODE_VEL_RAMP,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
} else if (this.data.axis == "axis1") {
|
||||
configStub = {
|
||||
axis1: {
|
||||
controller: {
|
||||
config: {
|
||||
vel_ramp_rate: this.vel_ramp_rate,
|
||||
input_mode: odriveEnums.INPUT_MODE_VEL_RAMP,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
this.$emit("choice", {
|
||||
choice: "Velocity ramp " + this.data.axis,
|
||||
configStub: configStub,
|
||||
hooks: [],
|
||||
});
|
||||
},
|
||||
setRampRate(e) {
|
||||
this.vel_ramp_rate = parseFloat(e.target.value);
|
||||
this.sendConfig();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.wizard-choice {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin: 2rem;
|
||||
}
|
||||
.left {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.name {
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
input {
|
||||
width: 5rem;
|
||||
font-family: inherit;
|
||||
border-style: none;
|
||||
border-bottom: 1px solid grey;
|
||||
text-align: center;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
input::-webkit-outer-spin-button,
|
||||
input::-webkit-inner-spin-button {
|
||||
/* display: none; <- Crashes Chrome on hover */
|
||||
-webkit-appearance: none;
|
||||
margin: 0; /* <-- Apparently some margin are still there even though it's hidden */
|
||||
}
|
||||
|
||||
input[type="number"] {
|
||||
-moz-appearance: textfield; /* Firefox */
|
||||
}
|
||||
|
||||
.measure-button {
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.measure-button:active {
|
||||
background-color: var(--bg-color);
|
||||
}
|
||||
|
||||
.name {
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,166 @@
|
||||
<template>
|
||||
<div
|
||||
class="card wizard-motor-custom wizard-choice"
|
||||
:class="{ 'choice-inactive': !allowed }"
|
||||
>
|
||||
<div class="left">
|
||||
<span>Motor Velocity Limit =</span>
|
||||
<input
|
||||
v-on:change="setVelocityLimit"
|
||||
:placeholder="velocityLimit"
|
||||
:value="vel_limit"
|
||||
/>
|
||||
<span class="unit">[{{ velUnit }}]</span>
|
||||
</div>
|
||||
<div class="left">
|
||||
<span>Motor Current Limit =</span>
|
||||
<input
|
||||
v-on:change="setCurrentLimit"
|
||||
:placeholder="currentLimit"
|
||||
:value="current_lim"
|
||||
/>
|
||||
<span class="unit">[Amps]</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getVal, getUnit, parseMath } from "../../../lib/odrive_utils.js";
|
||||
|
||||
export default {
|
||||
name: "wizardMisc",
|
||||
props: {
|
||||
data: Object,
|
||||
allowed: Boolean,
|
||||
},
|
||||
data: function () {
|
||||
return {
|
||||
current_lim: undefined,
|
||||
vel_limit: undefined,
|
||||
vel_set: false,
|
||||
current_set: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
velocityLimit: function () {
|
||||
let path = "odrive0." + this.data.axis + ".controller.config.vel_limit";
|
||||
return parseFloat(getVal(path));
|
||||
},
|
||||
currentLimit: function () {
|
||||
let path = "odrive0." + this.data.axis + ".motor.config.current_lim";
|
||||
return parseFloat(getVal(path));
|
||||
},
|
||||
velUnit() {
|
||||
let path = "odrive0." + this.data.axis + ".controller.config.vel_limit";
|
||||
return getUnit(this.$store.state.odrives.odrive0, path);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
sendConfig() {
|
||||
if (this.vel_set == true && this.current_set == true) {
|
||||
console.log("emitting choice event from limits page");
|
||||
let configStub = undefined;
|
||||
if (this.data.axis == "axis0") {
|
||||
configStub = {
|
||||
axis0: {
|
||||
controller: {
|
||||
config: {
|
||||
vel_limit: this.vel_limit,
|
||||
},
|
||||
},
|
||||
motor: {
|
||||
config: {
|
||||
current_lim: this.current_lim,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
} else if (this.data.axis == "axis1") {
|
||||
configStub = {
|
||||
axis1: {
|
||||
controller: {
|
||||
config: {
|
||||
vel_limit: this.vel_limit,
|
||||
},
|
||||
},
|
||||
motor: {
|
||||
config: {
|
||||
current_lim: this.current_lim,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
this.$emit("choice", {
|
||||
choice: "Misc " + this.data.axis,
|
||||
configStub: configStub,
|
||||
hooks: [],
|
||||
});
|
||||
}
|
||||
},
|
||||
setVelocityLimit(e) {
|
||||
let val = parseMath(e.target.value);
|
||||
if (val !== false) {
|
||||
this.vel_limit = parseFloat(e.target.value);
|
||||
this.vel_set = true;
|
||||
this.sendConfig();
|
||||
}
|
||||
},
|
||||
setCurrentLimit(e) {
|
||||
let val = parseMath(e.target.value);
|
||||
if (val !== false) {
|
||||
this.current_lim = parseFloat(e.target.value);
|
||||
this.current_set = true;
|
||||
this.sendConfig();
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.wizard-choice {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin: 2rem;
|
||||
}
|
||||
.left {
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
input {
|
||||
width: 5rem;
|
||||
font-family: inherit;
|
||||
border-style: none;
|
||||
border-bottom: 1px solid grey;
|
||||
text-align: center;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
input::-webkit-outer-spin-button,
|
||||
input::-webkit-inner-spin-button {
|
||||
/* display: none; <- Crashes Chrome on hover */
|
||||
-webkit-appearance: none;
|
||||
margin: 0; /* <-- Apparently some margin are still there even though it's hidden */
|
||||
}
|
||||
|
||||
input[type="number"] {
|
||||
-moz-appearance: textfield; /* Firefox */
|
||||
}
|
||||
|
||||
.measure-button {
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.measure-button:active {
|
||||
background-color: var(--bg-color);
|
||||
}
|
||||
|
||||
.name {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.unit {
|
||||
font-family: "Roboto Mono", monospace;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,198 @@
|
||||
<template>
|
||||
<div class="card wizard-motor-custom wizard-choice" :class="{'choice-inactive': !allowed}">
|
||||
<div class="left">
|
||||
<span>KV =</span>
|
||||
<input type="number" v-on:change="setKV" placeholder="Change Me!" />
|
||||
</div>
|
||||
<div class="left">
|
||||
<span>Pole Pairs =</span>
|
||||
<input type="number" v-on:change="setPP" placeholder="Change Me!" />
|
||||
</div>
|
||||
<div class="left">
|
||||
<span>Phase Resistance =</span>
|
||||
<span>{{" " + resistance}}</span>
|
||||
<span class="unit">[Ohms]</span>
|
||||
</div>
|
||||
<div class="left">
|
||||
<span>Phase Inductance =</span>
|
||||
<span>{{" " + inductance}}</span>
|
||||
<span class="unit">[Henries]</span>
|
||||
</div>
|
||||
<span class="name">Other Motor</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import odriveEnums from "../../../assets/odriveEnums.json";
|
||||
import { getVal } from "../../../lib/odrive_utils.js"
|
||||
|
||||
export default {
|
||||
name: "wizardMotor",
|
||||
props: {
|
||||
data: Object,
|
||||
axis: String,
|
||||
allowed: Boolean,
|
||||
},
|
||||
data: function () {
|
||||
return {
|
||||
kv_set: false,
|
||||
pp_set: false,
|
||||
r_set: false,
|
||||
l_set: false,
|
||||
pole_pairs: undefined,
|
||||
phase_resistance: undefined,
|
||||
phase_inductance: undefined,
|
||||
torque_constant: undefined,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
resistance: function () {
|
||||
let path = "odrive0." + this.data.axis + ".motor.config.phase_resistance";
|
||||
return parseFloat(getVal(path)).toExponential(3);
|
||||
},
|
||||
inductance: function () {
|
||||
let path = "odrive0." + this.data.axis + ".motor.config.phase_inductance";
|
||||
return parseFloat(getVal(path)).toExponential(3);
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
resistance: function (newVal) {
|
||||
console.log("from resistance watcher: " + newVal);
|
||||
let path = "odrive0." + this.data.axis + ".motor.config.phase_resistance";
|
||||
this.phase_resistance = parseFloat(getVal(path));
|
||||
},
|
||||
inductance: function (newVal) {
|
||||
console.log("from inductance watcher: " + newVal);
|
||||
let path = "odrive0." + this.data.axis + ".motor.config.phase_inductance";
|
||||
this.phase_inductance = parseFloat(getVal(path));
|
||||
},
|
||||
pp_set: function () {
|
||||
console.log("pp_set: " + this.pp_set);
|
||||
this.sendConfig();
|
||||
},
|
||||
kv_set: function () {
|
||||
console.log("kv_set: " + this.kv_set);
|
||||
this.sendConfig();
|
||||
},
|
||||
r_set: function () {
|
||||
console.log("r_set: " + this.r_set);
|
||||
this.sendConfig();
|
||||
},
|
||||
l_set: function () {
|
||||
console.log("l_set: " + this.l_set);
|
||||
this.sendConfig();
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
sendConfig() {
|
||||
// if all of the set flags are true, emit an event
|
||||
if (this.phase_resistance != 0) {
|
||||
this.r_set = true;
|
||||
} else {
|
||||
this.r_set = false;
|
||||
}
|
||||
if (this.phase_inductance != 0) {
|
||||
this.l_set = true;
|
||||
} else {
|
||||
this.l_set = false;
|
||||
}
|
||||
if (
|
||||
this.r_set == true &&
|
||||
this.l_set == true &&
|
||||
this.pp_set == true &&
|
||||
this.kv_set == true
|
||||
) {
|
||||
console.log("emitting choice event from other motor");
|
||||
let configStub = undefined;
|
||||
if (this.axis == "axis0") {
|
||||
configStub = {
|
||||
axis0: {
|
||||
motor: {
|
||||
config: {
|
||||
pole_pairs: this.pole_pairs,
|
||||
torque_constant: this.torque_constant,
|
||||
phase_resistance: this.phase_resistance,
|
||||
phase_inductance: this.phase_inductance,
|
||||
type: odriveEnums.MOTOR_TYPE_HIGH_CURRENT,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
} else if (this.axis == "axis1") {
|
||||
configStub = {
|
||||
axis1: {
|
||||
motor: {
|
||||
config: {
|
||||
pole_pairs: this.pole_pairs,
|
||||
torque_constant: this.torque_constant,
|
||||
phase_resistance: this.phase_resistance,
|
||||
phase_inductance: this.phase_inductance,
|
||||
type: odriveEnums.MOTOR_TYPE_HIGH_CURRENT,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
this.$emit("choice", {
|
||||
choice: "Other motor",
|
||||
configStub: configStub,
|
||||
hooks: [],
|
||||
});
|
||||
}
|
||||
},
|
||||
setKV(e) {
|
||||
this.torque_constant = 8.27 / parseFloat(e.target.value);
|
||||
this.kv_set = true;
|
||||
console.log(this.torque_constant);
|
||||
},
|
||||
setPP(e) {
|
||||
this.pole_pairs = parseFloat(e.target.value);
|
||||
this.pp_set = true;
|
||||
console.log(this.pole_pairs);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.wizard-choice {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin: 2rem;
|
||||
}
|
||||
.left {
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.name {
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
input {
|
||||
width: 5rem;
|
||||
font-family: inherit;
|
||||
border-style: none;
|
||||
border-bottom: 1px solid grey;
|
||||
text-align: center;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
input::-webkit-outer-spin-button,
|
||||
input::-webkit-inner-spin-button {
|
||||
/* display: none; <- Crashes Chrome on hover */
|
||||
-webkit-appearance: none;
|
||||
margin: 0; /* <-- Apparently some margin are still there even though it's hidden */
|
||||
}
|
||||
|
||||
input[type="number"] {
|
||||
-moz-appearance: textfield; /* Firefox */
|
||||
}
|
||||
|
||||
.name {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.unit {
|
||||
font-family: "Roboto Mono", monospace;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,38 @@
|
||||
<template>
|
||||
<div class="cal-message"
|
||||
v-tooltip.top="{
|
||||
content: data.tooltip,
|
||||
class: 'tooltip-custom tooltip-other-custom fade-in',
|
||||
delay: 0,
|
||||
visible: !calStatus,
|
||||
}"
|
||||
>{{message}}</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "wizardCalStatus",
|
||||
props: {
|
||||
data: Object,
|
||||
calStatus: Boolean,
|
||||
},
|
||||
computed: {
|
||||
message() {
|
||||
let retmsg = "";
|
||||
if (this.calStatus == true) {
|
||||
retmsg = this.data.success;
|
||||
} else if (this.calStatus == false) {
|
||||
retmsg = this.data.failure;
|
||||
}
|
||||
return retmsg;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.cal-message {
|
||||
margin-top: auto;
|
||||
margin-bottom: auto;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,35 @@
|
||||
<template>
|
||||
<div>
|
||||
<button class="measure-button card" @click="calibrate">{{text}}</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
name: "wizardEncoderCal",
|
||||
props: {
|
||||
data: Object,
|
||||
calibrating: Boolean,
|
||||
},
|
||||
computed: {
|
||||
text() {
|
||||
return this.calibrating ? "Calibrating..." : "Calibrate Encoder";
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
calibrate() {
|
||||
// ask ODrive to measure resistance and inductance
|
||||
this.$emit('page-comp-event', {data: "encoder calibration", axis: this.data.axis});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
.measure-button:active {
|
||||
background-color: var(--bg-color);
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,36 @@
|
||||
<template>
|
||||
<div>
|
||||
<button class="measure-button card" @click="calibrate">{{text}}</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
//import odriveEnums from "../../../assets/odriveEnums.json";
|
||||
//import { putVal } from "../../../lib/odrive_utils.js"
|
||||
|
||||
export default {
|
||||
name: "wizardMotorCal",
|
||||
props: {
|
||||
data: Object,
|
||||
calibrating: Boolean,
|
||||
},
|
||||
computed: {
|
||||
text() {
|
||||
return this.calibrating ? "Calibrating..." : "Calibrate Motor";
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
calibrate() {
|
||||
this.$emit('page-comp-event', {data: "motor calibration", axis: this.data.axis});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
.measure-button:active {
|
||||
background-color: var(--bg-color);
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,190 @@
|
||||
<template>
|
||||
<!-- render a list of choices horizontally in cards -->
|
||||
<div class="wizard-page card">
|
||||
<div class="wizard-title">{{title}}</div>
|
||||
<div class="wizard-choices">
|
||||
<!--
|
||||
<wizardChoice
|
||||
v-on:click.native="selectedChoice=choice.text;$emit('choice', {title: title, choice: choice.text, configStub: choice.configStub, hooks: choice.hooks})"
|
||||
/>
|
||||
-->
|
||||
<template v-for="choice in choices">
|
||||
<component
|
||||
:class="{chosen: selectedChoice == choice.title, unchosen: selectedChoice != choice.title, inactive: !requirementsMet(choice.requirements, config)}"
|
||||
:is="choice.component"
|
||||
:data="choice.data"
|
||||
:config="config"
|
||||
:axis="axis"
|
||||
:title="choice.title"
|
||||
:selected="selectedChoice == choice.title"
|
||||
:hooks="choice.hooks"
|
||||
:allowed="requirementsMet(choice.requirements, config)"
|
||||
v-tooltip.top="{
|
||||
content: requirementsMet(choice.requirements, config) ? choice.tooltip : choice.altTooltip,
|
||||
class: 'tooltip-custom tooltip-other-custom fade-in',
|
||||
delay: 0,
|
||||
visible: choice.tooltip!=null,
|
||||
}"
|
||||
:key="choice.title"
|
||||
v-on:click.native="choiceHandler(choice)"
|
||||
v-on:choice="handleCustomChoice"
|
||||
v-on:undo-choice="undoChoice"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
<div class="page-components">
|
||||
<template v-for="pageComponent in pageComponents">
|
||||
<component
|
||||
:is="pageComponent.component"
|
||||
:data="pageComponent.data"
|
||||
:key="pageComponent.id"
|
||||
:calibrating="calibrating"
|
||||
:calStatus="calStatus"
|
||||
v-on:page-comp-event="pageCompEvent"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import wizardChoice from "./choices/wizardChoice.vue";
|
||||
import wizardMisc from "./choices/wizardMisc.vue";
|
||||
import wizardMotor from "./choices/wizardMotor.vue";
|
||||
import wizardEncoderIncremental from "./choices/wizardEncoderIncremental.vue";
|
||||
import wizardEncoderIncrementalIndex from "./choices/wizardEncoderIncrementalIndex.vue";
|
||||
import wizardEnd from "./choices/wizardEnd.vue";
|
||||
import wizardMotorCal from "./page_components/wizardMotorCal.vue";
|
||||
import wizardEncoderCal from "./page_components/wizardEncoderCal.vue";
|
||||
import wizardCalStatus from "./page_components/wizardCalStatus.vue";
|
||||
import wizardBrake from "./choices/wizardBrake.vue";
|
||||
import wizardInputFiltered from "./choices/wizardInputFiltered.vue";
|
||||
import wizardInputVelRamp from "./choices/wizardInputVelRamp.vue";
|
||||
import wizardInputTrajectory from "./choices/wizardInputTrajectory.vue";
|
||||
|
||||
export default {
|
||||
name: "wizardPage",
|
||||
props: {
|
||||
choices: Array,
|
||||
title: String,
|
||||
customComponents: Array,
|
||||
axis: String,
|
||||
config: Object,
|
||||
pageComponents: Array,
|
||||
calibrating: Boolean,
|
||||
calStatus: Boolean,
|
||||
},
|
||||
components: {
|
||||
wizardChoice,
|
||||
wizardMisc,
|
||||
wizardMotor,
|
||||
wizardEncoderIncremental,
|
||||
wizardEncoderIncrementalIndex,
|
||||
wizardEnd,
|
||||
wizardMotorCal,
|
||||
wizardEncoderCal,
|
||||
wizardCalStatus,
|
||||
wizardBrake,
|
||||
wizardInputFiltered,
|
||||
wizardInputVelRamp,
|
||||
wizardInputTrajectory,
|
||||
},
|
||||
data: function () {
|
||||
return {
|
||||
selectedChoice: undefined,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
handleCustomChoice(e) {
|
||||
console.log(e);
|
||||
this.$emit("choice", e);
|
||||
},
|
||||
undoChoice(e) {
|
||||
this.$emit("undo-choice",e);
|
||||
},
|
||||
pageCompEvent(e) {
|
||||
this.$emit("page-comp-event", e);
|
||||
},
|
||||
requirementsMet(reqs, wizardConfig){
|
||||
let reqsMet = true;
|
||||
for (const fn of reqs) {
|
||||
reqsMet = reqsMet && fn(wizardConfig);
|
||||
}
|
||||
return reqsMet;
|
||||
},
|
||||
choiceHandler(choice) {
|
||||
console.log(choice.title);
|
||||
if (this.selectedChoice == choice.title) {
|
||||
// undo selection
|
||||
this.selectedChoice = undefined;
|
||||
}
|
||||
else if (this.requirementsMet(choice.requirements, this.config)){
|
||||
this.selectedChoice = choice.title;
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.wizard-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.wizard-choices {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.page-components {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.chosen {
|
||||
border: 2px solid black;
|
||||
}
|
||||
|
||||
.unchosen {
|
||||
border: 2px solid transparent;
|
||||
}
|
||||
|
||||
.inactive {
|
||||
background-color: var(--bg-color);
|
||||
}
|
||||
|
||||
.wizard-title {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.vue-tooltip.tooltip-custom {
|
||||
background-color: lightyellow; /* var(--fg-color); */
|
||||
border-radius: 0px;
|
||||
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.4);
|
||||
font-family: "Roboto", sans-serif;
|
||||
color: black;
|
||||
}
|
||||
|
||||
.vue-tooltip.tooltip-custom .tooltip-arrow {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.vue-tooltip.fade-in {
|
||||
opacity: 0;
|
||||
animation: fadeIn ease 0.25s;
|
||||
animation-fill-mode: both;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,242 @@
|
||||
import store from "../store.js";
|
||||
import * as socketio from "../comms/socketio.js";
|
||||
import {wait, waitFor} from "./utils.js";
|
||||
import odriveEnums from "../assets/odriveEnums.json"
|
||||
|
||||
// helper functions and utilities for getting ODrive values
|
||||
|
||||
// given a path like "odrive0.axis0.config.blah", return the value
|
||||
export function getParam(path) {
|
||||
let keys = path.split('.');
|
||||
if (store.state.ODrivesConnected[keys[0]]) {
|
||||
let odriveObj = store.state.odrives;
|
||||
for (const key of keys) {
|
||||
odriveObj = odriveObj[key];
|
||||
}
|
||||
return odriveObj;
|
||||
}
|
||||
else {
|
||||
console.log("getParam for " + path + " is for disconnected ODrive");
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
// wrapper for val field
|
||||
export function getVal(path) {
|
||||
return getParam(path + ".val");
|
||||
}
|
||||
|
||||
// wrapper for readonly field
|
||||
export function getReadonly(path) {
|
||||
return getParam(path + '.readonly');
|
||||
}
|
||||
|
||||
export function fetchParam(path) {
|
||||
if (store.state.serverConnected){
|
||||
socketio.sendEvent({
|
||||
type: "getProperty",
|
||||
data: {path: path},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function parseMath(inString) {
|
||||
// given an input string that is valid arithmetic, use eval() to evaluate it
|
||||
//let allowedChars = "0123456789eE/*-+.()";
|
||||
let send = true;
|
||||
//for (const c of inString) {
|
||||
// if (!allowedChars.includes(c)) {
|
||||
// send = false;
|
||||
// }
|
||||
//}
|
||||
if (send) {
|
||||
return eval(inString);
|
||||
}
|
||||
else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function putVal(path, value) {
|
||||
console.log("path: " + path + ", val: " + value + ", type: " + typeof value);
|
||||
if (store.state.ODrivesConnected[path.split('.')[0]]) {
|
||||
if (value == Number.POSITIVE_INFINITY) {
|
||||
value = "Infinity";
|
||||
}
|
||||
else if (value == Number.NEGATIVE_INFINITY) {
|
||||
value = "-Infinity";
|
||||
}
|
||||
socketio.sendEvent({
|
||||
type: "setProperty",
|
||||
data: {path: path, val: value, type: typeof value}
|
||||
});
|
||||
}
|
||||
else {
|
||||
console.log("requesting " + path + " from disconnected odrive")
|
||||
}
|
||||
}
|
||||
|
||||
// path is path to function, args is list of parameters
|
||||
export function callFcn(path, args = []) {
|
||||
console.log("calling function: " + path);
|
||||
socketio.sendEvent({
|
||||
type: "callFunction",
|
||||
data: {path: path, args: args}
|
||||
});
|
||||
}
|
||||
|
||||
export function odriveMinorVersion(odrive) {
|
||||
// given an odrive object, return the minor version
|
||||
return odrive.fw_version_minor.val;
|
||||
}
|
||||
|
||||
export function getUnit(odrive, path) {
|
||||
let unit = undefined;
|
||||
for ( const key of Object.keys(odriveUnits[odriveMinorVersion(odrive)]) ) {
|
||||
if (path.includes(key)) {
|
||||
unit = odriveUnits[odriveMinorVersion(odrive)][key];
|
||||
}
|
||||
}
|
||||
return unit;
|
||||
}
|
||||
|
||||
export function clearErrors(odrive, axis) {
|
||||
// odrive is odrive path like 'odrive0'
|
||||
// axis is 'axis0', etc
|
||||
let paths = [];
|
||||
[".error", ".motor.error", ".encoder.error", ".controller.error"].forEach((path) => {
|
||||
paths.push(odrive + axis + path);
|
||||
});
|
||||
paths.forEach((path) => {
|
||||
putVal(path, 0);
|
||||
})
|
||||
}
|
||||
|
||||
export async function motorCalibration(odrive, axis) {
|
||||
// set up our continuous fetch
|
||||
let state;
|
||||
let motorError;
|
||||
|
||||
let updateVals = () => {
|
||||
fetchParam(odrive + axis + ".current_state");
|
||||
fetchParam(odrive + axis + ".motor.config.phase_resistance");
|
||||
fetchParam(odrive + axis + ".motor.config.phase_inductance");
|
||||
fetchParam(odrive + axis + ".motor.is_calibrated");
|
||||
fetchParam(odrive + axis + ".motor.error")
|
||||
|
||||
state = getVal(odrive + axis + ".current_state");
|
||||
motorError = getVal(odrive + axis + ".motor.error");
|
||||
}
|
||||
|
||||
// set up our state watch function
|
||||
let end = () => {
|
||||
// return {done: boolean, data <whatever>}
|
||||
if (state != odriveEnums.AXIS_STATE_MOTOR_CALIBRATION) {
|
||||
return {done: true, data: motorError}
|
||||
}
|
||||
else {
|
||||
return {done: false}
|
||||
}
|
||||
}
|
||||
|
||||
// start getting live updates
|
||||
let cylicUpdate = setInterval(updateVals, 100);
|
||||
|
||||
// send motor calibration command to correct odrive and axis
|
||||
putVal(odrive + axis + ".requested_state", odriveEnums.AXIS_STATE_MOTOR_CALIBRATION);
|
||||
|
||||
// give it some time to start
|
||||
await wait(500);
|
||||
|
||||
// only two things can happen now, either we successfully calibrate or we error out for some reason
|
||||
const result = await waitFor(end);
|
||||
|
||||
// stop our parameter updates
|
||||
clearInterval(cylicUpdate);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function encoderCalibration(odrive,axis) {
|
||||
// set up our continuous fetch
|
||||
let state;
|
||||
let encoderError;
|
||||
|
||||
let updateVals = () => {
|
||||
fetchParam(odrive + axis + ".current_state");
|
||||
fetchParam(odrive + axis + ".encoder.is_ready");
|
||||
fetchParam(odrive + axis + ".encoder.error")
|
||||
|
||||
state = getVal(odrive + axis + ".current_state");
|
||||
encoderError = getVal(odrive + axis + ".encoder.error");
|
||||
}
|
||||
|
||||
// set up our state watch function
|
||||
let end = () => {
|
||||
// return {done: boolean, data <whatever>}
|
||||
if (state != odriveEnums.AXIS_STATE_ENCODER_OFFSET_CALIBRATION) {
|
||||
return {done: true, data: encoderError}
|
||||
}
|
||||
else {
|
||||
return {done: false}
|
||||
}
|
||||
}
|
||||
|
||||
// start getting live updates
|
||||
let cylicUpdate = setInterval(updateVals, 100);
|
||||
|
||||
// start encoder offset cal
|
||||
putVal(odrive + axis + ".requested_state", odriveEnums.AXIS_STATE_ENCODER_OFFSET_CALIBRATION);
|
||||
|
||||
// give it some time to start
|
||||
await wait(500);
|
||||
|
||||
// only two things can happen now, either we successfully calibrate or we error out for some reason
|
||||
const result = await waitFor(end);
|
||||
|
||||
// stop our parameter updates
|
||||
clearInterval(cylicUpdate);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// standins for wizard and gui unit displays
|
||||
// TODO - convert between odrive api units and user-defined units like degrees and rpm
|
||||
export let odriveUnits = {
|
||||
"4": {
|
||||
"config.brake_resistance": "Ohms",
|
||||
"controller.pos_setpoint": "counts",
|
||||
"controller.vel_setpoint": "counts/s",
|
||||
"controller.current_setpoint": "Amps",
|
||||
"controller.config.vel_limit": "counts/s",
|
||||
"controller.config.vel_ramp_rate": "counts/s^2",
|
||||
"encoder.pos_estimate": "counts",
|
||||
"encoder.vel_estimate": "counts/s",
|
||||
"trap_traj.config.vel_limit": "counts/s",
|
||||
"trap_traj.config.accel_limit": "counts/s^2",
|
||||
"trap_traj.config.decel_limit": "counts/s^2",
|
||||
"motor.config.phase_resistance": "Ohms",
|
||||
"motor.config.phase_inductance": "Henries",
|
||||
"motor.config.current_lim": "Amps",
|
||||
"vbus_voltage": "Volts",
|
||||
},
|
||||
"5": {
|
||||
"config.brake_resistance": "Ohms",
|
||||
"controller.input_pos": "turns",
|
||||
"controller.input_vel": "turns/s",
|
||||
"controller.torque_setpoint": "Amps",
|
||||
"controller.input_torque": "N/m",
|
||||
"controller.config.vel_limit": "turns/s",
|
||||
"controller.config.vel_ramp_rate": "turns/s^2",
|
||||
"encoder.pos_estimate": "turns",
|
||||
"encoder.vel_estimate": "turns/s",
|
||||
"trap_traj.config.vel_limit": "turns/s",
|
||||
"trap_traj.config.accel_limit": "turns/s^2",
|
||||
"trap_traj.config.decel_limit": "turns/s^2",
|
||||
"motor.config.phase_resistance": "Ohms",
|
||||
"motor.config.phase_inductance": "Henries",
|
||||
"motor.config.current_lim": "Amps",
|
||||
"motor.config.torque_lim": "N/m",
|
||||
"vbus_voltage": "Volts",
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
// general utilities for use in the GUI
|
||||
|
||||
// given an object and a test function, return a new object where only
|
||||
// the values where test(value) == true are kept, as well as the parents
|
||||
// of those objects. Nested non-matching children are set to {}
|
||||
// ex: obj = {parent: {child1: {child2: "hello"}, child1a: {child2a: "world"}}}, test = ((o) => o == "hello"),
|
||||
// filterBy(obj, test) -> {parent: {child1: {child2: "hello"}, child1a: {}}}
|
||||
|
||||
export let filterBy = (obj, test) => {
|
||||
let retobj = {};
|
||||
Object.entries(obj).forEach(([key, val]) => {
|
||||
if (test(val)) {
|
||||
retobj[key] = val;
|
||||
}
|
||||
else if (typeof val == 'object' && val != null) {
|
||||
retobj[key] = filterBy(val, test);
|
||||
}
|
||||
})
|
||||
return retobj;
|
||||
}
|
||||
|
||||
// given an object and test function, delete values where test(val) == true
|
||||
// modifies obj in place
|
||||
export let deleteBy = (obj, test) => {
|
||||
Object.keys(obj).forEach((key) => {
|
||||
if (typeof obj[key] == 'object' && obj[key] != null) {
|
||||
deleteBy(obj[key], test);
|
||||
}
|
||||
if (test(obj[key])) {
|
||||
delete obj[key];
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// sets up a timeout to wait for fun() to evaluate to true;
|
||||
// fun must return {done: boolean, data: <whatever>}
|
||||
export let waitFor = (fun) => {
|
||||
return new Promise(resolve => {
|
||||
let check = () => {
|
||||
let res = fun();
|
||||
if (res.done) {
|
||||
resolve(res.data);
|
||||
}
|
||||
else {
|
||||
setTimeout(check,100);
|
||||
}
|
||||
}
|
||||
check();
|
||||
});
|
||||
}
|
||||
|
||||
// utility function to allow time for things to happen
|
||||
// async wait
|
||||
export let wait = (time) => {
|
||||
return new Promise(resolve => {
|
||||
setTimeout(() => resolve(), time);
|
||||
});
|
||||
}
|
||||
|
||||
export let pathsFromTree = (tree) => {
|
||||
let flatpaths = [];
|
||||
let path = [];
|
||||
let pathFromTree = (tree) => {
|
||||
for (const key of Object.keys(tree)) {
|
||||
path.push(key);
|
||||
if (tree[key] != null && typeof tree[key] == "object") {
|
||||
pathFromTree(tree[key]);
|
||||
}
|
||||
else {
|
||||
flatpaths.push(path.join('.'));
|
||||
}
|
||||
path.pop();
|
||||
}
|
||||
}
|
||||
// array path
|
||||
pathFromTree(tree);
|
||||
return flatpaths;
|
||||
}
|
||||
|
||||
export let numberDisplay = (val) => {
|
||||
// if a number can be represented with 3 decimals, return it in that form
|
||||
// otherwise, return scientific notation
|
||||
let retVal = '';
|
||||
try {
|
||||
retVal = parseFloat(val).toFixed(3);
|
||||
if (retVal == '0.000' && val != 0 || retVal.length > 7) {
|
||||
retVal = parseFloat(val).toExponential(3);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
return retVal;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import Vue from 'vue'
|
||||
import App from './App.vue'
|
||||
import store from './store'
|
||||
import Tooltip from 'vue-directive-tooltip';
|
||||
import 'vue-directive-tooltip/dist/vueDirectiveTooltip.css';
|
||||
import 'typeface-roboto/index.css';
|
||||
|
||||
Vue.config.productionTip = false
|
||||
Vue.use(Tooltip);
|
||||
|
||||
new Vue({
|
||||
store,
|
||||
render: h => h(App)
|
||||
}).$mount('#app')
|
||||
@@ -0,0 +1,2 @@
|
||||
import { ipcRenderer } from 'electron'
|
||||
window.ipcRenderer = ipcRenderer
|
||||
@@ -0,0 +1,420 @@
|
||||
import Vue from 'vue';
|
||||
import Vuex from 'vuex';
|
||||
import TuningDashOld from "./assets/dashboards/Tuning_0_4_12.json";
|
||||
import TuningDashNew from "./assets/dashboards/Tuning_0_5_1.json";
|
||||
import * as socketio from "./comms/socketio";
|
||||
import {filterBy, deleteBy } from "./lib/utils.js";
|
||||
//import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
|
||||
|
||||
|
||||
Vue.use(Vuex);
|
||||
|
||||
export default new Vuex.Store({
|
||||
// state is the data for this app
|
||||
state: {
|
||||
odrives: Object,
|
||||
odriveConfigs: Object,
|
||||
odriveParameters: Object,
|
||||
odriveFunctions: Object,
|
||||
axes: Array,
|
||||
odriveServerAddress: String,
|
||||
serverConnected: Boolean,
|
||||
ODrivesConnected: Object,
|
||||
serverOutput: [],
|
||||
dashboards: [
|
||||
{
|
||||
name: "Start",
|
||||
component: "Start",
|
||||
},
|
||||
],
|
||||
timeSampleStart: 0,
|
||||
sampledProperties: [], // make this an object where the full path is a key and the value is the sampled var
|
||||
propSamples: { time: [] }, // {time: [time values], ...path: [path var values]}
|
||||
newData: false,
|
||||
sampling: false,
|
||||
currentDash: "Start",
|
||||
firstConn: false,
|
||||
wizardMotor: "odrive0",
|
||||
},
|
||||
// mutations are functions that change the data
|
||||
mutations: {
|
||||
setWizardMotor(state, odrive) {
|
||||
// odrive is string, "odrive0" or "odriveN"
|
||||
state.wizardMotor = odrive;
|
||||
},
|
||||
setDash(state, dashName) {
|
||||
state.currentDash = dashName;
|
||||
},
|
||||
setOdrives(state, odrives) {
|
||||
state.odrives = odrives;
|
||||
if (state.firstConn == false) {
|
||||
// first time we're getting odrive data, add correct config page
|
||||
let TuningDash;
|
||||
if (state.odrives.odrive0.fw_version_minor.val == "5") {
|
||||
TuningDash = TuningDashNew;
|
||||
}
|
||||
else if (state.odrives.odrive0.fw_version_minor.val == "4") {
|
||||
TuningDash = TuningDashOld;
|
||||
}
|
||||
else {
|
||||
TuningDash = TuningDashNew;
|
||||
}
|
||||
state.dashboards.push({
|
||||
name: "Wizard",
|
||||
component: "Wizard",
|
||||
});
|
||||
state.dashboards.push(TuningDash);
|
||||
// plots will have variables associated, add them to sampled variables list
|
||||
for (const plot of TuningDash.plots) {
|
||||
for (const plotVar of plot.vars) {
|
||||
//addsampledprop(path);
|
||||
let path = plotVar.path;
|
||||
if (!(path in state.sampledProperties)) {
|
||||
let newPath = path.split('.');
|
||||
newPath.splice(0, 1);
|
||||
state.sampledProperties.push(newPath.join('.'));
|
||||
state.propSamples[newPath.join('.')] = [];
|
||||
}
|
||||
socketio.sendEvent({
|
||||
type: 'sampledVarNames',
|
||||
data: {
|
||||
paths: state.sampledProperties
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
state.firstConn = true;
|
||||
},
|
||||
setOdriveConfigs(state, payload) {
|
||||
state.odriveConfigs['full'] = payload.full;
|
||||
state.odriveConfigs['functions'] = payload.functions;
|
||||
state.odriveConfigs['params'] = payload.params;
|
||||
state.odriveConfigs['writeAble'] = payload.writeAble;
|
||||
state.odriveConfigs['writeAbleNumeric'] = payload.writeAbleNumeric;
|
||||
},
|
||||
setODrivesStatus(state, obj) {
|
||||
// obj is {"odriveX": true/false}
|
||||
for (const odrive of Object.keys(obj)){
|
||||
state.ODrivesConnected[odrive] = obj[odrive];
|
||||
console.log(state.ODrivesConnected);
|
||||
}
|
||||
},
|
||||
setAxes(state, axes) {
|
||||
state.axes = axes;
|
||||
},
|
||||
setServerAddress(state, address) {
|
||||
state.odriveServerAddress = address;
|
||||
},
|
||||
updateOdriveProp(state, payload) {
|
||||
// need to use Vue.set!!!
|
||||
// payload is {path, value}
|
||||
//
|
||||
const createNestedObject = (odrive, path) => {
|
||||
let ref = odrive;
|
||||
let keys = path.split('.');
|
||||
for (const key of keys) {
|
||||
ref = ref[key];
|
||||
}
|
||||
return ref;
|
||||
};
|
||||
let val = payload.val
|
||||
if (val == "Infinity") {
|
||||
val = Number.POSITIVE_INFINITY;
|
||||
console.log("val is infinity");
|
||||
}
|
||||
else if (val == "-Infinity") {
|
||||
val = Number.NEGATIVE_INFINITY;
|
||||
}
|
||||
Vue.set(createNestedObject(state.odrives, payload.path), "val", val);
|
||||
|
||||
},
|
||||
addSampledProperty(state, path) {
|
||||
if (!(path in state.sampledProperties)) {
|
||||
let newPath = path.split('.');
|
||||
newPath.splice(0, 1);
|
||||
state.sampledProperties.push(newPath.join('.'));
|
||||
state.propSamples[newPath.join('.')] = [];
|
||||
}
|
||||
socketio.sendEvent({
|
||||
type: 'sampledVarNames',
|
||||
data: {
|
||||
paths: state.sampledProperties
|
||||
}
|
||||
});
|
||||
},
|
||||
removeSampledProperty(state, path) {
|
||||
let newPath = path.split('.');
|
||||
newPath.splice(0, 1);
|
||||
const index = state.sampledProperties.indexOf(newPath.join('.'));
|
||||
if (index > -1) {
|
||||
state.sampledProperties.splice(index, 1);
|
||||
}
|
||||
},
|
||||
updateSampledProperty(state, payload) {
|
||||
// payload is object of paths and values
|
||||
for (const path of Object.keys(payload)) {
|
||||
state.propSamples[path].push(payload[path]);
|
||||
if (state.propSamples[path].length > 250) {
|
||||
state.propSamples[path].splice(0, 1); // emulate circular buffer
|
||||
}
|
||||
}
|
||||
state.propSamples["time"].push((Date.now() - state.timeSampleStart) / 1000);
|
||||
if (state.propSamples["time"].length > 250) {
|
||||
state.propSamples["time"].splice(0, 1);
|
||||
}
|
||||
state.newData = true;
|
||||
},
|
||||
logServerMessage(state, payload) {
|
||||
// payload is string
|
||||
state.serverOutput.push(payload);
|
||||
// cut off to 1000 lines
|
||||
if (state.serverOutput.length > 1000) {
|
||||
state.serverOutput.splice(0,1);
|
||||
}
|
||||
},
|
||||
setServerStatus(state, val) {
|
||||
state.serverConnected = val;
|
||||
},
|
||||
removeCtrlFromDash(state, obj) {
|
||||
// obj is {dash: dashID, path: control path}
|
||||
for (const dash of state.dashboards) {
|
||||
if (obj.dashID == dash.id) {
|
||||
for (const control of dash.controls) {
|
||||
if (obj.path == control.path) {
|
||||
dash.controls.splice(dash.controls.indexOf(control), 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
removeActionFromDash(state, obj) {
|
||||
// obj is {dashID: dashID, actionID: action ID}
|
||||
for (const dash of state.dashboards) {
|
||||
if (obj.dashID == dash.id) {
|
||||
for (const action of dash.actions) {
|
||||
if (obj.actionID == action.id) {
|
||||
dash.actions.splice(dash.actions.indexOf(action), 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
removePlotFromDash(state, obj) {
|
||||
// obj is {dashID: dash ID, plotID: plot ID}
|
||||
for (const dash of state.dashboards) {
|
||||
if (obj.dashID == dash.id) {
|
||||
for (const plot of dash.plots) {
|
||||
if (obj.plotID == plot.name) {
|
||||
dash.plots.splice(dash.plots.indexOf(plot), 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
setActionVal(state, obj) {
|
||||
// obj is {dashID: dash ID, actionID: action ID, val: val}
|
||||
for (const dash of state.dashboards) {
|
||||
if (obj.dashID == dash.id) {
|
||||
for (const action of dash.actions) {
|
||||
if (obj.actionID == action.id) {
|
||||
action.val = obj.val;
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
// actions trigger mutations
|
||||
actions: {
|
||||
getOdrives() {
|
||||
socketio.sendEvent({
|
||||
type: 'getODrives',
|
||||
});
|
||||
},
|
||||
getOdriveConfigs(context) {
|
||||
// transform ODrive JSON
|
||||
function treeParse(odriveObj) {
|
||||
let retObj = {};
|
||||
for (const key of Object.keys(odriveObj)) {
|
||||
if (typeof odriveObj[key] === 'object' && odriveObj[key] !== null) {
|
||||
// check if "val" is a valid key
|
||||
if (Object.prototype.hasOwnProperty.call(odriveObj[key], "val")) {
|
||||
// parse from string to a type that we care about
|
||||
switch (odriveObj[key]["type"]) {
|
||||
case "str": {
|
||||
// for handling infinity
|
||||
let val;
|
||||
if (odriveObj[key]["val"] == 'Infinity') {
|
||||
val = Number.POSITIVE_INFINITY;
|
||||
}
|
||||
else if (odriveObj[key]["val"] == '-Infinity') {
|
||||
val = Number.NEGATIVE_INFINITY;
|
||||
}
|
||||
retObj[key] = val;
|
||||
break;
|
||||
}
|
||||
case "float":
|
||||
retObj[key] = parseFloat(parseFloat(odriveObj[key]["val"]).toFixed(3));
|
||||
break;
|
||||
case "int8":
|
||||
case "int16":
|
||||
case "int32":
|
||||
case "int64":
|
||||
case "uint8":
|
||||
case "uint16":
|
||||
case "uint32":
|
||||
case "uint64":
|
||||
retObj[key] = parseInt(odriveObj[key]["val"]);
|
||||
break;
|
||||
case "bool":
|
||||
retObj[key] = odriveObj[key]["val"] == 'True';
|
||||
break;
|
||||
default:
|
||||
retObj[key] = odriveObj[key]["val"];
|
||||
}
|
||||
}
|
||||
else {
|
||||
retObj[key] = treeParse(odriveObj[key]);
|
||||
}
|
||||
}
|
||||
else if (odriveObj[key] == "function") {
|
||||
retObj[key] = "function";
|
||||
}
|
||||
else {
|
||||
retObj[key] = odriveObj[key];
|
||||
}
|
||||
}
|
||||
return retObj;
|
||||
}
|
||||
// full parameter tree
|
||||
// create object containing only ODrive functions
|
||||
let fullTree = treeParse(context.state.odrives);
|
||||
let fcns = filterBy(fullTree, ((o) => o == "function"));
|
||||
deleteBy(fcns, ((o) => o.constructor == Object && Object.keys(o).length == 0));
|
||||
|
||||
// create object containing only ODrive parameters
|
||||
let params = filterBy(fullTree, ((o) => o != "function" && typeof o != "object"));
|
||||
deleteBy(params, ((o) => o.constructor == Object && Object.keys(o).length == 0))
|
||||
|
||||
// create object containing only ODrive parameters with write access
|
||||
let writeAble = filterBy(context.state.odrives, ((o) => o['readonly'] == false));
|
||||
deleteBy(writeAble, ((o) => o.constructor == Object && Object.keys(o).length == 0))
|
||||
|
||||
// create object of only ODrive parameters that are writeable and numeric
|
||||
let writeAbleNumeric = filterBy(context.state.odrives, ((o) => o['readonly'] == false && ['float', 'int', 'bool'].includes(o['type'])));
|
||||
deleteBy(writeAbleNumeric, ((o) => o.constructor == Object && Object.keys(o).length == 0))
|
||||
|
||||
context.commit('setOdriveConfigs', {full: fullTree,
|
||||
functions: fcns,
|
||||
params: params,
|
||||
writeAble: treeParse(writeAble),
|
||||
writeAbleNumeric: treeParse(writeAbleNumeric)});
|
||||
},
|
||||
getAxes(context) {
|
||||
let axes = [];
|
||||
//for each connected odrive, collect axes and display them
|
||||
for (const odrive of Object.keys(context.state.odrives)) {
|
||||
if ('axis0' in context.state.odrives[odrive]) {
|
||||
axes.push({
|
||||
name: `${odrive}.axis0`,
|
||||
});
|
||||
}
|
||||
if ('axis1' in context.state.odrives[odrive]) {
|
||||
axes.push({
|
||||
name: `${odrive}.axis1`,
|
||||
});
|
||||
}
|
||||
}
|
||||
context.commit('setAxes', axes);
|
||||
},
|
||||
setServerAddress(context, address) {
|
||||
context.commit('setServerAddress', address);
|
||||
socketio.setUrl(address);
|
||||
socketio.addEventListener({
|
||||
type: "connect",
|
||||
callback: () => {
|
||||
context.commit("setServerStatus", true);
|
||||
console.log('connected to server');
|
||||
//context.dispatch("getOdrives");
|
||||
socketio.sendEvent({
|
||||
type: "findODrives",
|
||||
});
|
||||
}
|
||||
});
|
||||
socketio.addEventListener({
|
||||
type: "odrive-found",
|
||||
callback: () => {
|
||||
console.log("odrive-found recieved from server");
|
||||
context.dispatch("getOdrives");
|
||||
}
|
||||
})
|
||||
socketio.addEventListener({
|
||||
type: "disconnect",
|
||||
callback: () => {
|
||||
context.commit("setServerStatus", false);
|
||||
console.log('server disconnect');
|
||||
socketio.closeSocket();
|
||||
context.commit('setAxes', []);
|
||||
}
|
||||
});
|
||||
socketio.addEventListener({
|
||||
type: "sampledData",
|
||||
callback: message => {
|
||||
context.commit("updateSampledProperty", JSON.parse(message));
|
||||
}
|
||||
});
|
||||
socketio.addEventListener({
|
||||
type: "samplingEnabled",
|
||||
callback: () => {
|
||||
socketio.sendEvent({
|
||||
type: "startSampling"
|
||||
});
|
||||
}
|
||||
});
|
||||
socketio.addEventListener({
|
||||
type: "odrives",
|
||||
callback: odrives => {
|
||||
context.commit('setOdrives', JSON.parse(odrives));
|
||||
context.dispatch('getOdriveConfigs');
|
||||
context.dispatch('getAxes');
|
||||
}
|
||||
});
|
||||
// getVal event gets sent to server, server emits ODriveProperty event with path and val of property
|
||||
socketio.addEventListener({
|
||||
type: "ODriveProperty",
|
||||
callback: retmsg => {
|
||||
// retmsg is {path, val}
|
||||
context.commit('updateOdriveProp', JSON.parse(retmsg));
|
||||
}
|
||||
});
|
||||
socketio.addEventListener({
|
||||
type: "odrive-disconnected",
|
||||
callback: (odrive_name) => {
|
||||
console.log(odrive_name + " disconnected");
|
||||
//console.log("restarting server...");
|
||||
//window.ipcRenderer.send('kill-server');
|
||||
//window.ipcRenderer.send('start-server');
|
||||
//context.dispatch('setServerAddress', context.state.odriveServerAddress);
|
||||
}
|
||||
});
|
||||
socketio.addEventListener({
|
||||
type: "odrives-status",
|
||||
callback: (odrives_status) => {
|
||||
console.log("From odrives-status msg " + odrives_status);
|
||||
context.commit('setODrivesStatus', JSON.parse(odrives_status));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,561 @@
|
||||
<template>
|
||||
<div class="dashboard">
|
||||
<!-- PARAMETER DROPDOWN MENU -->
|
||||
<div v-show="paramsVisible" class="dropdown">
|
||||
<div class="card dropdown-content">
|
||||
<div>
|
||||
<button class="close-button" @click="hideTree">X</button>
|
||||
Parameters
|
||||
</div>
|
||||
<div class="param-tree">
|
||||
<json-view
|
||||
:data="treeParams"
|
||||
:rootKey="'odrives'"
|
||||
v-on:selected="addVarToElement"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dashboard_container">
|
||||
<div class="controls">
|
||||
<template v-for="(control, index) in dash.controls">
|
||||
<component
|
||||
:is="control.controlType"
|
||||
:key="index + '-control'"
|
||||
:path="control.path"
|
||||
:name="control.name"
|
||||
:options="control.options"
|
||||
:odrives="odrives"
|
||||
:dashID="dash.id"
|
||||
/>
|
||||
</template>
|
||||
<div class="control-buttons">
|
||||
<div class="add-button card" @click="addComponent('control')">Add Control</div>
|
||||
<div class="add-button card" @click="addComponent('slider')">Add Slider</div>
|
||||
<div class="add-button card" @click="addComponent('function')">Add Function</div>
|
||||
</div>
|
||||
<template v-for="(action, index) in dash.actions">
|
||||
<component
|
||||
:is="action.actionType"
|
||||
:id="action.id"
|
||||
:key="index + '-action'"
|
||||
:path="action.path"
|
||||
:odrives="odrives"
|
||||
:initVal="action.val"
|
||||
:dashID="dash.id"
|
||||
:options="action.options"
|
||||
/>
|
||||
</template>
|
||||
<div class="add-button card" @click="addComponent('action')">Add Action</div>
|
||||
</div>
|
||||
<div class="plots">
|
||||
<template v-for="(plot, index) in dash.plots">
|
||||
<plot
|
||||
:plot="plot"
|
||||
:key="index"
|
||||
:name="plot.name"
|
||||
:dashID="dash.id"
|
||||
v-on:add-var="currentPlot=plot.name;addComponent('plot')"
|
||||
/>
|
||||
</template>
|
||||
<div class="add-button card" @click="addPlot">Add Plot</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
//this component will get passed a list of controls and plots
|
||||
//display controls on the left and plots on the right?
|
||||
//leave full names for plots
|
||||
//collate controls into individual cards based on the deepest common level
|
||||
import CtrlBoolean from "../components/controls/CtrlBoolean.vue";
|
||||
import CtrlNumeric from "../components/controls/CtrlNumeric.vue";
|
||||
import CtrlFunction from "../components/controls/CtrlFunction.vue";
|
||||
import CtrlSlider from "../components/controls/CtrlSlider.vue";
|
||||
import CtrlEnum from "../components/controls/CtrlEnum.vue";
|
||||
import Plot from "../components/plots/Plot.vue";
|
||||
import Action from "../components/actions/Action.vue";
|
||||
import ActionEnum from "../components/actions/ActionEnum.vue"
|
||||
import { JSONView } from "vue-json-component";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
let plotColors = [
|
||||
"#1f77b4", // blue
|
||||
"#ff7f0e", // orange
|
||||
"#2ca02c", // green
|
||||
"#d62728", // red
|
||||
"#9467bd", // purple
|
||||
"#8c564b", // brown
|
||||
"#e377c2", // pink
|
||||
"#7f7f7f", // gray
|
||||
"#bcbd22", // olive
|
||||
"#17becf", // cyan
|
||||
];
|
||||
|
||||
let odriveEnums = {
|
||||
// axis requested state
|
||||
requested_state: [
|
||||
{
|
||||
text: "Undefined",
|
||||
value: 0,
|
||||
},
|
||||
{
|
||||
text: "Idle",
|
||||
value: 1,
|
||||
},
|
||||
{
|
||||
text: "Startup Sequence",
|
||||
value: 2,
|
||||
},
|
||||
{
|
||||
text: "Full Calibration Sequence",
|
||||
value: 3,
|
||||
},
|
||||
{
|
||||
text: "Motor Calibration",
|
||||
value: 4,
|
||||
},
|
||||
{
|
||||
text: "Sensorless Control",
|
||||
value: 5,
|
||||
},
|
||||
{
|
||||
text: "Encoder Index Search",
|
||||
value: 6,
|
||||
},
|
||||
{
|
||||
text: "Encoder Offset Calibration",
|
||||
value: 7,
|
||||
},
|
||||
{
|
||||
text: "Closed Loop Control",
|
||||
value: 8,
|
||||
},
|
||||
{
|
||||
text: "Lockin Spin",
|
||||
value: 9,
|
||||
},
|
||||
{
|
||||
text: "Encoder Direction Find",
|
||||
value: 10,
|
||||
},
|
||||
{
|
||||
text: "Homing",
|
||||
value: 11,
|
||||
},
|
||||
{
|
||||
text: "Encoder Hall Polarity Calibration",
|
||||
value: 12,
|
||||
},
|
||||
{
|
||||
text: "Encoder Hall Phase Calibration",
|
||||
value: 13,
|
||||
}
|
||||
],
|
||||
current_state: [
|
||||
{
|
||||
text: "Undefined",
|
||||
value: 0,
|
||||
},
|
||||
{
|
||||
text: "Idle",
|
||||
value: 1,
|
||||
},
|
||||
{
|
||||
text: "Startup Sequence",
|
||||
value: 2,
|
||||
},
|
||||
{
|
||||
text: "Full Calibration Sequence",
|
||||
value: 3,
|
||||
},
|
||||
{
|
||||
text: "Motor Calibration",
|
||||
value: 4,
|
||||
},
|
||||
{
|
||||
text: "Sensorless Control",
|
||||
value: 5,
|
||||
},
|
||||
{
|
||||
text: "Encoder Index Search",
|
||||
value: 6,
|
||||
},
|
||||
{
|
||||
text: "Encoder Offset Calibration",
|
||||
value: 7,
|
||||
},
|
||||
{
|
||||
text: "Closed Loop Control",
|
||||
value: 8,
|
||||
},
|
||||
{
|
||||
text: "Lockin Spin",
|
||||
value: 9,
|
||||
},
|
||||
{
|
||||
text: "Encoder Direction Find",
|
||||
value: 10,
|
||||
},
|
||||
{
|
||||
text: "Homing",
|
||||
value: 11,
|
||||
},
|
||||
{
|
||||
text: "Encoder Hall Polarity Calibration",
|
||||
value: 12,
|
||||
},
|
||||
{
|
||||
text: "Encoder Hall Phase Calibration",
|
||||
value: 13,
|
||||
}
|
||||
],
|
||||
// encoder mode
|
||||
mode: [
|
||||
{
|
||||
text: "Incremental",
|
||||
value: 0,
|
||||
},
|
||||
{
|
||||
text: "Hall Effect",
|
||||
value: 1,
|
||||
},
|
||||
{
|
||||
text: "Sine Cosine",
|
||||
value: 2,
|
||||
},
|
||||
{
|
||||
text: "CUI Absolute",
|
||||
value: 256,
|
||||
},
|
||||
{
|
||||
text: "AMS Absolute",
|
||||
value: 257,
|
||||
},
|
||||
{
|
||||
text: "AEAT Absolute",
|
||||
value: 258,
|
||||
},
|
||||
{
|
||||
text: "RLS Absolute",
|
||||
value: 259,
|
||||
},
|
||||
],
|
||||
// motor type
|
||||
motor_type: [
|
||||
{
|
||||
text: "High Current",
|
||||
value: 0,
|
||||
},
|
||||
{
|
||||
text: "Gimbal",
|
||||
value: 2,
|
||||
},
|
||||
{
|
||||
text: "Induction",
|
||||
value: 3,
|
||||
},
|
||||
],
|
||||
// controller control mode
|
||||
control_mode: [
|
||||
{
|
||||
text: "Voltage Control",
|
||||
value: 0,
|
||||
},
|
||||
{
|
||||
text: "Torque Control",
|
||||
value: 1,
|
||||
},
|
||||
{
|
||||
text: "Velocity Control",
|
||||
value: 2,
|
||||
},
|
||||
{
|
||||
text: "Position Control",
|
||||
value: 3,
|
||||
},
|
||||
],
|
||||
input_mode: [
|
||||
{
|
||||
text: "Inactive",
|
||||
value: 0
|
||||
},
|
||||
{
|
||||
text: "Passthrough",
|
||||
value: 1
|
||||
},
|
||||
{
|
||||
text: "Velocity Ramp",
|
||||
value: 2
|
||||
},
|
||||
{
|
||||
text: "Position Filter",
|
||||
value: 3
|
||||
},
|
||||
{
|
||||
text: "Mix Channels",
|
||||
value: 4
|
||||
},
|
||||
{
|
||||
text: "Trapezoidal Trajectory",
|
||||
value: 5
|
||||
},
|
||||
{
|
||||
text: "Torque Ramp",
|
||||
value: 6
|
||||
},
|
||||
{
|
||||
text: "Mirror",
|
||||
value: 7
|
||||
},
|
||||
{
|
||||
text: "Tuning",
|
||||
value: 8
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
export default {
|
||||
name: "Dashboard",
|
||||
components: {
|
||||
CtrlBoolean,
|
||||
CtrlNumeric,
|
||||
CtrlFunction,
|
||||
CtrlSlider,
|
||||
CtrlEnum,
|
||||
Plot,
|
||||
Action,
|
||||
ActionEnum,
|
||||
"json-view": JSONView,
|
||||
},
|
||||
props: ["dash", "odrives"],
|
||||
data() {
|
||||
return {
|
||||
paramsVisible: false,
|
||||
addCompType: undefined,
|
||||
currentPlot: undefined,
|
||||
treeParams: undefined,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
deleteAction(e) {
|
||||
this.$emit("delete-action", e);
|
||||
console.log(e);
|
||||
},
|
||||
deletePlot(e) {
|
||||
this.$emit("delete-plot", e);
|
||||
},
|
||||
addVar(e) {
|
||||
this.$emit("add-var", e);
|
||||
},
|
||||
showTree() {
|
||||
//show the parameter tree
|
||||
this.paramsVisible = true;
|
||||
},
|
||||
hideTree() {
|
||||
this.paramsVisible = false;
|
||||
this.addCompType = undefined;
|
||||
},
|
||||
addComponent(componentType) {
|
||||
this.addCompType = componentType;
|
||||
switch (this.addCompType) {
|
||||
case "control":
|
||||
this.treeParams = this.$store.state.odriveConfigs['params'];
|
||||
console.log(this.treeParams);
|
||||
break;
|
||||
case "plot":
|
||||
this.treeParams = this.$store.state.odriveConfigs['params'];
|
||||
break;
|
||||
case "slider":
|
||||
this.treeParams = this.$store.state.odriveConfigs['writeAbleNumeric'];
|
||||
break;
|
||||
case "action":
|
||||
this.treeParams = this.$store.state.odriveConfigs['writeAble'];
|
||||
break;
|
||||
case "function":
|
||||
this.treeParams = this.$store.state.odriveConfigs['functions'];
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
this.paramsVisible = true;
|
||||
},
|
||||
addVarToElement(e) {
|
||||
//when the parameter tree is open and a parameter is clicked,
|
||||
//add the clicked parameter to the list of controls for the
|
||||
//current dashboard
|
||||
console.log(e);
|
||||
switch (this.addCompType) {
|
||||
case "control":
|
||||
switch (typeof e.value) {
|
||||
case "boolean":
|
||||
this.dash.controls.push({
|
||||
controlType: "CtrlBoolean",
|
||||
path: e.path,
|
||||
});
|
||||
//this.$store.commit("addSampledProperty", e.path);
|
||||
break;
|
||||
case "number":
|
||||
if (Object.keys(odriveEnums).includes(e.key)){
|
||||
this.dash.controls.push({
|
||||
controlType: "CtrlEnum",
|
||||
path: e.path,
|
||||
options: odriveEnums[e.key],
|
||||
})
|
||||
}
|
||||
else {
|
||||
this.dash.controls.push({
|
||||
controlType: "CtrlNumeric",
|
||||
path: e.path,
|
||||
});
|
||||
}
|
||||
//this.$store.commit("addSampledProperty", e.path);
|
||||
break;
|
||||
case "string":
|
||||
this.dash.controls.push({
|
||||
controlType: "CtrlFunction",
|
||||
path: e.path,
|
||||
});
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case "function":
|
||||
this.dash.controls.push({
|
||||
controlType: "CtrlFunction",
|
||||
path: e.path,
|
||||
});
|
||||
break;
|
||||
case "plot":
|
||||
// add the selected element to the plot var list
|
||||
// add the selected element to the sampling var list
|
||||
// find the plot, append path to plot.vars
|
||||
console.log(e);
|
||||
for (const plot of this.dash.plots) {
|
||||
if (plot.name == this.currentPlot) {
|
||||
plot.vars.push({
|
||||
path: e.path,
|
||||
color: plotColors[plot.vars.length % plotColors.length],
|
||||
});
|
||||
this.$store.commit("addSampledProperty", e.path);
|
||||
console.log(plot);
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case "action":
|
||||
{
|
||||
// add an action to the current dash
|
||||
let id = uuidv4();
|
||||
console.log("action, key = " + e.key);
|
||||
if (Object.keys(odriveEnums).includes(e.key)){
|
||||
this.dash.actions.push({
|
||||
actionType: "ActionEnum",
|
||||
id: id,
|
||||
path: e.path,
|
||||
options: odriveEnums[e.key],
|
||||
})
|
||||
}
|
||||
else {
|
||||
this.dash.actions.push({
|
||||
actionType: "Action",
|
||||
id: id,
|
||||
path: e.path,
|
||||
val: undefined,
|
||||
});
|
||||
}
|
||||
}
|
||||
break;
|
||||
case "slider":
|
||||
// add a slider to the list of controls if the selected item is valid (numeric)
|
||||
switch (typeof e.value) {
|
||||
case "number":
|
||||
this.dash.controls.push({
|
||||
controlType: "CtrlSlider",
|
||||
path: e.path,
|
||||
});
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
addPlot() {
|
||||
let plotId = uuidv4();
|
||||
this.dash.plots.push({
|
||||
name: plotId,
|
||||
vars: [],
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.dashboard {
|
||||
background-color: var(--bg-color);
|
||||
height: 100vh;
|
||||
max-height: 100vh;
|
||||
width: 100vw;
|
||||
padding-top: var(--top-height);
|
||||
padding-bottom: var(--bottom-height);
|
||||
}
|
||||
|
||||
.dashboard_container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
height: 95vh;
|
||||
}
|
||||
.controls {
|
||||
flex-grow: 1;
|
||||
border-right: 1px solid lightgrey;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
.add-button {
|
||||
background-color: lightcyan;
|
||||
width: 120px;
|
||||
margin: 10px auto;
|
||||
margin-left: 10px;
|
||||
margin-right: 10px;
|
||||
text-align: center;
|
||||
border-radius: 20px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.add-button:active {
|
||||
background-color: lightblue;
|
||||
}
|
||||
|
||||
.plots {
|
||||
flex-grow: 1;
|
||||
max-height: 94vh;
|
||||
overflow-y: scroll;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.control-buttons {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.dropdown {
|
||||
position: absolute;
|
||||
padding: 0 0;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.dropdown-content {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.param-tree {
|
||||
overflow-y: scroll;
|
||||
max-height: 85vh;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,118 @@
|
||||
<template>
|
||||
<div class="home">
|
||||
<div class="logo">
|
||||
<img alt="Odrive Logo" src="../assets/odrive_logo.png">
|
||||
</div>
|
||||
<div class="home_text">
|
||||
To set up your ODrive, connect it and power it up.
|
||||
</div>
|
||||
<div class="connected-container">
|
||||
<input type="text" :class="{ notConnected: notConnected, connected: connected}" v-on:change="setUrl" :value="serverAddress">
|
||||
</div>
|
||||
<button class="show-msg-button" @click="showServerMessages = !showServerMessages;">Having trouble? Click here for debug output</button>
|
||||
<div class="server-msgs" v-if="showServerMessages">
|
||||
<div class="server-msg" v-for="(msg, index) in serverMessages" :key="msg + index">{{msg}}</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
name: 'Home',
|
||||
components: {
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showServerMessages: false,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
connected() {
|
||||
return this.$store.state.serverConnected == true;
|
||||
},
|
||||
notConnected() {
|
||||
return this.$store.state.serverConnected != true;
|
||||
},
|
||||
serverAddress() {
|
||||
return this.$store.state.odriveServerAddress;
|
||||
},
|
||||
serverMessages() {
|
||||
if (this.$store.state.serverOutput.length >= 10) {
|
||||
let len = this.$store.state.serverOutput.length;
|
||||
return this.$store.state.serverOutput.slice(len-10,len);
|
||||
}
|
||||
else {
|
||||
return this.$store.state.serverOutput;
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
setUrl(e) {
|
||||
console.log(e.target.value);
|
||||
this.$store.dispatch("setServerAddress", e.target.value);
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.home {
|
||||
padding: 10% 0;
|
||||
margin: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: var(--bg-color);
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
img {
|
||||
margin: auto;
|
||||
display: block;
|
||||
max-width: 50%;
|
||||
width: auto;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.home_text {
|
||||
font-size: 1.5rem;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.connected, .notConnected {
|
||||
width: 40vw;
|
||||
margin: 0;
|
||||
padding: 10px;
|
||||
border: none;
|
||||
border-bottom: 1px solid lightgrey;
|
||||
font-size: 1.5rem;
|
||||
background-color: var(--bg-color);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
input:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.connected {
|
||||
border-bottom: 1px solid lightgreen;
|
||||
background-color: lightgreen;
|
||||
}
|
||||
|
||||
.notConnected {
|
||||
border-bottom: 1px solid red;
|
||||
}
|
||||
|
||||
.show-msg-button {
|
||||
border: none;
|
||||
background-color: var(--bg-color);
|
||||
margin-top: 0.5rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.server-msg {
|
||||
font-family: "Roboto Mono", monospace;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,395 @@
|
||||
<template>
|
||||
<div class="wizard">
|
||||
<div class="wizard-container">
|
||||
<div class="wizard-nav card">
|
||||
<span class="wizard-nav-title">Wizard Pages</span>
|
||||
<span
|
||||
v-for="page in wizardPages"
|
||||
:key="page.title"
|
||||
class="wizard-link"
|
||||
:class="{
|
||||
'active-link': currentStep == page,
|
||||
'finished-page': page.choiceMade,
|
||||
}"
|
||||
>{{ page.link }}</span
|
||||
>
|
||||
</div>
|
||||
<div class="wizard-page">
|
||||
<wizardPage
|
||||
:choices="currentStep.choices"
|
||||
:customComponents="currentStep.customComponents"
|
||||
:pageComponents="currentStep.pageComponents"
|
||||
:title="currentStep.title"
|
||||
:config="wizardConfig"
|
||||
:calibrating="calibrating"
|
||||
:calStatus="calStatus"
|
||||
v-on:choice="choiceHandler"
|
||||
v-on:undo-choice="undoChoice"
|
||||
v-on:page-comp-event="pageEventHandler"
|
||||
/>
|
||||
<div class="wizard-controls">
|
||||
<!-- show breadcrumbs, back, apply, next buttons -->
|
||||
<button class="wizard-button card" @click="back">Back</button>
|
||||
<button class="wizard-button card" @click="finish">Finish</button>
|
||||
<button
|
||||
class="wizard-button card"
|
||||
:class="{ 'next-green': choiceMade }"
|
||||
v-tooltip.right="{
|
||||
content: currentStep.nextTooltip,
|
||||
class: 'tooltip-custom tooltip-other-custom fade-in',
|
||||
delay: 0,
|
||||
visible: !choiceMade && currentStep.nextTooltip != null,
|
||||
}"
|
||||
@click="next"
|
||||
>
|
||||
Next
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import configTemplate from "../assets/wizard/configTemplate.json";
|
||||
import wizardPage from "../components/wizard/wizardPage.vue";
|
||||
import odriveEnums from "../assets/odriveEnums.json";
|
||||
import { pages } from "../assets/wizard/wizard.js";
|
||||
import {
|
||||
getVal,
|
||||
putVal,
|
||||
fetchParam,
|
||||
clearErrors,
|
||||
motorCalibration,
|
||||
encoderCalibration
|
||||
} from "../lib/odrive_utils.js";
|
||||
import {wait} from "../lib/utils.js"
|
||||
|
||||
export default {
|
||||
name: "Wizard",
|
||||
components: {
|
||||
wizardPage,
|
||||
},
|
||||
props: [],
|
||||
data: function () {
|
||||
return {
|
||||
currentStep: pages.ODrive,
|
||||
wizardConfig: configTemplate,
|
||||
choiceMade: false,
|
||||
calibrating: false, // for indicating that calibration is in progress
|
||||
calStatus: undefined, // for indicating the status of a calibration attempt
|
||||
odrive: "odrive0.",
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
wizardPages() {
|
||||
return pages;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
// certain actions, like starting motor or encoder calibration,
|
||||
// require special handling. This function is used for those events
|
||||
async pageEventHandler(e) {
|
||||
this.choiceMade = false;
|
||||
if (e.data == "motor calibration") {
|
||||
let apply = () => {
|
||||
let configStub = {};
|
||||
configStub[e.axis] = {
|
||||
motor: {
|
||||
config: {
|
||||
phase_resistance: getVal(this.odrive + e.axis + ".motor.config.phase_resistance"),
|
||||
phase_inductance: getVal(this.odrive + e.axis + ".motor.config.phase_inductance"),
|
||||
}
|
||||
}
|
||||
}
|
||||
this.choiceHandler({
|
||||
choice: "Motor Calibration",
|
||||
configStub: configStub,
|
||||
hooks: [],
|
||||
});
|
||||
this.choiceMade = true;
|
||||
};
|
||||
let updateCalInfo = () => {
|
||||
// parse error code, update calibration information
|
||||
let motorError = getVal(this.odrive + e.axis + ".motor.error");
|
||||
if (
|
||||
motorError == odriveEnums.MOTOR_ERROR_PHASE_RESISTANCE_OUT_OF_RANGE ||
|
||||
motorError == odriveEnums.MOTOR_ERROR_PHASE_INDUCTANCE_OUT_OF_RANGE
|
||||
) {
|
||||
//we got the expected calibration error
|
||||
console.log("Calibration failed.");
|
||||
this.calStatus = false;
|
||||
}
|
||||
};
|
||||
|
||||
// result is motor.error
|
||||
this.calibrating = true;
|
||||
let result = await motorCalibration(this.odrive, e.axis);
|
||||
this.calibrating = false;
|
||||
fetchParam(this.odrive + e.axis + '.motor.is_calibrated');
|
||||
// no error, we're good!
|
||||
if (result == 0) {
|
||||
wait(250);
|
||||
apply();
|
||||
this.calStatus = true;
|
||||
} else {
|
||||
console.log("motor cal error" + result.data);
|
||||
this.calibrating = false;
|
||||
this.choiceMade = false;
|
||||
// clear errors, display info.
|
||||
updateCalInfo();
|
||||
clearErrors(this.odrive, e.axis);
|
||||
}
|
||||
} else if (e.data == "encoder calibration") {
|
||||
// get old CPR
|
||||
// apply CPR from this.wizardConfig
|
||||
// start calibration
|
||||
// wait for cal to finish
|
||||
// set odrive cpr back to oldVal
|
||||
let oldCPR = getVal(this.odrive + e.axis + ".encoder.config.cpr");
|
||||
console.log("oldCPR = " + oldCPR);
|
||||
console.log("axis = " + e.axis);
|
||||
|
||||
// set up the calib_scan_distance to a value that will work
|
||||
let pp = this.wizardConfig[e.axis].motor.config.pole_pairs;
|
||||
|
||||
// smallest multiple of 4pi that is bigger than pole_pairs * 2pi
|
||||
let scan_distance = pp % 0 ? pp * 2 * Math.PI : (pp + 1) * 2 * Math.PI;
|
||||
putVal(this.odrive + e.axis + ".encoder.config.calib_scan_distance", scan_distance);
|
||||
putVal(
|
||||
this.odrive + e.axis + ".encoder.config.cpr",
|
||||
this.wizardConfig[e.axis].encoder.config.cpr
|
||||
);
|
||||
|
||||
await wait(250);
|
||||
|
||||
this.calibrating = true;
|
||||
let result = await encoderCalibration(this.odrive, e.axis);
|
||||
this.calibrating = false;
|
||||
putVal(this.odrive + e.axis + ".encoder.config.cpr", oldCPR);
|
||||
fetchParam(this.odrive + e.axis + '.encoder.is_ready');
|
||||
if (result == 0){
|
||||
// no encoder error, we're good
|
||||
this.choiceMade = true;
|
||||
this.currentStep.choiceMade = true;
|
||||
this.calStatus = true;
|
||||
}
|
||||
else {
|
||||
clearErrors(this.odrive, e.axis);
|
||||
this.calStatus = false;
|
||||
}
|
||||
}
|
||||
},
|
||||
async choiceHandler(e) {
|
||||
// apply static configStub
|
||||
this.updateConfig(this.wizardConfig, e.configStub);
|
||||
|
||||
// run hooks
|
||||
for (const fn of e.hooks) {
|
||||
this.wizardConfig = fn(this.wizardConfig);
|
||||
}
|
||||
|
||||
// ugly, but a special case.
|
||||
// for motors, wait for calibration to finish before giving the green light unless motor.is_calibrated == true
|
||||
// for encoders, wait for calibration to finish unless encoder.is_ready == true
|
||||
if (
|
||||
this.currentStep == pages.Motor_0 ||
|
||||
this.currentStep == pages.Motor_1
|
||||
) {
|
||||
let axis;
|
||||
if (this.currentStep == pages.Motor_0) axis = "axis0";
|
||||
if (this.currentStep == pages.Motor_1) axis = "axis1";
|
||||
fetchParam(this.odrive + axis + ".motor.is_calibrated");
|
||||
await wait(100);
|
||||
if (getVal(this.odrive + axis + ".motor.is_calibrated") == false) {
|
||||
this.choiceMade = false;
|
||||
}
|
||||
else {
|
||||
this.choiceMade = true;
|
||||
}
|
||||
}
|
||||
else if (
|
||||
this.currentStep == pages.Encoder_0 ||
|
||||
this.currentStep == pages.Encoder_1
|
||||
) {
|
||||
let axis;
|
||||
if (this.currentStep == pages.Encoder_0) axis = "axis0";
|
||||
if (this.currentStep == pages.Encoder_1) axis = "axis1";
|
||||
fetchParam(this.odrive + axis + ".encoder.is_ready");
|
||||
await wait(100);
|
||||
if (getVal(this.odrive + axis + ".encoder.is_ready") == false) {
|
||||
this.choiceMade = false;
|
||||
}
|
||||
else {
|
||||
this.choiceMade = true;
|
||||
}
|
||||
}
|
||||
else {
|
||||
this.choiceMade = true;
|
||||
}
|
||||
this.currentStep.choiceMade = this.choiceMade;
|
||||
console.log(JSON.parse(JSON.stringify(this.wizardConfig)));
|
||||
},
|
||||
undoChoice(e) {
|
||||
this.choiceMade = false;
|
||||
this.currentStep.choiceMade = this.choiceMade;
|
||||
this.nullConfig(this.wizardConfig, e.configStub);
|
||||
console.log(JSON.parse(JSON.stringify(this.wizardConfig)));
|
||||
},
|
||||
updateConfig(config, configStub) {
|
||||
// iterate over keys in configStub
|
||||
if (configStub != null) {
|
||||
Object.keys(configStub).forEach((key) => {
|
||||
if (typeof configStub[key] == "object") {
|
||||
this.updateConfig(config[key], configStub[key]);
|
||||
} else {
|
||||
config[key] = configStub[key];
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
nullConfig(config, configStub) {
|
||||
// iterate over keys in configStub
|
||||
if (configStub != null) {
|
||||
console.log("NullConfig");
|
||||
Object.keys(configStub).forEach((key) => {
|
||||
console.log(key);
|
||||
if (typeof configStub[key] == "object") {
|
||||
this.nullConfig(config[key], configStub[key]);
|
||||
} else {
|
||||
config[key] = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
next() {
|
||||
console.log("next");
|
||||
if (this.choiceMade == true) {
|
||||
this.currentStep = pages[this.currentStep.next];
|
||||
this.choiceMade = false;
|
||||
}
|
||||
this.calStatus = undefined;
|
||||
},
|
||||
finish() {
|
||||
console.log("finish");
|
||||
this.currentStep = pages.End;
|
||||
},
|
||||
back() {
|
||||
console.log("back");
|
||||
this.currentStep = pages[this.currentStep.back];
|
||||
this.choiceMade = false;
|
||||
},
|
||||
},
|
||||
beforeDestroy() {
|
||||
for (const page of Object.keys(pages)) {
|
||||
pages[page].choiceMade = false;
|
||||
}
|
||||
let erase = (obj) => {
|
||||
Object.keys(obj).forEach((key) => {
|
||||
if (typeof obj[key] == "object" && obj[key] != null) {
|
||||
erase(obj[key]);
|
||||
} else {
|
||||
obj[key] = null;
|
||||
}
|
||||
});
|
||||
};
|
||||
erase(this.wizardConfig);
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.wizard {
|
||||
background-color: var(--bg-color);
|
||||
height: 100vh;
|
||||
max-height: 100vh;
|
||||
width: 100vw;
|
||||
padding-top: var(--top-height);
|
||||
padding-bottom: var(--bottom-height);
|
||||
}
|
||||
|
||||
.wizard-nav {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: fit-content;
|
||||
}
|
||||
|
||||
.wizard-nav-title {
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
margin-bottom: 0.4rem;
|
||||
}
|
||||
|
||||
.wizard-link {
|
||||
margin: 0.2rem;
|
||||
}
|
||||
|
||||
.active-link {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.wizard-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
height: 95vh;
|
||||
}
|
||||
|
||||
.wizard-page {
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.wizard-controls {
|
||||
margin-top: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.active {
|
||||
background-color: var(--fg-color);
|
||||
}
|
||||
|
||||
.inactive {
|
||||
background-color: var(--bg-color);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.wizard-button:active {
|
||||
background-color: var(--bg-color);
|
||||
}
|
||||
|
||||
.next-green {
|
||||
color: green;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.vue-tooltip.tooltip-custom {
|
||||
background-color: lightyellow; /* var(--fg-color); */
|
||||
border-radius: 0px;
|
||||
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.4);
|
||||
font-family: "Roboto", sans-serif;
|
||||
color: black;
|
||||
}
|
||||
|
||||
.vue-tooltip.tooltip-custom .tooltip-arrow {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.vue-tooltip.fade-in {
|
||||
opacity: 0;
|
||||
animation: fadeIn ease 0.25s;
|
||||
animation-fill-mode: both;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.finished-page {
|
||||
color: green;
|
||||
}
|
||||
</style>
|
||||
@@ -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']
|
||||
}
|
||||
}
|
||||
}
|
||||