mirror of
https://github.com/odriverobotics/ODrive.git
synced 2026-09-22 16:14:37 +08:00
Merge branch 'devel' into libfibre
This commit is contained in:
@@ -2,7 +2,7 @@ name: Build and publish HTML documentation website
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ feature/doc_autogen ]
|
||||
branches: [ master ]
|
||||
|
||||
jobs:
|
||||
jekyll:
|
||||
@@ -52,15 +52,22 @@ jobs:
|
||||
cd /srv/jekyll/docs && \
|
||||
bundle config path vendor/bundle && \
|
||||
bundle install && \
|
||||
JEKYLL_ENV=production bundle exec jekyll build
|
||||
bundle exec jekyll build --baseurl \"\"
|
||||
cd ..
|
||||
mv docs/_site _site
|
||||
rm -rdf docs
|
||||
mv _site docs
|
||||
touch docs/.nojekyll
|
||||
"
|
||||
touch .nojekyll
|
||||
# Extra checks to reduce likelihood of defect build
|
||||
test -f docs/CNAME
|
||||
test -f docs/index.html
|
||||
|
||||
- name: Push to documentation branch
|
||||
run: |
|
||||
git config user.name "${GITHUB_ACTOR}"
|
||||
git config user.email "${GITHUB_ACTOR}@users.noreply.github.com"
|
||||
git add -f docs/_site
|
||||
git add -f docs
|
||||
git commit -m "jekyll build from Action ${GITHUB_SHA}"
|
||||
git push --force origin HEAD:${REMOTE_BRANCH}
|
||||
env:
|
||||
|
||||
@@ -58,3 +58,8 @@ ODrive\.files
|
||||
ODrive\.includes
|
||||
|
||||
Firmware/Tests/bin/
|
||||
|
||||
# GUI
|
||||
GUI/dist_electron
|
||||
GUI/node_modules
|
||||
GUI/build
|
||||
|
||||
@@ -59,14 +59,14 @@ int32_t ODriveArduino::readInt() {
|
||||
return readString().toInt();
|
||||
}
|
||||
|
||||
bool ODriveArduino::run_state(int axis, int requested_state, bool wait) {
|
||||
int timeout_ctr = 100;
|
||||
bool ODriveArduino::run_state(int axis, int requested_state, bool wait_for_idle, float timeout) {
|
||||
int timeout_ctr = (int)(timeout * 10.0f);
|
||||
serial_ << "w axis" << axis << ".requested_state " << requested_state << '\n';
|
||||
if (wait) {
|
||||
if (wait_for_idle) {
|
||||
do {
|
||||
delay(100);
|
||||
serial_ << "r axis" << axis << ".current_state\n";
|
||||
} while (readInt() != requested_state && --timeout_ctr > 0);
|
||||
} while (readInt() != AXIS_STATE_IDLE && --timeout_ctr > 0);
|
||||
}
|
||||
|
||||
return timeout_ctr > 0;
|
||||
|
||||
@@ -35,7 +35,7 @@ public:
|
||||
int32_t readInt();
|
||||
|
||||
// State helper
|
||||
bool run_state(int axis, int requested_state, bool wait);
|
||||
bool run_state(int axis, int requested_state, bool wait_for_idle, float timeout = 10.0f);
|
||||
private:
|
||||
String readString();
|
||||
|
||||
|
||||
@@ -1,14 +1,39 @@
|
||||
|
||||
// includes
|
||||
#include <HardwareSerial.h>
|
||||
#include <SoftwareSerial.h>
|
||||
#include <ODriveArduino.h>
|
||||
|
||||
// Printing with stream operator
|
||||
// Printing with stream operator helper functions
|
||||
template<class T> inline Print& operator <<(Print &obj, T arg) { obj.print(arg); return obj; }
|
||||
template<> inline Print& operator <<(Print &obj, float arg) { obj.print(arg, 4); return obj; }
|
||||
|
||||
// Serial to the ODrive
|
||||
SoftwareSerial odrive_serial(8, 9); //RX (ODrive TX), TX (ODrive RX)
|
||||
// Note: you must also connect GND on ODrive to GND on Arduino!
|
||||
|
||||
////////////////////////////////
|
||||
// Set up serial pins to the ODrive
|
||||
////////////////////////////////
|
||||
|
||||
// Below are some sample configurations.
|
||||
// You can comment out the default Teensy one and uncomment the one you wish to use.
|
||||
// You can of course use something different if you like
|
||||
// Don't forget to also connect ODrive GND to Arduino GND.
|
||||
|
||||
// Teensy 3 and 4 (all versions) - Serial1
|
||||
// pin 0: RX - connect to ODrive TX
|
||||
// pin 1: TX - connect to ODrive RX
|
||||
// See https://www.pjrc.com/teensy/td_uart.html for other options on Teensy
|
||||
HardwareSerial& odrive_serial = Serial1;
|
||||
|
||||
// Arduino Mega or Due - Serial1
|
||||
// pin 19: RX - connect to ODrive TX
|
||||
// pin 18: TX - connect to ODrive RX
|
||||
// See https://www.arduino.cc/reference/en/language/functions/communication/serial/ for other options
|
||||
// HardwareSerial& odrive_serial = Serial1;
|
||||
|
||||
// Arduino without spare serial ports (such as Arduino UNO) have to use software serial.
|
||||
// Note that this is implemented poorly and can lead to wrong data sent or read.
|
||||
// pin 8: RX - connect to ODrive TX
|
||||
// pin 9: TX - connect to ODrive RX
|
||||
// SoftwareSerial odrive_serial(8, 9);
|
||||
|
||||
|
||||
// ODrive object
|
||||
ODriveArduino odrive(odrive_serial);
|
||||
@@ -28,7 +53,7 @@ void setup() {
|
||||
// You can of course set them different if you want.
|
||||
// See the documentation or play around in odrivetool to see the available parameters
|
||||
for (int axis = 0; axis < 2; ++axis) {
|
||||
odrive_serial << "w axis" << axis << ".controller.config.vel_limit " << 22000.0f << '\n';
|
||||
odrive_serial << "w axis" << axis << ".controller.config.vel_limit " << 10.0f << '\n';
|
||||
odrive_serial << "w axis" << axis << ".motor.config.current_lim " << 11.0f << '\n';
|
||||
// This ends up writing something like "w axis0.motor.config.current_lim 10.0\n"
|
||||
}
|
||||
@@ -52,23 +77,23 @@ void loop() {
|
||||
|
||||
requested_state = ODriveArduino::AXIS_STATE_MOTOR_CALIBRATION;
|
||||
Serial << "Axis" << c << ": Requesting state " << requested_state << '\n';
|
||||
odrive.run_state(motornum, requested_state, true);
|
||||
if(!odrive.run_state(motornum, requested_state, true)) return;
|
||||
|
||||
requested_state = ODriveArduino::AXIS_STATE_ENCODER_OFFSET_CALIBRATION;
|
||||
Serial << "Axis" << c << ": Requesting state " << requested_state << '\n';
|
||||
odrive.run_state(motornum, requested_state, true);
|
||||
if(!odrive.run_state(motornum, requested_state, true, 25.0f)) return;
|
||||
|
||||
requested_state = ODriveArduino::AXIS_STATE_CLOSED_LOOP_CONTROL;
|
||||
Serial << "Axis" << c << ": Requesting state " << requested_state << '\n';
|
||||
odrive.run_state(motornum, requested_state, false); // don't wait
|
||||
if(!odrive.run_state(motornum, requested_state, false /*don't wait*/)) return;
|
||||
}
|
||||
|
||||
// Sinusoidal test move
|
||||
if (c == 's') {
|
||||
Serial.println("Executing test move");
|
||||
for (float ph = 0.0f; ph < 6.28318530718f; ph += 0.01f) {
|
||||
float pos_m0 = 20000.0f * cos(ph);
|
||||
float pos_m1 = 20000.0f * sin(ph);
|
||||
float pos_m0 = 2.0f * cos(ph);
|
||||
float pos_m1 = 2.0f * sin(ph);
|
||||
odrive.SetPosition(0, pos_m0);
|
||||
odrive.SetPosition(1, pos_m1);
|
||||
delay(5);
|
||||
|
||||
+18
-5
@@ -2,25 +2,32 @@
|
||||
Please add a note of your changes below this heading if you make a Pull Request.
|
||||
### Added
|
||||
* [Mechanical brake support](docs/mechanical-brakes.md)
|
||||
* Added periodic sending of encoder position on CAN
|
||||
|
||||
### Changed
|
||||
|
||||
* Modified encoder offset calibration to work correctly when calib_scan_distance is not a multiple of 4pi
|
||||
* Moved thermistors from being a top level object to belonging to Motor objects. Also changed errors: thermistor errors rolled into motor errors
|
||||
* Use DMA for DRV8301 setup
|
||||
* Make NVM configuration code more dynamic so that the layout doesn't have to be known at compile time.
|
||||
* GPIO initialization logic was changed. GPIOs now need to be explicitly set to the mode corresponding to the feature that they are used by. See `<odrv>.config.gpioX_mode`.
|
||||
* Previously, if two components used the same interrupt pin (e.g. step input for axis0 and axis1) then the one that was configured later would override the other one. Now this is no longer the case (the old component remains the owner of the pin).
|
||||
|
||||
### API Miration Notes
|
||||
### API Migration Notes
|
||||
|
||||
* `odrive.axis.fet_thermistor`, `odrive.axis.motor_thermistor` moved to `odrive.axis.motor` object
|
||||
* `enable_uart` and `uart_baudrate` were renamed to `enable_uart0` and `uart0_baudrate`.
|
||||
* `enable_i2c_instead_of_can` was replaced by the separate settings `enable_i2c0` and `enable_can0`.
|
||||
* `<axis>.motor.gate_driver` was moved to `<axis>.gate_driver`.
|
||||
* `<axis>.min_endstop.pullup` and `<axis>.max_endstop.pullup` were removed. Use `<odrv>.config.gpioX_mode = GPIO_MODE_DIGITAL / GPIO_MODE_DIGITAL_PULL_UP / GPIO_MODE_DIGITAL_PULL_DOWN` instead.
|
||||
* `<axis>.config.can_node_id` was moved to `<axis>.config.can.node_id`
|
||||
* `<axis>.config.can_node_id_extended` was moved to `<axis>.config.can.is_extended`
|
||||
* `<axis>.config.can_heartbeat_rate_ms` was moved to `<axis>.config.can.heartbeat_rate_ms`
|
||||
|
||||
# Release Candidate
|
||||
## [0.5.1] - Date TBD
|
||||
# Releases
|
||||
## [0.5.1] - 2020-09-27
|
||||
### Added
|
||||
* Added motor `torque_constant`: units of torque are now [Nm] instead of just motor current.
|
||||
* Added `motor.config.torque_lim`: limit for motor torque in [Nm].
|
||||
* [Motor thermistors support](docs/thermistors.md)
|
||||
* Enable/disable of thermistor thermal limits according `setting axis.<thermistor>.enabled`.
|
||||
* Introduced `odrive-interface.yaml` as a root source for the ODrive's API. `odrivetool` connects much faster as a side effect.
|
||||
@@ -28,12 +35,18 @@ Please add a note of your changes below this heading if you make a Pull Request.
|
||||
|
||||
### Changed
|
||||
* **`input_pos`, `input_vel`, `pos_estimate_linear`, `pos_estimate_circular`, are now in units of [turns] or [turns/s] instead of [counts] or [counts/s]**
|
||||
* **`pos_gain`, `vel_gain`, `vel_integrator_gain`, are now in units of [(turns/s) / turns], [Nm/(turns/s)], [Nm/(turns/s * s)] instead of [(counts/s) / counts], [A/(counts/s)], [A/((counts/s) * s)].** `pos_gain` is not affected. Old values of `vel_gain` and `vel_integrator_gain` should be multiplied by `torque_constant * encoder cpr` to convert from the old units to the new units. `torque_constant` is approximately equal to 8.27 / (motor KV).
|
||||
* `axis.motor.thermal_current_lim` has been removed. Instead a new property is available `axis.motor.effective_current_lim` which contains the effective current limit including any thermal limits.
|
||||
* `axis.motor.get_inverter_temp()`, `axis.motor.inverter_temp_limit_lower` and `axis.motor.inverter_temp_limit_upper` have been moved to seperate fet thermistor object under `axis.fet_thermistor`. `get_inverter_temp()` function has been renamed to `temp` and is now a read-only property.
|
||||
* `axis.config.counts_per_step` is now `axis.config.turns_per_step`
|
||||
* Outputs of `axis.sensorless_estimator` are now in turns/s instead of electrical rad/s
|
||||
* Fixed bug of high current during lockin-ramp caused by `motor::update()` expecting a torque command instead of current
|
||||
* Fixed bug where commanded velocity was extremely high just after sensorless ramp when using `input_mode` INPUT_MODE_VEL_RAMP caused by `vel_setpoint` and `axis.config.sensorless_ramp.vel` being in different units
|
||||
|
||||
### Fixed
|
||||
* Fixed bug of high current during lockin-ramp caused by `motor::update()` expecting a torque command instead of current
|
||||
* Fixed bug where commanded velocity was extremely high just after sensorless ramp when using `input_mode` INPUT_MODE_VEL_RAMP caused by `vel_setpoint` and `axis.config.sensorless_ramp.vel` being in different units
|
||||
|
||||
# Releases
|
||||
## [0.5.0] - 2020-08-03
|
||||
### Added
|
||||
* AC Induction Motor support.
|
||||
|
||||
+13
-7
@@ -7,16 +7,22 @@ RUN add-apt-repository ppa:team-gcc-arm-embedded/ppa
|
||||
RUN add-apt-repository ppa:jonathonf/tup
|
||||
RUN apt-get update
|
||||
RUN apt-get -y upgrade
|
||||
RUN apt-get -y install gcc-arm-embedded openocd tup python3.7 build-essential git
|
||||
RUN apt-get -y install gcc-arm-embedded openocd tup python3.7 python3-yaml python3-jinja2 python3-jsonschema build-essential git
|
||||
|
||||
# Build step below does not know about debian's python naming schemme
|
||||
RUN ln -s /usr/bin/python3.7 /usr/bin/python
|
||||
|
||||
# Copy the firmware tree into the container
|
||||
RUN mkdir ODrive
|
||||
COPY . ODrive
|
||||
RUN mkdir -p ODrive
|
||||
WORKDIR ODrive/Firmware
|
||||
|
||||
# Hack around Tup's dependency on FUSE
|
||||
RUN tup generate build.sh
|
||||
RUN ./build.sh
|
||||
# Must attach the firmware tree into the container
|
||||
CMD \
|
||||
# Regenerate python interface
|
||||
python interface_generator_stub.py \
|
||||
--definitions odrive-interface.yaml \
|
||||
--template ../tools/enums_template.j2 \
|
||||
--output ../tools/odrive/enums.py && \
|
||||
# Hack around Tup's dependency on FUSE
|
||||
tup init && \
|
||||
tup generate build.sh && \
|
||||
./build.sh
|
||||
|
||||
+14
-4
@@ -17,8 +17,18 @@
|
||||
"__packed=\"__attribute__((__packed__))\"",
|
||||
"__GNUC__"
|
||||
],
|
||||
"intelliSenseMode": "gcc-x64",
|
||||
"compilerPath": "\"${ARM_GCC_ROOT}/bin/arm-none-eabi-g++.exe\" -mthumb -mcpu=cortex-m4 -mfpu=fpv4-sp-d16 -mfloat-abi=hard -specs=nosys.specs -specs=nano.specs -u _printf_float -u _scanf_float",
|
||||
"intelliSenseMode": "gcc-arm",
|
||||
"compilerPath": "arm-none-eabi-g++.exe",
|
||||
"compilerArgs": [
|
||||
"-mthumb",
|
||||
"-mcpu=cortex-m4",
|
||||
"-mfpu=fpv4-sp-d16",
|
||||
"-mfloat-abi=hard",
|
||||
"-specs=nosys.specs",
|
||||
"-specs=nano.specs",
|
||||
"-u _printf_float",
|
||||
"-u _scanf_float"
|
||||
],
|
||||
"cStandard": "c11",
|
||||
"cppStandard": "c++17"
|
||||
},
|
||||
@@ -39,7 +49,7 @@
|
||||
"__packed=\"__attribute__((__packed__))\"",
|
||||
"__GNUC__"
|
||||
],
|
||||
"intelliSenseMode": "gcc-x64",
|
||||
"intelliSenseMode": "gcc-arm",
|
||||
"compilerPath": "arm-none-eabi-g++ -mthumb -mcpu=cortex-m4 -mfpu=fpv4-sp-d16 -mfloat-abi=hard -specs=nosys.specs -specs=nano.specs -u _printf_float -u _scanf_float",
|
||||
"cStandard": "c11",
|
||||
"cppStandard": "c++17"
|
||||
@@ -61,7 +71,7 @@
|
||||
"__packed=\"__attribute__((__packed__))\"",
|
||||
"__GNUC__"
|
||||
],
|
||||
"intelliSenseMode": "gcc-x64",
|
||||
"intelliSenseMode": "gcc-arm",
|
||||
"cStandard": "c11",
|
||||
"cppStandard": "c++17"
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
#include "stm32f4xx.h"
|
||||
#include "stm32f4xx_it.h"
|
||||
#include "cmsis_os.h"
|
||||
#include <stdbool.h>
|
||||
|
||||
/* USER CODE BEGIN 0 */
|
||||
#include <Drivers/STM32/stm32_system.h>
|
||||
@@ -85,6 +86,13 @@ void get_regs(void** stack_ptr) {
|
||||
void* volatile pc __attribute__((unused)) = stack_ptr[6]; // Program counter
|
||||
void* volatile psr __attribute__((unused)) = stack_ptr[7]; // Program status register
|
||||
|
||||
void* volatile cfsr __attribute__((unused)) = (void*)SCB->CFSR; // Configurable fault status register
|
||||
void* volatile cpacr __attribute__((unused)) = (void*)SCB->CPACR;
|
||||
void* volatile fpccr __attribute__((unused)) = (void*)FPU->FPCCR;
|
||||
|
||||
volatile bool preciserr __attribute__((unused)) = (uint32_t)cfsr & 0x200;
|
||||
volatile bool ibuserr __attribute__((unused)) = (uint32_t)cfsr & 0x100;
|
||||
|
||||
volatile int stay_looping = 1;
|
||||
while(stay_looping);
|
||||
}
|
||||
|
||||
@@ -58,20 +58,26 @@ OnboardThermistorCurrentLimiter fet_thermistors[AXIS_COUNT] = {
|
||||
}
|
||||
};
|
||||
|
||||
OffboardThermistorCurrentLimiter motor_thermistors[AXIS_COUNT];
|
||||
|
||||
Motor motors[AXIS_COUNT] = {
|
||||
{
|
||||
&htim1, // timer
|
||||
TIM_1_8_PERIOD_CLOCKS, // control_deadline
|
||||
1.0f / SHUNT_RESISTANCE, // shunt_conductance [S]
|
||||
m0_gate_driver, // gate_driver
|
||||
m0_gate_driver // opamp
|
||||
m0_gate_driver, // opamp
|
||||
fet_thermistors[0],
|
||||
motor_thermistors[0]
|
||||
},
|
||||
{
|
||||
&htim8, // timer
|
||||
(3 * TIM_1_8_PERIOD_CLOCKS) / 2, // control_deadline
|
||||
1.0f / SHUNT_RESISTANCE, // shunt_conductance [S]
|
||||
m1_gate_driver, // gate_driver
|
||||
m1_gate_driver // opamp
|
||||
m1_gate_driver, // opamp
|
||||
fet_thermistors[1],
|
||||
motor_thermistors[1]
|
||||
}
|
||||
};
|
||||
|
||||
@@ -101,7 +107,6 @@ MechanicalBrake mechanical_brakes[AXIS_COUNT];
|
||||
SensorlessEstimator sensorless_estimators[AXIS_COUNT];
|
||||
Controller controllers[AXIS_COUNT];
|
||||
TrapezoidalTrajectory trap[AXIS_COUNT];
|
||||
OffboardThermistorCurrentLimiter motor_thermistors[AXIS_COUNT];
|
||||
|
||||
std::array<Axis, AXIS_COUNT> axes{{
|
||||
{
|
||||
@@ -112,8 +117,6 @@ std::array<Axis, AXIS_COUNT> axes{{
|
||||
encoders[0], // encoder
|
||||
sensorless_estimators[0], // sensorless_estimator
|
||||
controllers[0], // controller
|
||||
fet_thermistors[0], // fet_thermistor
|
||||
motor_thermistors[0], // motor_thermistor
|
||||
motors[0], // motor
|
||||
trap[0], // trap
|
||||
endstops[0], endstops[1], // min_endstop, max_endstop
|
||||
@@ -132,8 +135,6 @@ std::array<Axis, AXIS_COUNT> axes{{
|
||||
encoders[1], // encoder
|
||||
sensorless_estimators[1], // sensorless_estimator
|
||||
controllers[1], // controller
|
||||
fet_thermistors[1], // fet_thermistor
|
||||
motor_thermistors[1], // motor_thermistor
|
||||
motors[1], // motor
|
||||
trap[1], // trap
|
||||
endstops[2], endstops[3], // min_endstop, max_endstop
|
||||
|
||||
@@ -14,8 +14,6 @@ Axis::Axis(int axis_num,
|
||||
Encoder& encoder,
|
||||
SensorlessEstimator& sensorless_estimator,
|
||||
Controller& controller,
|
||||
OnboardThermistorCurrentLimiter& fet_thermistor,
|
||||
OffboardThermistorCurrentLimiter& motor_thermistor,
|
||||
Motor& motor,
|
||||
TrapezoidalTrajectory& trap,
|
||||
Endstop& min_endstop,
|
||||
@@ -28,25 +26,15 @@ Axis::Axis(int axis_num,
|
||||
encoder_(encoder),
|
||||
sensorless_estimator_(sensorless_estimator),
|
||||
controller_(controller),
|
||||
fet_thermistor_(fet_thermistor),
|
||||
motor_thermistor_(motor_thermistor),
|
||||
motor_(motor),
|
||||
trap_traj_(trap),
|
||||
min_endstop_(min_endstop),
|
||||
max_endstop_(max_endstop),
|
||||
mechanical_brake_(mechanical_brake),
|
||||
current_limiters_(make_array(
|
||||
static_cast<CurrentLimiter*>(&fet_thermistor),
|
||||
static_cast<CurrentLimiter*>(&motor_thermistor))),
|
||||
thermistors_(make_array(
|
||||
static_cast<ThermistorCurrentLimiter*>(&fet_thermistor),
|
||||
static_cast<ThermistorCurrentLimiter*>(&motor_thermistor)))
|
||||
mechanical_brake_(mechanical_brake)
|
||||
{
|
||||
encoder_.axis_ = this;
|
||||
sensorless_estimator_.axis_ = this;
|
||||
controller_.axis_ = this;
|
||||
fet_thermistor_.axis_ = this;
|
||||
motor_thermistor.axis_ = this;
|
||||
motor_.axis_ = this;
|
||||
trap_traj_.axis_ = this;
|
||||
min_endstop_.axis_ = this;
|
||||
@@ -97,7 +85,7 @@ void Axis::clear_config() {
|
||||
config_ = {};
|
||||
config_.step_gpio_pin = default_step_gpio_pin_;
|
||||
config_.dir_gpio_pin = default_dir_gpio_pin_;
|
||||
config_.can_node_id = axis_num_;
|
||||
config_.can.node_id = axis_num_;
|
||||
}
|
||||
|
||||
// @brief Does Nothing
|
||||
@@ -180,18 +168,15 @@ bool Axis::do_checks() {
|
||||
|
||||
// Sub-components should use set_error which will propegate to this error_
|
||||
motor_.effective_current_lim();
|
||||
for (ThermistorCurrentLimiter* thermistor : thermistors_) {
|
||||
thermistor->do_checks();
|
||||
}
|
||||
motor_.do_checks();
|
||||
// encoder_.do_checks();
|
||||
// sensorless_estimator_.do_checks();
|
||||
// controller_.do_checks();
|
||||
|
||||
// Check for endstop presses
|
||||
if (min_endstop_.config_.enabled && min_endstop_.get_state() && !(current_state_ == AXIS_STATE_HOMING)) {
|
||||
if (min_endstop_.config_.enabled && min_endstop_.rose() && !(current_state_ == AXIS_STATE_HOMING)) {
|
||||
error_ |= ERROR_MIN_ENDSTOP_PRESSED;
|
||||
} else if (max_endstop_.config_.enabled && max_endstop_.get_state() && !(current_state_ == AXIS_STATE_HOMING)) {
|
||||
} else if (max_endstop_.config_.enabled && max_endstop_.rose() && !(current_state_ == AXIS_STATE_HOMING)) {
|
||||
error_ |= ERROR_MAX_ENDSTOP_PRESSED;
|
||||
}
|
||||
|
||||
@@ -201,15 +186,14 @@ bool Axis::do_checks() {
|
||||
// @brief Update all esitmators
|
||||
bool Axis::do_updates() {
|
||||
// Sub-components should use set_error which will propegate to this error_
|
||||
for (ThermistorCurrentLimiter* thermistor : thermistors_) {
|
||||
thermistor->update();
|
||||
}
|
||||
encoder_.update();
|
||||
sensorless_estimator_.update();
|
||||
motor_.fet_thermistor_.update();
|
||||
motor_.motor_thermistor_.update();
|
||||
min_endstop_.update();
|
||||
max_endstop_.update();
|
||||
bool ret = check_for_errors();
|
||||
odCAN->send_heartbeat(this);
|
||||
odCAN->send_cyclic(*this);
|
||||
return ret;
|
||||
}
|
||||
|
||||
@@ -238,9 +222,9 @@ bool Axis::run_lockin_spin(const LockinConfig_t &lockin_config) {
|
||||
float x = 0.0f;
|
||||
run_control_loop([&]() {
|
||||
float phase = wrap_pm_pi(lockin_config.ramp_distance * x);
|
||||
float I_mag = lockin_config.current * x;
|
||||
float torque = lockin_config.current * motor_.config_.torque_constant * x;
|
||||
x += current_meas_period / lockin_config.ramp_time;
|
||||
if (!motor_.update(I_mag, phase, 0.0f))
|
||||
if (!motor_.update(torque, phase, 0.0f))
|
||||
return false;
|
||||
return x < 1.0f;
|
||||
});
|
||||
@@ -269,7 +253,7 @@ bool Axis::run_lockin_spin(const LockinConfig_t &lockin_config) {
|
||||
distance += vel * current_meas_period;
|
||||
phase = wrap_pm_pi(phase + vel * current_meas_period);
|
||||
|
||||
if (!motor_.update(lockin_config.current, phase, vel))
|
||||
if (!motor_.update(lockin_config.current * motor_.config_.torque_constant, phase, vel))
|
||||
return false;
|
||||
return !spin_done(true); //vel_override to go to next phase
|
||||
});
|
||||
@@ -285,7 +269,7 @@ bool Axis::run_lockin_spin(const LockinConfig_t &lockin_config) {
|
||||
distance += vel * current_meas_period;
|
||||
phase = wrap_pm_pi(phase + vel * current_meas_period);
|
||||
|
||||
if (!motor_.update(lockin_config.current, phase, vel))
|
||||
if (!motor_.update(lockin_config.current * motor_.config_.torque_constant, phase, vel))
|
||||
return false;
|
||||
return !spin_done();
|
||||
});
|
||||
@@ -410,6 +394,7 @@ bool Axis::run_homing() {
|
||||
// Avoid integrator windup issues
|
||||
controller_.vel_integrator_torque_ = 0.0f;
|
||||
|
||||
// Driving toward the endstop
|
||||
run_control_loop([this](){
|
||||
// Note that all estimators are updated in the loop prefix in run_control_loop
|
||||
float torque_setpoint;
|
||||
@@ -561,7 +546,7 @@ void Axis::run_state_machine_loop() {
|
||||
if (status) {
|
||||
// call to controller.reset() that happend when arming means that vel_setpoint
|
||||
// is zeroed. So we make the setpoint the spinup target for smooth transition.
|
||||
controller_.vel_setpoint_ = config_.sensorless_ramp.vel;
|
||||
controller_.vel_setpoint_ = config_.sensorless_ramp.vel / (2.0f * M_PI * motor_.config_.pole_pairs);
|
||||
status = run_sensorless_control_loop();
|
||||
}
|
||||
} break;
|
||||
|
||||
@@ -33,6 +33,13 @@ public:
|
||||
static LockinConfig_t default_sensorless();
|
||||
static LockinConfig_t default_lockin();
|
||||
|
||||
struct CANConfig_t {
|
||||
uint32_t node_id = 0;
|
||||
bool is_extended = false;
|
||||
uint32_t heartbeat_rate_ms = 100;
|
||||
uint32_t encoder_rate_ms = 10;
|
||||
};
|
||||
|
||||
struct Config_t {
|
||||
bool startup_motor_calibration = false; //<! run motor calibration at startup, skip otherwise
|
||||
bool startup_encoder_index_search = false; //<! run encoder index search after startup, skip otherwise
|
||||
@@ -61,9 +68,8 @@ public:
|
||||
LockinConfig_t calibration_lockin = default_calibration();
|
||||
LockinConfig_t sensorless_ramp = default_sensorless();
|
||||
LockinConfig_t general_lockin;
|
||||
uint32_t can_node_id = 0; // Both axes will have the same id to start
|
||||
bool can_node_id_extended = false;
|
||||
uint32_t can_heartbeat_rate_ms = 100;
|
||||
|
||||
CANConfig_t can;
|
||||
|
||||
// custom setters
|
||||
Axis* parent = nullptr;
|
||||
@@ -75,6 +81,11 @@ public:
|
||||
bool is_homed = false;
|
||||
};
|
||||
|
||||
struct CAN_t {
|
||||
uint32_t last_heartbeat = 0;
|
||||
uint32_t last_encoder = 0;
|
||||
};
|
||||
|
||||
enum thread_signals {
|
||||
M_SIGNAL_PH_CURRENT_MEAS = 1u << 0
|
||||
};
|
||||
@@ -86,8 +97,6 @@ public:
|
||||
Encoder& encoder,
|
||||
SensorlessEstimator& sensorless_estimator,
|
||||
Controller& controller,
|
||||
OnboardThermistorCurrentLimiter& fet_thermistor,
|
||||
OffboardThermistorCurrentLimiter& motor_thermistor,
|
||||
Motor& motor,
|
||||
TrapezoidalTrajectory& trap,
|
||||
Endstop& min_endstop,
|
||||
@@ -216,19 +225,12 @@ public:
|
||||
Encoder& encoder_;
|
||||
SensorlessEstimator& sensorless_estimator_;
|
||||
Controller& controller_;
|
||||
OnboardThermistorCurrentLimiter& fet_thermistor_;
|
||||
OffboardThermistorCurrentLimiter& motor_thermistor_;
|
||||
Motor& motor_;
|
||||
TrapezoidalTrajectory& trap_traj_;
|
||||
Endstop& min_endstop_;
|
||||
Endstop& max_endstop_;
|
||||
MechanicalBrake& mechanical_brake_;
|
||||
|
||||
// List of current_limiters and thermistors to
|
||||
// provide easy iteration.
|
||||
std::array<CurrentLimiter*, 2> current_limiters_;
|
||||
std::array<ThermistorCurrentLimiter*, 2> thermistors_;
|
||||
|
||||
osThreadId thread_id_;
|
||||
const uint32_t stack_size_ = 2048; // Bytes
|
||||
volatile bool thread_id_valid_ = false;
|
||||
@@ -246,8 +248,9 @@ public:
|
||||
AxisState& current_state_ = task_chain_.front();
|
||||
uint32_t loop_counter_ = 0;
|
||||
LockinState lockin_state_ = LOCKIN_STATE_INACTIVE;
|
||||
Homing_t homing_;
|
||||
uint32_t last_heartbeat_ = 0;
|
||||
Homing_t homing_;
|
||||
CAN_t can_;
|
||||
|
||||
|
||||
// watchdog
|
||||
uint32_t watchdog_current_value_= 0;
|
||||
|
||||
@@ -228,10 +228,13 @@ bool Encoder::run_offset_calibration() {
|
||||
else
|
||||
return false;
|
||||
|
||||
// go to motor zero phase for start_lock_duration to get ready to scan
|
||||
// go to start position of forward scan for start_lock_duration to get ready to scan
|
||||
int i = 0;
|
||||
axis_->run_control_loop([&](){
|
||||
if (!axis_->motor_.enqueue_voltage_timings(voltage_magnitude, 0.0f))
|
||||
float phase = wrap_pm_pi(0 - config_.calib_scan_distance / 2.0f);
|
||||
float v_alpha = voltage_magnitude * our_arm_cos_f32(phase);
|
||||
float v_beta = voltage_magnitude * our_arm_sin_f32(phase);
|
||||
if (!axis_->motor_.enqueue_voltage_timings(v_alpha, v_beta))
|
||||
return false; // error set inside enqueue_voltage_timings
|
||||
axis_->motor_.log_timing(TIMING_LOG_ENC_CALIB);
|
||||
return ++i < start_lock_duration * current_meas_hz;
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
void Endstop::update() {
|
||||
debounceTimer_.update();
|
||||
last_state_ = endstop_state_;
|
||||
if (config_.enabled) {
|
||||
bool last_pin_state = pin_state_;
|
||||
|
||||
@@ -19,10 +20,6 @@ void Endstop::update() {
|
||||
}
|
||||
}
|
||||
|
||||
bool Endstop::get_state() {
|
||||
return endstop_state_;
|
||||
}
|
||||
|
||||
bool Endstop::apply_config() {
|
||||
debounceTimer_.reset();
|
||||
if (config_.enabled) {
|
||||
|
||||
@@ -26,11 +26,22 @@ class Endstop {
|
||||
bool apply_config();
|
||||
|
||||
void update();
|
||||
bool get_state();
|
||||
constexpr bool get_state(){
|
||||
return endstop_state_;
|
||||
}
|
||||
|
||||
constexpr bool rose(){
|
||||
return (endstop_state_ != last_state_) && endstop_state_;
|
||||
}
|
||||
|
||||
constexpr bool fell(){
|
||||
return (endstop_state_ != last_state_) && !endstop_state_;
|
||||
}
|
||||
|
||||
bool endstop_state_ = false;
|
||||
|
||||
private:
|
||||
bool last_state_ = false;
|
||||
bool pin_state_ = false;
|
||||
float pos_when_pressed_ = 0.0f;
|
||||
Timer<float> debounceTimer_;
|
||||
|
||||
@@ -380,7 +380,7 @@ void vbus_sense_adc_cb(ADC_HandleTypeDef* hadc, bool injected) {
|
||||
}
|
||||
|
||||
// This is the callback from the ADC that we expect after the PWM has triggered an ADC conversion.
|
||||
// TODO: Document how the phasing is done, link to timing diagram
|
||||
// Timing diagram: Firmware/timing_diagram_v3.png
|
||||
void pwm_trig_adc_cb(ADC_HandleTypeDef* hadc, bool injected) {
|
||||
#define calib_tau 0.2f //@TOTO make more easily configurable
|
||||
constexpr float calib_filter_k = CURRENT_MEAS_PERIOD / calib_tau;
|
||||
|
||||
@@ -49,8 +49,8 @@ static bool config_read_all() {
|
||||
config_manager.read(&axes[i].max_endstop_.config_) &&
|
||||
config_manager.read(&axes[i].mechanical_brake_.config_) &&
|
||||
config_manager.read(&motors[i].config_) &&
|
||||
config_manager.read(&fet_thermistors[i].config_) &&
|
||||
config_manager.read(&axes[i].motor_thermistor_.config_) &&
|
||||
config_manager.read(&motors[i].fet_thermistor_.config_) &&
|
||||
config_manager.read(&motors[i].motor_thermistor_.config_) &&
|
||||
config_manager.read(&axes[i].config_);
|
||||
}
|
||||
return success;
|
||||
@@ -69,8 +69,8 @@ static bool config_write_all() {
|
||||
config_manager.write(&axes[i].max_endstop_.config_) &&
|
||||
config_manager.write(&axes[i].mechanical_brake_.config_) &&
|
||||
config_manager.write(&motors[i].config_) &&
|
||||
config_manager.write(&fet_thermistors[i].config_) &&
|
||||
config_manager.write(&axes[i].motor_thermistor_.config_) &&
|
||||
config_manager.write(&motors[i].fet_thermistor_.config_) &&
|
||||
config_manager.write(&motors[i].motor_thermistor_.config_) &&
|
||||
config_manager.write(&axes[i].config_);
|
||||
}
|
||||
return success;
|
||||
@@ -89,8 +89,8 @@ static void config_clear_all() {
|
||||
axes[i].max_endstop_.config_ = {};
|
||||
axes[i].mechanical_brake_.config_ = {};
|
||||
motors[i].config_ = {};
|
||||
fet_thermistors[i].config_ = {};
|
||||
axes[i].motor_thermistor_.config_ = {};
|
||||
motors[i].fet_thermistor_.config_ = {};
|
||||
motors[i].motor_thermistor_.config_ = {};
|
||||
axes[i].clear_config();
|
||||
}
|
||||
}
|
||||
@@ -103,6 +103,7 @@ static bool config_apply_all() {
|
||||
&& axes[i].min_endstop_.apply_config()
|
||||
&& axes[i].max_endstop_.apply_config()
|
||||
&& motors[i].apply_config()
|
||||
&& motors[i].motor_thermistor_.apply_config()
|
||||
&& axes[i].apply_config();
|
||||
}
|
||||
return success;
|
||||
|
||||
@@ -10,13 +10,19 @@ Motor::Motor(TIM_HandleTypeDef* timer,
|
||||
uint16_t control_deadline,
|
||||
float shunt_conductance,
|
||||
TGateDriver& gate_driver,
|
||||
TOpAmp& opamp) :
|
||||
TOpAmp& opamp,
|
||||
OnboardThermistorCurrentLimiter& fet_thermistor,
|
||||
OffboardThermistorCurrentLimiter& motor_thermistor) :
|
||||
timer_(timer),
|
||||
control_deadline_(control_deadline),
|
||||
shunt_conductance_(shunt_conductance),
|
||||
gate_driver_(gate_driver),
|
||||
opamp_(opamp) {
|
||||
opamp_(opamp),
|
||||
fet_thermistor_(fet_thermistor),
|
||||
motor_thermistor_(motor_thermistor) {
|
||||
apply_config();
|
||||
fet_thermistor_.motor_ = this;
|
||||
motor_thermistor_.motor_ = this;
|
||||
}
|
||||
|
||||
// @brief Arms the PWM outputs that belong to this motor.
|
||||
@@ -49,6 +55,7 @@ void Motor::reset_current_control() {
|
||||
current_control_.v_current_control_integral_d = 0.0f;
|
||||
current_control_.v_current_control_integral_q = 0.0f;
|
||||
current_control_.acim_rotor_flux = 0.0f;
|
||||
current_control_.Ibus = 0.0f;
|
||||
}
|
||||
|
||||
// @brief Tune the current controller based on phase resistance and inductance
|
||||
@@ -109,7 +116,16 @@ bool Motor::do_checks() {
|
||||
set_error(ERROR_DRV_FAULT);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!motor_thermistor_.do_checks()) {
|
||||
axis_->error_ |= Axis::ERROR_MOTOR_FAILED;
|
||||
set_error(ERROR_MOTOR_THERMISTOR_OVER_TEMP);
|
||||
return false;
|
||||
}
|
||||
if (!fet_thermistor_.do_checks()) {
|
||||
axis_->error_ |= Axis::ERROR_MOTOR_FAILED;
|
||||
set_error(ERROR_FET_THERMISTOR_OVER_TEMP);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -123,11 +139,9 @@ float Motor::effective_current_lim() {
|
||||
current_lim = std::min(current_lim, axis_->motor_.current_control_.max_allowed_current);
|
||||
}
|
||||
|
||||
// Apply axis current limiters
|
||||
for (const CurrentLimiter* const limiter : axis_->current_limiters_) {
|
||||
current_lim = std::min(current_lim, limiter->get_current_limit(config_.current_lim));
|
||||
}
|
||||
|
||||
// Apply thermistor current limiters
|
||||
current_lim = std::min(current_lim, motor_thermistor_.get_current_limit(config_.current_lim));
|
||||
current_lim = std::min(current_lim, fet_thermistor_.get_current_limit(config_.current_lim));
|
||||
effective_current_lim_ = current_lim;
|
||||
|
||||
return effective_current_lim_;
|
||||
|
||||
@@ -99,7 +99,9 @@ public:
|
||||
uint16_t control_deadline,
|
||||
float shunt_conductance,
|
||||
TGateDriver& gate_driver,
|
||||
TOpAmp& opamp);
|
||||
TOpAmp& opamp,
|
||||
OnboardThermistorCurrentLimiter& fet_thermistor,
|
||||
OffboardThermistorCurrentLimiter& motor_thermistor);
|
||||
|
||||
bool arm();
|
||||
void disarm();
|
||||
@@ -130,6 +132,8 @@ public:
|
||||
const float shunt_conductance_;
|
||||
TGateDriver& gate_driver_;
|
||||
TOpAmp& opamp_;
|
||||
OnboardThermistorCurrentLimiter& fet_thermistor_;
|
||||
OffboardThermistorCurrentLimiter& motor_thermistor_;
|
||||
|
||||
Config_t config_;
|
||||
Axis* axis_ = nullptr; // set by Axis constructor
|
||||
|
||||
@@ -66,13 +66,15 @@ bool SensorlessEstimator::update() {
|
||||
}
|
||||
|
||||
// predict PLL phase with velocity
|
||||
pll_pos_ = wrap_pm_pi(pll_pos_ + current_meas_period * vel_estimate_);
|
||||
pll_pos_ = wrap_pm_pi(pll_pos_ + current_meas_period * vel_estimate_erad_);
|
||||
// update PLL phase with observer permanent magnet phase
|
||||
phase_ = fast_atan2(eta[1], eta[0]);
|
||||
float delta_phase = wrap_pm_pi(phase_ - pll_pos_);
|
||||
pll_pos_ = wrap_pm_pi(pll_pos_ + current_meas_period * pll_kp * delta_phase);
|
||||
// update PLL velocity
|
||||
vel_estimate_ += current_meas_period * pll_ki * delta_phase;
|
||||
vel_estimate_erad_ += current_meas_period * pll_ki * delta_phase;
|
||||
// convert to mechanical turns/s for controller usage.
|
||||
vel_estimate_ = vel_estimate_erad_ / (std::max((float)axis_->motor_.config_.pole_pairs, 1.0f) * 2.0f * M_PI);
|
||||
|
||||
vel_estimate_valid_ = true;
|
||||
return true;
|
||||
|
||||
@@ -18,7 +18,8 @@ public:
|
||||
Error error_ = ERROR_NONE;
|
||||
float phase_ = 0.0f; // [rad]
|
||||
float pll_pos_ = 0.0f; // [rad]
|
||||
float vel_estimate_ = 0.0f; // [rad/s]
|
||||
float vel_estimate_ = 0.0f; // [turn/s]
|
||||
float vel_estimate_erad_ = 0.0f; // [rad/s]
|
||||
bool vel_estimate_valid_ = false;
|
||||
// float pll_kp_ = 0.0f; // [rad/s / rad]
|
||||
// float pll_ki_ = 0.0f; // [(rad/s^2) / rad]
|
||||
|
||||
@@ -14,8 +14,7 @@ ThermistorCurrentLimiter::ThermistorCurrentLimiter(uint16_t adc_channel,
|
||||
temperature_(NAN),
|
||||
temp_limit_lower_(temp_limit_lower),
|
||||
temp_limit_upper_(temp_limit_upper),
|
||||
enabled_(enabled),
|
||||
error_(ERROR_NONE)
|
||||
enabled_(enabled)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -27,8 +26,6 @@ void ThermistorCurrentLimiter::update() {
|
||||
|
||||
bool ThermistorCurrentLimiter::do_checks() {
|
||||
if (enabled_ && temperature_ >= temp_limit_upper_ + 5) {
|
||||
error_ = ERROR_OVER_TEMP;
|
||||
axis_->error_ |= Axis::ERROR_OVER_TEMP;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
@@ -70,6 +67,12 @@ OffboardThermistorCurrentLimiter::OffboardThermistorCurrentLimiter() :
|
||||
decode_pin();
|
||||
}
|
||||
|
||||
bool OffboardThermistorCurrentLimiter::apply_config() {
|
||||
config_.parent = this;
|
||||
decode_pin();
|
||||
return true;
|
||||
}
|
||||
|
||||
void OffboardThermistorCurrentLimiter::decode_pin() {
|
||||
adc_channel_ = channel_from_gpio(get_gpio(config_.gpio_pin));
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#ifndef __THERMISTOR_HPP
|
||||
#define __THERMISTOR_HPP
|
||||
|
||||
class Axis; // declared in axis.hpp
|
||||
class Motor; // declared in motor.hpp
|
||||
|
||||
#include "current_limiter.hpp"
|
||||
#include <autogen/interfaces.hpp>
|
||||
@@ -28,8 +28,7 @@ public:
|
||||
const float& temp_limit_lower_;
|
||||
const float& temp_limit_upper_;
|
||||
const bool& enabled_;
|
||||
Error error_;
|
||||
Axis* axis_ = nullptr; // set by Axis constructor
|
||||
Motor* motor_ = nullptr; // set by Motor::apply_config()
|
||||
};
|
||||
|
||||
class OnboardThermistorCurrentLimiter : public ThermistorCurrentLimiter, public ODriveIntf::OnboardThermistorCurrentLimiterIntf {
|
||||
@@ -68,6 +67,8 @@ public:
|
||||
|
||||
Config_t config_;
|
||||
|
||||
bool apply_config();
|
||||
|
||||
private:
|
||||
void decode_pin();
|
||||
};
|
||||
|
||||
@@ -39,6 +39,9 @@ TEST_SUITE("CAN Functions") {
|
||||
val = can_getSignal<uint16_t>(rxmsg, 0, 16, true, 1, 0);
|
||||
CHECK(val == 0x1234);
|
||||
|
||||
val = can_getSignal<uint16_t>(rxmsg, 0, 16, true);
|
||||
CHECK(val == 0x1234);
|
||||
|
||||
val = can_getSignal<uint16_t>(rxmsg, 0, 16, false, 1, 0);
|
||||
CHECK(val == 0x3412);
|
||||
|
||||
|
||||
@@ -118,6 +118,7 @@ FLAGS += '-DUSE_HAL_DRIVER'
|
||||
FLAGS += '-mthumb'
|
||||
FLAGS += '-mfloat-abi=hard'
|
||||
FLAGS += { '-Wall', '-Wdouble-promotion', '-Wfloat-conversion', '-fdata-sections', '-ffunction-sections'}
|
||||
FLAGS += '-g'
|
||||
FLAGS += '-DFIBRE_ENABLE_SERVER'
|
||||
|
||||
-- linker flags
|
||||
@@ -128,7 +129,7 @@ LDFLAGS += '-Wl,--undefined=uxTopUsedPriority'
|
||||
|
||||
-- debug build
|
||||
if tup.getconfig("DEBUG") == "true" then
|
||||
FLAGS += '-g -gdwarf-2'
|
||||
FLAGS += '-gdwarf-2'
|
||||
OPT += '-Og'
|
||||
else
|
||||
OPT += '-O2'
|
||||
|
||||
@@ -115,6 +115,7 @@ void AsciiProtocol::process_line(cbufptr_t buffer) {
|
||||
case 'r': cmd_read_property(cmd, use_checksum); break; // read property
|
||||
case 'w': cmd_write_property(cmd, use_checksum); break; // write property
|
||||
case 'u': cmd_update_axis_wdg(cmd, use_checksum); break; // Update axis watchdog.
|
||||
case 'e': cmd_encoder(cmd, use_checksum); break; // Encoder commands
|
||||
default : cmd_unknown(nullptr, use_checksum); break;
|
||||
}
|
||||
}
|
||||
@@ -213,11 +214,37 @@ void AsciiProtocol::cmd_set_torque(char * pStr, bool use_checksum) {
|
||||
}
|
||||
}
|
||||
|
||||
// @brief Sets the encoder linear count
|
||||
// @param pStr buffer of ASCII encoded values
|
||||
// @param response_channel reference to the stream to respond on
|
||||
// @param use_checksum bool to indicate whether a checksum is required on response
|
||||
void cmd_encoder(char * pStr, StreamSink& response_channel, bool use_checksum) {
|
||||
if (pStr[1] == 's') {
|
||||
pStr += 2; // Substring two characters to the right (ok because we have guaranteed null termination after all chars)
|
||||
|
||||
unsigned motor_number;
|
||||
int encoder_count;
|
||||
|
||||
if (sscanf(pStr, "l %u %i", &motor_number, &encoder_count) < 2) {
|
||||
respond(response_channel, use_checksum, "invalid command format");
|
||||
} else if (motor_number >= AXIS_COUNT) {
|
||||
respond(response_channel, use_checksum, "invalid motor %u", motor_number);
|
||||
} else {
|
||||
Axis& axis = axes[motor_number];
|
||||
axis.encoder_.set_linear_count(encoder_count);
|
||||
axis.watchdog_feed();
|
||||
respond(response_channel, use_checksum, "encoder set to %u", encoder_count);
|
||||
}
|
||||
} else {
|
||||
respond(response_channel, use_checksum, "invalid command format");
|
||||
}
|
||||
}
|
||||
|
||||
// @brief Executes the set trapezoid trajectory command
|
||||
// @param pStr buffer of ASCII encoded values
|
||||
// @param response_channel reference to the stream to respond on
|
||||
// @param use_checksum bool to indicate whether a checksum is required on response
|
||||
void AsciiProtocol::cmd_set_trapezoid_trajectory(char * pStr, bool use_checksum) {
|
||||
void AsciiProtocol::cmd_set_trapezoid_trajectory(char* pStr, bool use_checksum) {
|
||||
unsigned motor_number;
|
||||
float goal_point;
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ private:
|
||||
void cmd_write_property(char * pStr, bool use_checksum);
|
||||
void cmd_update_axis_wdg(char * pStr, bool use_checksum);
|
||||
void cmd_unknown(char * pStr, bool use_checksum);
|
||||
void cmd_encoder(char * pStr, bool use_checksum);
|
||||
|
||||
template<typename ... TArgs> void respond(bool include_checksum, const char * fmt, TArgs&& ... args);
|
||||
void process_line(fibre::cbufptr_t buffer);
|
||||
|
||||
@@ -21,12 +21,16 @@ struct can_Signal_t {
|
||||
const float offset;
|
||||
};
|
||||
|
||||
struct can_Cyclic_t {
|
||||
uint32_t cycleTime_ms;
|
||||
uint32_t lastTime_ms;
|
||||
};
|
||||
|
||||
#include <iterator>
|
||||
template <typename T>
|
||||
T can_getSignal(can_Message_t msg, const uint8_t startBit, const uint8_t length, const bool isIntel) {
|
||||
constexpr T can_getSignal(can_Message_t msg, const uint8_t startBit, const uint8_t length, const bool isIntel) {
|
||||
uint64_t tempVal = 0;
|
||||
uint64_t mask = (1ULL << length) - 1;
|
||||
uint64_t mask = length < 64 ? (1ULL << length) - 1ULL : -1ULL;
|
||||
|
||||
if (isIntel) {
|
||||
std::memcpy(&tempVal, msg.buf, sizeof(tempVal));
|
||||
@@ -42,19 +46,12 @@ T can_getSignal(can_Message_t msg, const uint8_t startBit, const uint8_t length,
|
||||
return retVal;
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
float can_getSignal(can_Message_t msg, const uint8_t startBit, const uint8_t length, const bool isIntel, const float factor, const float offset) {
|
||||
T retVal = can_getSignal<T>(msg, startBit, length, isIntel);
|
||||
return (retVal * factor) + offset;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
void can_setSignal(can_Message_t& msg, const T& val, const uint8_t startBit, const uint8_t length, const bool isIntel, const float factor, const float offset) {
|
||||
T scaledVal = (val - offset) / factor;
|
||||
constexpr void can_setSignal(can_Message_t& msg, const T& val, const uint8_t startBit, const uint8_t length, const bool isIntel) {
|
||||
uint64_t valAsBits = 0;
|
||||
std::memcpy(&valAsBits, &scaledVal, sizeof(scaledVal));
|
||||
std::memcpy(&valAsBits, &val, sizeof(val));
|
||||
|
||||
uint64_t mask = (1ULL << length) - 1;
|
||||
uint64_t mask = length < 64 ? (1ULL << length) - 1ULL : -1ULL;
|
||||
|
||||
if (isIntel) {
|
||||
uint64_t data = 0;
|
||||
@@ -77,6 +74,18 @@ void can_setSignal(can_Message_t& msg, const T& val, const uint8_t startBit, con
|
||||
}
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
void can_setSignal(can_Message_t& msg, const T& val, const uint8_t startBit, const uint8_t length, const bool isIntel, const float factor, const float offset) {
|
||||
T scaledVal = static_cast<T>((val - offset) / factor);
|
||||
can_setSignal<T>(msg, scaledVal, startBit, length, isIntel);
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
float can_getSignal(can_Message_t msg, const uint8_t startBit, const uint8_t length, const bool isIntel, const float factor, const float offset) {
|
||||
T retVal = can_getSignal<T>(msg, startBit, length, isIntel);
|
||||
return (retVal * factor) + offset;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
float can_getSignal(can_Message_t msg, const can_Signal_t& signal) {
|
||||
return can_getSignal<T>(msg, signal.startBit, signal.length, signal.isIntel, signal.factor, signal.offset);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -6,7 +6,7 @@
|
||||
class CANSimple {
|
||||
public:
|
||||
enum {
|
||||
MSG_CO_NMT_CTRL = 0x000, // CANOpen NMT Message REC
|
||||
MSG_CO_NMT_CTRL = 0x000, // CANOpen NMT Message REC
|
||||
MSG_ODRIVE_HEARTBEAT,
|
||||
MSG_ODRIVE_ESTOP,
|
||||
MSG_GET_MOTOR_ERROR, // Errors
|
||||
@@ -34,51 +34,56 @@ class CANSimple {
|
||||
MSG_CO_HEARTBEAT_CMD = 0x700, // CANOpen NMT Heartbeat SEND
|
||||
};
|
||||
|
||||
static void handle_can_message(can_Message_t& msg);
|
||||
static void send_heartbeat(Axis* axis);
|
||||
static void handle_can_message(const can_Message_t& msg);
|
||||
static void doCommand(Axis& axis, const can_Message_t& cmd);
|
||||
|
||||
// Cyclic Senders
|
||||
static int32_t send_heartbeat(const Axis& axis);
|
||||
static void send_cyclic(Axis& axis);
|
||||
|
||||
private:
|
||||
static void nmt_callback(Axis* axis, can_Message_t& msg);
|
||||
static void estop_callback(Axis* axis, can_Message_t& msg);
|
||||
static void get_motor_error_callback(Axis* axis, can_Message_t& msg);
|
||||
static void get_encoder_error_callback(Axis* axis, can_Message_t& msg);
|
||||
static void get_controller_error_callback(Axis* axis, can_Message_t& msg);
|
||||
static void get_sensorless_error_callback(Axis* axis, can_Message_t& msg);
|
||||
static void set_axis_nodeid_callback(Axis* axis, can_Message_t& msg);
|
||||
static void set_axis_requested_state_callback(Axis* axis, can_Message_t& msg);
|
||||
static void set_axis_startup_config_callback(Axis* axis, can_Message_t& msg);
|
||||
static void get_encoder_estimates_callback(Axis* axis, can_Message_t& msg);
|
||||
static void get_encoder_count_callback(Axis* axis, can_Message_t& msg);
|
||||
static void set_input_pos_callback(Axis* axis, can_Message_t& msg);
|
||||
static void set_input_vel_callback(Axis* axis, can_Message_t& msg);
|
||||
static void set_input_torque_callback(Axis* axis, can_Message_t& msg);
|
||||
static void set_controller_modes_callback(Axis* axis, can_Message_t& msg);
|
||||
static void set_vel_limit_callback(Axis* axis, can_Message_t& msg);
|
||||
static void start_anticogging_callback(Axis* axis, can_Message_t& msg);
|
||||
static void set_traj_vel_limit_callback(Axis* axis, can_Message_t& msg);
|
||||
static void set_traj_accel_limits_callback(Axis* axis, can_Message_t& msg);
|
||||
static void set_traj_inertia_callback(Axis* axis, can_Message_t& msg);
|
||||
static void get_iq_callback(Axis* axis, can_Message_t& msg);
|
||||
static void get_sensorless_estimates_callback(Axis* axis, can_Message_t& msg);
|
||||
static void get_vbus_voltage_callback(Axis* axis, can_Message_t& msg);
|
||||
static void clear_errors_callback(Axis* axis, can_Message_t& msg);
|
||||
// Get functions (msg.rtr bit must be set)
|
||||
static int32_t get_motor_error_callback(const Axis& axis);
|
||||
static int32_t get_encoder_error_callback(const Axis& axis);
|
||||
static int32_t get_controller_error_callback(const Axis& axis);
|
||||
static int32_t get_sensorless_error_callback(const Axis& axis);
|
||||
static int32_t get_encoder_estimates_callback(const Axis& axis);
|
||||
static int32_t get_encoder_count_callback(const Axis& axis);
|
||||
static int32_t get_iq_callback(const Axis& axis);
|
||||
static int32_t get_sensorless_estimates_callback(const Axis& axis);
|
||||
static int32_t get_vbus_voltage_callback(const Axis& axis);
|
||||
|
||||
// Set functions
|
||||
static void set_axis_nodeid_callback(Axis& axis, const can_Message_t& msg);
|
||||
static void set_axis_requested_state_callback(Axis& axis, const can_Message_t& msg);
|
||||
static void set_axis_startup_config_callback(Axis& axis, const can_Message_t& msg);
|
||||
static void set_input_pos_callback(Axis& axis, const can_Message_t& msg);
|
||||
static void set_input_vel_callback(Axis& axis, const can_Message_t& msg);
|
||||
static void set_input_torque_callback(Axis& axis, const can_Message_t& msg);
|
||||
static void set_controller_modes_callback(Axis& axis, const can_Message_t& msg);
|
||||
static void set_vel_limit_callback(Axis& axis, const can_Message_t& msg);
|
||||
static void set_traj_vel_limit_callback(Axis& axis, const can_Message_t& msg);
|
||||
static void set_traj_accel_limits_callback(Axis& axis, const can_Message_t& msg);
|
||||
static void set_traj_inertia_callback(Axis& axis, const can_Message_t& msg);
|
||||
static void set_linear_count_callback(Axis& axis, const can_Message_t& msg);
|
||||
|
||||
// Other functions
|
||||
static void nmt_callback(const Axis& axis, const can_Message_t& msg);
|
||||
static void estop_callback(Axis& axis, const can_Message_t& msg);
|
||||
static void clear_errors_callback(Axis& axis, const can_Message_t& msg);
|
||||
static void start_anticogging_callback(const Axis& axis, const can_Message_t& msg);
|
||||
|
||||
static constexpr uint8_t NUM_NODE_ID_BITS = 6;
|
||||
static constexpr uint8_t NUM_CMD_ID_BITS = 11 - NUM_NODE_ID_BITS;
|
||||
|
||||
// Utility functions
|
||||
static uint32_t get_node_id(uint32_t msgID);
|
||||
static uint8_t get_cmd_id(uint32_t msgID);
|
||||
static constexpr uint32_t get_node_id(uint32_t msgID) {
|
||||
return (msgID >> NUM_CMD_ID_BITS); // Upper 6 or more bits
|
||||
};
|
||||
|
||||
// Fetch a specific signal from the message
|
||||
|
||||
// This functional way of handling the messages is neat and is much cleaner from
|
||||
// a data security point of view, but it will require some tweaking
|
||||
//
|
||||
// const std::map<uint32_t, std::function<void(can_Message_t&)>> callback_map = {
|
||||
// {0x000, std::bind(&CANSimple::heartbeat_callback, this, _1)}
|
||||
// };
|
||||
static constexpr uint8_t get_cmd_id(uint32_t msgID) {
|
||||
return (msgID & 0x01F); // Bottom 5 bits
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
#endif
|
||||
@@ -36,13 +36,13 @@ void ODriveCAN::can_server_thread() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
HAL_CAN_ActivateNotification(handle_, CAN_IT_RX_FIFO0_MSG_PENDING);
|
||||
HAL_CAN_ActivateNotification(handle_, CAN_IT_RX_FIFO0_MSG_PENDING | CAN_IT_TX_MAILBOX_EMPTY);
|
||||
} else {
|
||||
if (status == HAL_CAN_ERROR_TIMEOUT) {
|
||||
HAL_CAN_ResetError(handle_);
|
||||
status = HAL_CAN_Start(handle_);
|
||||
if (status == HAL_OK)
|
||||
status = HAL_CAN_ActivateNotification(handle_, CAN_IT_RX_FIFO0_MSG_PENDING);
|
||||
status = HAL_CAN_ActivateNotification(handle_, CAN_IT_RX_FIFO0_MSG_PENDING | CAN_IT_TX_MAILBOX_EMPTY);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -75,7 +75,7 @@ bool ODriveCAN::start_can_server() {
|
||||
|
||||
status = HAL_CAN_Start(handle_);
|
||||
if (status == HAL_OK)
|
||||
status = HAL_CAN_ActivateNotification(handle_, CAN_IT_RX_FIFO0_MSG_PENDING);
|
||||
status = HAL_CAN_ActivateNotification(handle_, CAN_IT_RX_FIFO0_MSG_PENDING | CAN_IT_TX_MAILBOX_EMPTY);
|
||||
|
||||
osThreadDef(can_server_thread_def, can_server_thread_wrapper, osPriorityNormal, 0, stack_size_ / sizeof(StackType_t));
|
||||
thread_id_ = osThreadCreate(osThread(can_server_thread_def), this);
|
||||
@@ -85,7 +85,7 @@ bool ODriveCAN::start_can_server() {
|
||||
}
|
||||
|
||||
// Send a CAN message on the bus
|
||||
uint32_t ODriveCAN::write(can_Message_t &txmsg) {
|
||||
int32_t ODriveCAN::write(can_Message_t &txmsg) {
|
||||
if (HAL_CAN_GetError(handle_) == HAL_CAN_ERROR_NONE) {
|
||||
CAN_TxHeaderTypeDef header;
|
||||
header.StdId = txmsg.id;
|
||||
@@ -98,8 +98,9 @@ uint32_t ODriveCAN::write(can_Message_t &txmsg) {
|
||||
uint32_t retTxMailbox = 0;
|
||||
if (HAL_CAN_GetTxMailboxesFreeLevel(handle_) > 0)
|
||||
HAL_CAN_AddTxMessage(handle_, &header, txmsg.buf, &retTxMailbox);
|
||||
|
||||
return retTxMailbox;
|
||||
else
|
||||
return -1;
|
||||
return (int32_t)retTxMailbox;
|
||||
} else {
|
||||
return -1;
|
||||
}
|
||||
@@ -168,32 +169,36 @@ void ODriveCAN::reinit_can() {
|
||||
HAL_CAN_Init(handle_);
|
||||
auto status = HAL_CAN_Start(handle_);
|
||||
if (status == HAL_OK)
|
||||
status = HAL_CAN_ActivateNotification(handle_, CAN_IT_RX_FIFO0_MSG_PENDING);
|
||||
status = HAL_CAN_ActivateNotification(handle_, CAN_IT_RX_FIFO0_MSG_PENDING | CAN_IT_TX_MAILBOX_EMPTY);
|
||||
}
|
||||
|
||||
void ODriveCAN::set_error(Error error) {
|
||||
error_ |= error;
|
||||
}
|
||||
|
||||
// This function is called by each axis.
|
||||
// It provides an abstraction from the specific CAN protocol in use
|
||||
void ODriveCAN::send_heartbeat(Axis *axis) {
|
||||
void ODriveCAN::send_cyclic(Axis &axis) {
|
||||
// Handle heartbeat message
|
||||
if (axis->config_.can_heartbeat_rate_ms > 0) {
|
||||
uint32_t now = osKernelSysTick();
|
||||
if ((now - axis->last_heartbeat_) >= axis->config_.can_heartbeat_rate_ms) {
|
||||
switch (config_.protocol) {
|
||||
case PROTOCOL_SIMPLE:
|
||||
CANSimple::send_heartbeat(axis);
|
||||
break;
|
||||
}
|
||||
axis->last_heartbeat_ = now;
|
||||
}
|
||||
switch (config_.protocol) {
|
||||
case PROTOCOL_SIMPLE:
|
||||
CANSimple::send_cyclic(axis);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void HAL_CAN_TxMailbox0CompleteCallback(CAN_HandleTypeDef *hcan) {}
|
||||
void HAL_CAN_TxMailbox1CompleteCallback(CAN_HandleTypeDef *hcan) {}
|
||||
void HAL_CAN_TxMailbox2CompleteCallback(CAN_HandleTypeDef *hcan) {}
|
||||
void HAL_CAN_TxMailbox0CompleteCallback(CAN_HandleTypeDef *hcan) {
|
||||
HAL_CAN_DeactivateNotification(hcan, CAN_IT_TX_MAILBOX_EMPTY);
|
||||
osSemaphoreRelease(sem_can);
|
||||
}
|
||||
void HAL_CAN_TxMailbox1CompleteCallback(CAN_HandleTypeDef *hcan) {
|
||||
HAL_CAN_DeactivateNotification(hcan, CAN_IT_TX_MAILBOX_EMPTY);
|
||||
osSemaphoreRelease(sem_can);
|
||||
}
|
||||
void HAL_CAN_TxMailbox2CompleteCallback(CAN_HandleTypeDef *hcan) {
|
||||
HAL_CAN_DeactivateNotification(hcan, CAN_IT_TX_MAILBOX_EMPTY);
|
||||
osSemaphoreRelease(sem_can);
|
||||
}
|
||||
void HAL_CAN_TxMailbox0AbortCallback(CAN_HandleTypeDef *hcan) {}
|
||||
void HAL_CAN_TxMailbox1AbortCallback(CAN_HandleTypeDef *hcan) {}
|
||||
void HAL_CAN_TxMailbox2AbortCallback(CAN_HandleTypeDef *hcan) {}
|
||||
|
||||
@@ -35,14 +35,14 @@ class ODriveCAN : public ODriveIntf::CanIntf {
|
||||
volatile bool thread_id_valid_ = false;
|
||||
bool start_can_server();
|
||||
void can_server_thread();
|
||||
void send_heartbeat(Axis *axis);
|
||||
void send_cyclic(Axis& axis);
|
||||
void reinit_can();
|
||||
|
||||
void set_error(Error error);
|
||||
|
||||
// I/O Functions
|
||||
uint32_t available();
|
||||
uint32_t write(can_Message_t &txmsg);
|
||||
int32_t write(can_Message_t &txmsg);
|
||||
bool read(can_Message_t &rxmsg);
|
||||
|
||||
ODriveCAN::Config_t &config_;
|
||||
|
||||
@@ -366,7 +366,8 @@ interfaces:
|
||||
bit: 17
|
||||
doc: the min endstop was not enabled during homing
|
||||
OverTemp:
|
||||
doc: Check `fet_thermistor.error` and `motor_thermistor.error` for more information.
|
||||
# unused
|
||||
doc: Check `motor.error` for more details.
|
||||
step_dir_active: readonly bool
|
||||
current_state: readonly AxisState
|
||||
requested_state: AxisState
|
||||
@@ -414,7 +415,6 @@ interfaces:
|
||||
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'}
|
||||
@@ -428,11 +428,7 @@ interfaces:
|
||||
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
|
||||
can: CanConfig
|
||||
gate_driver:
|
||||
c_name: gate_driver_exported_
|
||||
c_is_class: False
|
||||
@@ -456,8 +452,6 @@ interfaces:
|
||||
# status_reg_2: readonly uint32
|
||||
# ctrl_reg_1: readonly uint32
|
||||
# ctrl_reg_2: readonly uint32
|
||||
fet_thermistor: OnboardThermistorCurrentLimiter
|
||||
motor_thermistor: OffboardThermistorCurrentLimiter
|
||||
motor: Motor
|
||||
controller: Controller
|
||||
encoder: Encoder
|
||||
@@ -470,7 +464,7 @@ interfaces:
|
||||
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.
|
||||
doc: Clear all the errors of this axis including all contained submodules.
|
||||
|
||||
ODrive.Axis.LockinConfig:
|
||||
c_is_class: False
|
||||
@@ -497,13 +491,20 @@ interfaces:
|
||||
finish_on_distance: bool
|
||||
finish_on_enc_idx: bool
|
||||
|
||||
ODrive.Axis.CanConfig:
|
||||
c_is_class: False
|
||||
attributes:
|
||||
node_id: uint32
|
||||
is_extended: bool
|
||||
heartbeat_rate_ms: uint32
|
||||
encoder_rate_ms: uint32
|
||||
|
||||
ODrive.ThermistorCurrentLimiter:
|
||||
c_is_class: False
|
||||
|
||||
ODrive.OnboardThermistorCurrentLimiter:
|
||||
c_is_class: True
|
||||
attributes:
|
||||
error: ThermistorCurrentLimiter.Error
|
||||
temperature: readonly float32
|
||||
config:
|
||||
c_is_class: False
|
||||
@@ -519,7 +520,6 @@ interfaces:
|
||||
ODrive.OffboardThermistorCurrentLimiter:
|
||||
c_is_class: True
|
||||
attributes:
|
||||
error: ThermistorCurrentLimiter.Error
|
||||
temperature: readonly float32
|
||||
config:
|
||||
c_is_class: False
|
||||
@@ -617,6 +617,8 @@ interfaces:
|
||||
DcBusOverRegenCurrent: {doc: too much current pushed into the power supply}
|
||||
DcBusOverCurrent: {doc: too much current pulled out of the power supply}
|
||||
ModulationIsNan:
|
||||
MotorThermistorOverTemp: {doc: The motor thermistor measured a temperature above motor.motor_thermistor.config.temp_limit_upper}
|
||||
FetThermistorOverTemp: {doc: The inverter thermistor measured a temperature above motor.fet_thermistor.config.temp_limit_upper}
|
||||
armed_state:
|
||||
typeargs: {fibre.Property.mode: readonly}
|
||||
values:
|
||||
@@ -631,6 +633,8 @@ interfaces:
|
||||
DC_calib_phC: {type: float32, c_name: DC_calib_.phC}
|
||||
phase_current_rev_gain: float32
|
||||
effective_current_lim: readonly float32
|
||||
fet_thermistor: OnboardThermistorCurrentLimiter
|
||||
motor_thermistor: OffboardThermistorCurrentLimiter
|
||||
current_control:
|
||||
c_is_class: False
|
||||
attributes:
|
||||
@@ -746,12 +750,15 @@ interfaces:
|
||||
pos_gain:
|
||||
type: float32
|
||||
unit: (turn/s) / turn
|
||||
doc: units = (turn/s) / turn, default = 20
|
||||
vel_gain:
|
||||
type: float32
|
||||
unit: 'Nm/(turn/s)'
|
||||
doc: units = 'Nm/(turn/s), default = 0.16
|
||||
vel_integrator_gain:
|
||||
type: float32
|
||||
unit: Nm/(turn/s * s)
|
||||
doc: units = Nm/(turn/s * s), default = 0.32
|
||||
vel_limit:
|
||||
type: float32
|
||||
unit: turn/s
|
||||
@@ -1046,12 +1053,6 @@ valuetypes:
|
||||
doc:
|
||||
Endstops must be enabled to use this feature.
|
||||
|
||||
ODrive.ThermistorCurrentLimiter.Error:
|
||||
nullflag: None
|
||||
flags:
|
||||
OverTemp:
|
||||
doc: The thermistor temperature upper limit was exceeded.
|
||||
|
||||
ODrive.Encoder.Mode:
|
||||
values:
|
||||
Incremental:
|
||||
@@ -1179,4 +1180,4 @@ valuetypes:
|
||||
HighCurrent:
|
||||
#LowCurrent: # not implemented
|
||||
Gimbal: {value: 2}
|
||||
Acim:
|
||||
Acim:
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 217 KiB |
@@ -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,42 @@
|
||||
# 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
|
||||
```
|
||||
|
||||
### Customize configuration
|
||||
See [Configuration Reference](https://cli.vuejs.org/config/).
|
||||
@@ -0,0 +1,5 @@
|
||||
module.exports = {
|
||||
presets: [
|
||||
'@vue/cli-plugin-babel/preset'
|
||||
]
|
||||
}
|
||||
Generated
+15382
File diff suppressed because it is too large
Load Diff
@@ -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",
|
||||
"electron:build": "electron-icon-builder --input=./public/icon.png --output=build --flatten && vue-cli-service electron:build",
|
||||
"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": "^9.1.1",
|
||||
"file-saver": "^2.0.2",
|
||||
"socket.io": "^2.3.0",
|
||||
"socket.io-client": "^2.3.0",
|
||||
"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.2",
|
||||
"vue-socket.io": "^3.0.9",
|
||||
"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": "^9.0.0",
|
||||
"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-json-component": "^0.4.1",
|
||||
"vue-template-compiler": "^2.6.11"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"root": true,
|
||||
"env": {
|
||||
"node": true
|
||||
},
|
||||
"extends": [
|
||||
"plugin:vue/essential",
|
||||
"eslint:recommended"
|
||||
],
|
||||
"parserOptions": {
|
||||
"parser": "babel-eslint"
|
||||
},
|
||||
"rules": {}
|
||||
},
|
||||
"browserslist": [
|
||||
"> 1%",
|
||||
"last 2 versions",
|
||||
"not dead"
|
||||
]
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
@@ -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,276 @@
|
||||
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
|
||||
|
||||
# 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 = 100
|
||||
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')
|
||||
|
||||
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 = '{:012X}'.format(device.serial_number) if hasattr(device, 'serial_number') else "[unknown serial number]"
|
||||
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)
|
||||
shutdown = fibre.Event()
|
||||
fibre.find_all("usb", None, discovered_device, shutdown, shutdown, log)
|
||||
|
||||
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 RO._remote_attributes.keys():
|
||||
if isinstance(RO._remote_attributes[key], fibre.remote_object.RemoteObject):
|
||||
# recurse
|
||||
returnDict[key] = dictFromRO(RO._remote_attributes[key])
|
||||
elif isinstance(RO._remote_attributes[key], fibre.remote_object.RemoteProperty):
|
||||
# grab value of that property
|
||||
# indicate if this property can be written or not
|
||||
returnDict[key] = {"val": str(RO._remote_attributes[key].get_value()),
|
||||
"readonly": not RO._remote_attributes[key]._can_write,
|
||||
"type": str(RO._remote_attributes[key]._property_type.__name__)}
|
||||
elif isinstance(RO._remote_attributes[key], fibre.remote_object.RemoteFunction):
|
||||
# this is a function - do nothing for now.
|
||||
returnDict[key] = "function"
|
||||
else:
|
||||
returnDict[key] = RO._remote_attributes[key]
|
||||
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:
|
||||
#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 = RO._remote_attributes[key]
|
||||
if argType == "number":
|
||||
RO.set_value(float(value))
|
||||
elif argType == "boolean":
|
||||
RO.set_value(value)
|
||||
else:
|
||||
pass # dont support that type yet
|
||||
except fibre.protocol.ChannelBrokenException:
|
||||
handle_disconnect(odrv)
|
||||
except:
|
||||
print("exception in postVal")
|
||||
|
||||
def getVal(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 = RO._remote_attributes[key]
|
||||
if isinstance(RO, fibre.remote_object.RemoteObject):
|
||||
return dictFromRO(RO)
|
||||
else:
|
||||
return RO.get_value()
|
||||
except fibre.protocol.ChannelBrokenException:
|
||||
handle_disconnect(odrv)
|
||||
except:
|
||||
print("exception in getVal")
|
||||
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 = RO._remote_attributes[key]
|
||||
if isinstance(RO, fibre.remote_object.RemoteFunction):
|
||||
RO.__call__()
|
||||
except fibre.protocol.ChannelBrokenException:
|
||||
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)
|
||||
+398
@@ -0,0 +1,398 @@
|
||||
<template>
|
||||
<div id="app">
|
||||
<!-- HEADER -->
|
||||
<div class="header">
|
||||
<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>
|
||||
<button class="dash-button" @click="exportDash">export dash</button>
|
||||
<button class="dash-button" @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>
|
||||
</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";
|
||||
|
||||
export default {
|
||||
name: "App",
|
||||
components: {
|
||||
Start,
|
||||
Dashboard,
|
||||
Axis,
|
||||
Wizard,
|
||||
},
|
||||
data: function () {
|
||||
return {
|
||||
//currentDash: "Start",
|
||||
};
|
||||
},
|
||||
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: {
|
||||
changeDash(dashName) {
|
||||
console.log(dashName);
|
||||
this.$store.commit("setDash", dashName);
|
||||
},
|
||||
//updateOdrives() {
|
||||
// if (this.$store.state.serverConnected == true) {
|
||||
//} && this.sampling == false) {
|
||||
// this.$store.dispatch("getOdrives");
|
||||
// }
|
||||
//setTimeout(() => {
|
||||
// this.updateOdrives();
|
||||
//}, 1000);
|
||||
//console.log("updating data...");
|
||||
//},
|
||||
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);
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
</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": "Starup 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": "#195bd7"
|
||||
},
|
||||
{
|
||||
"path": "odrives.odrive0.axis0.encoder.pos_estimate",
|
||||
"color": "#d6941a"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "d9ee2474-3e53-408f-9eab-1656342eb531",
|
||||
"vars": [
|
||||
{
|
||||
"path": "odrives.odrive0.axis0.controller.vel_setpoint",
|
||||
"color": "#195bd7"
|
||||
},
|
||||
{
|
||||
"path": "odrives.odrive0.axis0.encoder.vel_estimate",
|
||||
"color": "#d6941a"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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": "Starup 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": "#195bd7"
|
||||
},
|
||||
{
|
||||
"path": "odrives.odrive0.axis0.encoder.pos_estimate",
|
||||
"color": "#d6941a"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "d9ee2474-3e53-408f-9eab-1656342eb531",
|
||||
"vars": [
|
||||
{
|
||||
"path": "odrives.odrive0.axis0.controller.vel_setpoint",
|
||||
"color": "#195bd7"
|
||||
},
|
||||
{
|
||||
"path": "odrives.odrive0.axis0.encoder.vel_estimate",
|
||||
"color": "#d6941a"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 169 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 171 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 109 KiB |
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user