Merge branch 'devel' into libfibre

This commit is contained in:
Samuel Sadok
2020-10-30 16:59:01 +01:00
133 changed files with 24088 additions and 838 deletions
+11 -4
View File
@@ -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:
+5
View File
@@ -58,3 +58,8 @@ ODrive\.files
ODrive\.includes
Firmware/Tests/bin/
# GUI
GUI/dist_electron
GUI/node_modules
GUI/build
+4 -4
View File
@@ -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;
+1 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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"
}
+8
View File
@@ -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);
}
+8 -7
View File
@@ -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
+13 -28
View File
@@ -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;
+17 -14
View File
@@ -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;
+5 -2
View File
@@ -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;
+1 -4
View File
@@ -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) {
+12 -1
View File
@@ -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_;
+1 -1
View File
@@ -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;
+7 -6
View File
@@ -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;
+22 -8
View File
@@ -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_;
+5 -1
View File
@@ -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]
+7 -4
View File
@@ -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));
}
+4 -3
View File
@@ -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();
};
+3
View File
@@ -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);
+2 -1
View File
@@ -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'
+28 -1
View File
@@ -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
View File
@@ -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
+46 -41
View File
@@ -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
+26 -21
View File
@@ -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) {}
+2 -2
View File
@@ -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_;
+20 -19
View File
@@ -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

+28
View File
@@ -0,0 +1,28 @@
.DS_Store
node_modules
/dist
# local env files
.env.local
.env.*.local
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
#Electron-builder output
/dist_electron
#Electron icon generator output
/build
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020 ODrive Robotics
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+42
View File
@@ -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/).
+5
View File
@@ -0,0 +1,5 @@
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset'
]
}
+15382
View File
File diff suppressed because it is too large Load Diff
+70
View File
@@ -0,0 +1,70 @@
{
"name": "ODriveGUI",
"version": "0.1.0",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint",
"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

+17
View File
@@ -0,0 +1,17 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title><%= htmlWebpackPlugin.options.title %></title>
</head>
<body>
<noscript>
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>
+276
View File
@@ -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
View File
@@ -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"
}
]
}
]
}
+127
View File
@@ -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