From da7f7192833e0f02501378e1505989ed3fa3a137 Mon Sep 17 00:00:00 2001 From: Samuel Sadok Date: Thu, 14 May 2020 11:02:36 +0200 Subject: [PATCH 01/28] improve naming consistency between code and fibre Consistent naming makes code autogeneration easier. This commit does not claim that the exported names of the variables were more sensible than the in-code names. However changing the exported names can break external tools and needs to happen in a controlled and documented way. --- Firmware/MotorControl/axis.cpp | 16 ++++----- Firmware/MotorControl/axis.hpp | 30 ++++++++-------- Firmware/MotorControl/controller.cpp | 34 +++++++++---------- Firmware/MotorControl/controller.hpp | 32 ++++++++--------- Firmware/MotorControl/encoder.cpp | 2 +- Firmware/MotorControl/encoder.hpp | 8 ++--- Firmware/MotorControl/low_level.cpp | 2 +- Firmware/MotorControl/motor.cpp | 2 +- Firmware/MotorControl/motor.hpp | 8 ++--- .../MotorControl/sensorless_estimator.hpp | 6 ++-- Firmware/Tests/test_can.cpp | 6 ++-- Firmware/communication/ascii_protocol.cpp | 8 ++--- Firmware/communication/can_simple.cpp | 10 +++--- Firmware/communication/interface_can.cpp | 12 +++---- Firmware/communication/interface_can.hpp | 12 +++---- docs/commands.md | 8 ++--- docs/getting-started.md | 8 ++--- docs/hoverboard.md | 2 +- docs/input_modes.md | 18 +++++----- tools/odrive/enums.py | 8 ++--- tools/odrive/tests/can_test.py | 4 +-- tools/odrive/tests/closed_loop_test.py | 8 ++--- tools/odrive/tests/endstop_test.py | 2 +- tools/odrive/tests/uart_ascii_test.py | 8 ++--- tools/setup_hall_as_index.py | 2 +- 25 files changed, 128 insertions(+), 128 deletions(-) diff --git a/Firmware/MotorControl/axis.cpp b/Firmware/MotorControl/axis.cpp index b68ef01b..c1d11a4b 100644 --- a/Firmware/MotorControl/axis.cpp +++ b/Firmware/MotorControl/axis.cpp @@ -25,7 +25,7 @@ Axis::Axis(int axis_num, sensorless_estimator_(sensorless_estimator), controller_(controller), motor_(motor), - trap_(trap), + trap_traj_(trap), min_endstop_(min_endstop), max_endstop_(max_endstop) { @@ -33,7 +33,7 @@ Axis::Axis(int axis_num, sensorless_estimator_.axis_ = this; controller_.axis_ = this; motor_.axis_ = this; - trap_.axis_ = this; + trap_traj_.axis_ = this; min_endstop_.axis_ = this; max_endstop_.axis_ = this; decode_step_dir_pins(); @@ -173,7 +173,7 @@ bool Axis::do_checks() { // controller_.do_checks(); // Check for endstop presses - bool vel_dependent_stopping = (current_state_ == AXIS_STATE_HOMING) && (controller_.config_.control_mode >= Controller::CTRL_MODE_VELOCITY_CONTROL); + bool vel_dependent_stopping = (current_state_ == AXIS_STATE_HOMING) && (controller_.config_.control_mode >= Controller::CONTROL_MODE_VELOCITY_CONTROL); if (min_endstop_.config_.enabled && min_endstop_.get_state() && (!vel_dependent_stopping || controller_.vel_setpoint_ < 0.0f)) { error_ |= ERROR_MIN_ENDSTOP_PRESSED; } else if (max_endstop_.config_.enabled && max_endstop_.get_state() && (!vel_dependent_stopping || controller_.vel_setpoint_ > 0.0f)) { @@ -331,8 +331,8 @@ bool Axis::run_closed_loop_control_loop() { // Slowly drive in the negative direction at homing_speed until the min endstop is pressed // When pressed, set the linear count to the offset (default 0), and then go to position 0 bool Axis::run_homing() { - Controller::ControlMode_t stored_control_mode = controller_.config_.control_mode; - Controller::InputMode_t stored_input_mode = controller_.config_.input_mode; + Controller::ControlMode stored_control_mode = controller_.config_.control_mode; + Controller::InputMode stored_input_mode = controller_.config_.input_mode; // TODO: theoretically this check should be inside the update loop, // otherwise someone could disable the endstop while homing is in progress. @@ -340,7 +340,7 @@ bool Axis::run_homing() { return error_ |= ERROR_HOMING_WITHOUT_ENDSTOP, false; } - controller_.config_.control_mode = Controller::CTRL_MODE_VELOCITY_CONTROL; + controller_.config_.control_mode = Controller::CONTROL_MODE_VELOCITY_CONTROL; controller_.config_.input_mode = Controller::INPUT_MODE_VEL_RAMP; controller_.input_pos_ = 0.0f; @@ -381,7 +381,7 @@ bool Axis::run_homing() { // Set our current position in encoder counts to make control more logical encoder_.set_linear_count(static_cast(controller_.pos_setpoint_)); - controller_.config_.control_mode = Controller::CTRL_MODE_POSITION_CONTROL; + controller_.config_.control_mode = Controller::CONTROL_MODE_POSITION_CONTROL; controller_.config_.input_mode = Controller::INPUT_MODE_TRAP_TRAJ; controller_.input_pos_ = 0.0f; @@ -499,7 +499,7 @@ void Axis::run_state_machine_loop() { case AXIS_STATE_LOCKIN_SPIN: { if (!motor_.is_calibrated_ || motor_.config_.direction==0) goto invalid_state_label; - status = run_lockin_spin(config_.lockin); + status = run_lockin_spin(config_.general_lockin); } break; case AXIS_STATE_SENSORLESS_CONTROL: { diff --git a/Firmware/MotorControl/axis.hpp b/Firmware/MotorControl/axis.hpp index 5b489a93..63925753 100644 --- a/Firmware/MotorControl/axis.hpp +++ b/Firmware/MotorControl/axis.hpp @@ -7,7 +7,7 @@ class Axis { public: - enum Error_t { + enum Error { ERROR_NONE = 0x00, ERROR_INVALID_STATE = 0x01, //error_ |= Axis::ERROR_CONTROLLER_FAILED; } @@ -50,11 +50,11 @@ bool Controller::select_encoder(size_t encoder_num) { } void Controller::move_to_pos(float goal_point) { - axis_->trap_.planTrapezoidal(goal_point, pos_setpoint_, vel_setpoint_, - axis_->trap_.config_.vel_limit, - axis_->trap_.config_.accel_limit, - axis_->trap_.config_.decel_limit); - axis_->trap_.t_ = 0.0f; + axis_->trap_traj_.planTrapezoidal(goal_point, pos_setpoint_, vel_setpoint_, + axis_->trap_traj_.config_.vel_limit, + axis_->trap_traj_.config_.accel_limit, + axis_->trap_traj_.config_.decel_limit); + axis_->trap_traj_.t_ = 0.0f; trajectory_done_ = false; } @@ -90,7 +90,7 @@ bool Controller::anticogging_calibration(float pos_estimate, float vel_estimate) config_.anticogging.cogging_map[std::clamp(config_.anticogging.index++, 0, 3600)] = vel_integrator_current_; } if (config_.anticogging.index < 3600) { - config_.control_mode = CTRL_MODE_POSITION_CONTROL; + config_.control_mode = CONTROL_MODE_POSITION_CONTROL; input_pos_ = config_.anticogging.index * axis_->encoder_.getCoggingRatio(); input_vel_ = 0.0f; input_current_ = 0.0f; @@ -98,7 +98,7 @@ bool Controller::anticogging_calibration(float pos_estimate, float vel_estimate) return false; } else { config_.anticogging.index = 0; - config_.control_mode = CTRL_MODE_POSITION_CONTROL; + config_.control_mode = CONTROL_MODE_POSITION_CONTROL; input_pos_ = 0.0f; // Send the motor home input_vel_ = 0.0f; input_current_ = 0.0f; @@ -200,19 +200,19 @@ bool Controller::update(float* current_setpoint_output) { if (trajectory_done_) break; - if (axis_->trap_.t_ > axis_->trap_.Tf_) { + if (axis_->trap_traj_.t_ > axis_->trap_traj_.Tf_) { // Drop into position control mode when done to avoid problems on loop counter delta overflow - config_.control_mode = CTRL_MODE_POSITION_CONTROL; + config_.control_mode = CONTROL_MODE_POSITION_CONTROL; pos_setpoint_ = input_pos_; vel_setpoint_ = 0.0f; current_setpoint_ = 0.0f; trajectory_done_ = true; } else { - TrapezoidalTrajectory::Step_t traj_step = axis_->trap_.eval(axis_->trap_.t_); + TrapezoidalTrajectory::Step_t traj_step = axis_->trap_traj_.eval(axis_->trap_traj_.t_); pos_setpoint_ = traj_step.Y; vel_setpoint_ = traj_step.Yd; current_setpoint_ = traj_step.Ydd * config_.inertia; - axis_->trap_.t_ += current_meas_period; + axis_->trap_traj_.t_ += current_meas_period; } anticogging_pos = pos_setpoint_; // FF the position setpoint instead of the pos_estimate } break; @@ -227,7 +227,7 @@ bool Controller::update(float* current_setpoint_output) { // TODO Decide if we want to use encoder or pll position here float gain_scheduling_multiplier = 1.0f; float vel_des = vel_setpoint_; - if (config_.control_mode >= CTRL_MODE_POSITION_CONTROL) { + if (config_.control_mode >= CONTROL_MODE_POSITION_CONTROL) { float pos_err; if (!pos_estimate_src) { set_error(ERROR_INVALID_ESTIMATE); @@ -292,12 +292,12 @@ bool Controller::update(float* current_setpoint_output) { // Anti-cogging is enabled after calibration // We get the current position and apply a current feed-forward // ensuring that we handle negative encoder positions properly (-1 == motor->encoder.encoder_cpr - 1) - if (anticogging_valid_ && config_.anticogging.enable) { + if (anticogging_valid_ && config_.anticogging.anticogging_enabled) { Iq += config_.anticogging.cogging_map[std::clamp(mod(static_cast(anticogging_pos), 3600), 0, 3600)]; } float v_err = 0.0f; - if (config_.control_mode >= CTRL_MODE_VELOCITY_CONTROL) { + if (config_.control_mode >= CONTROL_MODE_VELOCITY_CONTROL) { if (!vel_estimate_src) { set_error(ERROR_INVALID_ESTIMATE); return false; @@ -311,7 +311,7 @@ bool Controller::update(float* current_setpoint_output) { } // Velocity limiting in current mode - if (config_.control_mode < CTRL_MODE_VELOCITY_CONTROL && config_.enable_current_vel_limit) { + if (config_.control_mode < CONTROL_MODE_VELOCITY_CONTROL && config_.enable_current_mode_vel_limit) { if (!vel_estimate_src) { set_error(ERROR_INVALID_ESTIMATE); return false; @@ -334,7 +334,7 @@ bool Controller::update(float* current_setpoint_output) { } // Velocity integrator (behaviour dependent on limiting) - if (config_.control_mode < CTRL_MODE_VELOCITY_CONTROL) { + if (config_.control_mode < CONTROL_MODE_VELOCITY_CONTROL) { // reset integral if not in use vel_integrator_current_ = 0.0f; } else { diff --git a/Firmware/MotorControl/controller.hpp b/Firmware/MotorControl/controller.hpp index 56a60020..293b892a 100644 --- a/Firmware/MotorControl/controller.hpp +++ b/Firmware/MotorControl/controller.hpp @@ -7,7 +7,7 @@ class Controller { public: - enum Error_t { + enum Error { ERROR_NONE = 0, ERROR_OVERSPEED = 0x01, ERROR_INVALID_INPUT_MODE = 0x02, @@ -19,14 +19,14 @@ public: // Note: these should be sorted from lowest level of control to // highest level of control, to allow "<" style comparisons. - enum ControlMode_t{ - CTRL_MODE_VOLTAGE_CONTROL = 0, - CTRL_MODE_CURRENT_CONTROL = 1, - CTRL_MODE_VELOCITY_CONTROL = 2, - CTRL_MODE_POSITION_CONTROL = 3 + enum ControlMode{ + CONTROL_MODE_VOLTAGE_CONTROL = 0, + CONTROL_MODE_CURRENT_CONTROL = 1, + CONTROL_MODE_VELOCITY_CONTROL = 2, + CONTROL_MODE_POSITION_CONTROL = 3 }; - enum InputMode_t{ + enum InputMode{ INPUT_MODE_INACTIVE, INPUT_MODE_PASSTHROUGH, INPUT_MODE_VEL_RAMP, @@ -45,12 +45,12 @@ public: float calib_pos_threshold = 1.0f; float calib_vel_threshold = 1.0f; float cogging_ratio = 1.0f; - bool enable = true; + bool anticogging_enabled = true; } Anticogging_t; struct Config_t { - ControlMode_t control_mode = CTRL_MODE_POSITION_CONTROL; //see: ControlMode_t - InputMode_t input_mode = INPUT_MODE_PASSTHROUGH; //see: InputMode_t + ControlMode control_mode = CONTROL_MODE_POSITION_CONTROL; //see: ControlMode + InputMode input_mode = INPUT_MODE_PASSTHROUGH; //see: InputMode float pos_gain = 20.0f; // [(counts/s) / counts] float vel_gain = 5.0f / 10000.0f; // [A/(counts/s)] // float vel_gain = 5.0f / 200.0f, // [A/(rad/s)] @@ -68,7 +68,7 @@ public: bool enable_gain_scheduling = false; bool enable_vel_limit = true; bool enable_overspeed_error = true; - bool enable_current_vel_limit = true; // enable velocity limit in current control mode (requires a valid velocity estimator) + bool enable_current_mode_vel_limit = true; // enable velocity limit in current control mode (requires a valid velocity estimator) uint8_t axis_to_mirror = -1; float mirror_ratio = 1.0f; uint8_t load_encoder_axis = -1; // default depends on Axis number and is set in load_configuration() @@ -76,7 +76,7 @@ public: explicit Controller(Config_t& config); void reset(); - void set_error(Error_t error); + void set_error(Error error); void input_pos_updated(); bool select_encoder(size_t encoder_num); @@ -95,7 +95,7 @@ public: Config_t& config_; Axis* axis_ = nullptr; // set by Axis constructor - Error_t error_ = ERROR_NONE; + Error error_ = ERROR_NONE; float* pos_estimate_src_ = nullptr; bool* pos_estimate_valid_src_ = nullptr; @@ -138,7 +138,7 @@ public: make_protocol_property("gain_scheduling_width", &config_.gain_scheduling_width), make_protocol_object("config", make_protocol_property("enable_vel_limit", &config_.enable_vel_limit), - make_protocol_property("enable_current_mode_vel_limit", &config_.enable_current_vel_limit), + make_protocol_property("enable_current_mode_vel_limit", &config_.enable_current_mode_vel_limit), make_protocol_property("enable_gain_scheduling", &config_.enable_gain_scheduling), make_protocol_property("enable_overspeed_error", &config_.enable_overspeed_error), make_protocol_property("control_mode", &config_.control_mode), @@ -164,13 +164,13 @@ public: make_protocol_property("calib_pos_threshold", &config_.anticogging.calib_pos_threshold), make_protocol_property("calib_vel_threshold", &config_.anticogging.calib_vel_threshold), make_protocol_ro_property("cogging_ratio", &config_.anticogging.cogging_ratio), - make_protocol_property("anticogging_enabled", &config_.anticogging.enable))), + make_protocol_property("anticogging_enabled", &config_.anticogging.anticogging_enabled))), make_protocol_function("move_incremental", *this, &Controller::move_incremental, "displacement", "from_goal_point"), make_protocol_function("start_anticogging_calibration", *this, &Controller::start_anticogging_calibration) ); } }; -DEFINE_ENUM_FLAG_OPERATORS(Controller::Error_t) +DEFINE_ENUM_FLAG_OPERATORS(Controller::Error) #endif // __CONTROLLER_HPP diff --git a/Firmware/MotorControl/encoder.cpp b/Firmware/MotorControl/encoder.cpp index 77b809aa..c21683e7 100644 --- a/Firmware/MotorControl/encoder.cpp +++ b/Firmware/MotorControl/encoder.cpp @@ -35,7 +35,7 @@ void Encoder::setup() { } } -void Encoder::set_error(Error_t error) { +void Encoder::set_error(Error error) { vel_estimate_valid_ = false; pos_estimate_valid_ = false; error_ |= error; diff --git a/Firmware/MotorControl/encoder.hpp b/Firmware/MotorControl/encoder.hpp index 28830c38..77ad84b1 100644 --- a/Firmware/MotorControl/encoder.hpp +++ b/Firmware/MotorControl/encoder.hpp @@ -7,7 +7,7 @@ class Encoder { public: - enum Error_t { + enum Error { ERROR_NONE = 0, ERROR_UNSTABLE_GAIN = 0x01, ERROR_CPR_POLEPAIRS_MISMATCH = 0x02, @@ -59,7 +59,7 @@ public: Config_t& config, const Motor::Config_t& motor_config); void setup(); - void set_error(Error_t error); + void set_error(Error error); bool do_checks(); void enc_index_cb(); @@ -81,7 +81,7 @@ public: Config_t& config_; Axis* axis_ = nullptr; // set by Axis constructor - Error_t error_ = ERROR_NONE; + Error error_ = ERROR_NONE; bool index_found_ = false; bool is_ready_ = false; int32_t shadow_count_ = 0; @@ -171,6 +171,6 @@ public: } }; -DEFINE_ENUM_FLAG_OPERATORS(Encoder::Error_t) +DEFINE_ENUM_FLAG_OPERATORS(Encoder::Error) #endif // __ENCODER_HPP diff --git a/Firmware/MotorControl/low_level.cpp b/Firmware/MotorControl/low_level.cpp index fda04cf4..f0e32f0c 100644 --- a/Firmware/MotorControl/low_level.cpp +++ b/Firmware/MotorControl/low_level.cpp @@ -84,7 +84,7 @@ static uint16_t GPIO_port_samples [2][num_GPIO]; */ // @brief Floats ALL phases immediately and disarms both motors and the brake resistor. -void low_level_fault(Motor::Error_t error) { +void low_level_fault(Motor::Error error) { // Disable all motors NOW! for (size_t i = 0; i < AXIS_COUNT; ++i) { safety_critical_disarm_motor_pwm(axes[i]->motor_); diff --git a/Firmware/MotorControl/motor.cpp b/Firmware/MotorControl/motor.cpp index 223aba1c..6b8c8d0f 100644 --- a/Firmware/MotorControl/motor.cpp +++ b/Firmware/MotorControl/motor.cpp @@ -137,7 +137,7 @@ bool Motor::check_DRV_fault() { return true; } -void Motor::set_error(Motor::Error_t error){ +void Motor::set_error(Motor::Error error){ error_ |= error; axis_->error_ |= Axis::ERROR_MOTOR_FAILED; safety_critical_disarm_motor_pwm(*this); diff --git a/Firmware/MotorControl/motor.hpp b/Firmware/MotorControl/motor.hpp index 8c627aa7..67a0751d 100644 --- a/Firmware/MotorControl/motor.hpp +++ b/Firmware/MotorControl/motor.hpp @@ -9,7 +9,7 @@ class Motor { public: - enum Error_t { + enum Error { ERROR_NONE = 0, ERROR_PHASE_RESISTANCE_OUT_OF_RANGE = 0x0001, ERROR_PHASE_INDUCTANCE_OUT_OF_RANGE = 0x0002, @@ -128,7 +128,7 @@ public: void update_current_controller_gains(); void DRV8301_setup(); bool check_DRV_fault(); - void set_error(Error_t error); + void set_error(Error error); bool do_checks(); float get_inverter_temp(); bool update_thermal_limits(float fet_temp); @@ -163,7 +163,7 @@ public: uint16_t timing_log_[TIMING_LOG_NUM_SLOTS] = { 0 }; // variables exposed on protocol - Error_t error_ = ERROR_NONE; + Error error_ = ERROR_NONE; // Do not write to this variable directly! // It is for exclusive use by the safety_critical_... functions. ArmedState_t armed_state_ = ARMED_STATE_DISARMED; @@ -279,6 +279,6 @@ public: } }; -DEFINE_ENUM_FLAG_OPERATORS(Motor::Error_t) +DEFINE_ENUM_FLAG_OPERATORS(Motor::Error) #endif // __MOTOR_HPP diff --git a/Firmware/MotorControl/sensorless_estimator.hpp b/Firmware/MotorControl/sensorless_estimator.hpp index e47db893..dd48c705 100644 --- a/Firmware/MotorControl/sensorless_estimator.hpp +++ b/Firmware/MotorControl/sensorless_estimator.hpp @@ -3,7 +3,7 @@ class SensorlessEstimator { public: - enum Error_t { + enum Error { ERROR_NONE = 0, ERROR_UNSTABLE_GAIN = 0x01, }; @@ -22,7 +22,7 @@ public: Config_t& config_; // TODO: expose on protocol - Error_t error_ = ERROR_NONE; + Error error_ = ERROR_NONE; float phase_ = 0.0f; // [rad] float pll_pos_ = 0.0f; // [rad] float vel_estimate_ = 0.0f; // [rad/s] @@ -51,6 +51,6 @@ public: } }; -DEFINE_ENUM_FLAG_OPERATORS(SensorlessEstimator::Error_t) +DEFINE_ENUM_FLAG_OPERATORS(SensorlessEstimator::Error) #endif /* __SENSORLESS_ESTIMATOR_HPP */ diff --git a/Firmware/Tests/test_can.cpp b/Firmware/Tests/test_can.cpp index ef2ffecf..5a0db2fe 100644 --- a/Firmware/Tests/test_can.cpp +++ b/Firmware/Tests/test_can.cpp @@ -5,7 +5,7 @@ #include "communication/can_helpers.hpp" -enum InputMode_t { +enum InputMode { INPUT_MODE_INACTIVE, INPUT_MODE_PASSTHROUGH, INPUT_MODE_VEL_RAMP, @@ -84,7 +84,7 @@ TEST_SUITE("CAN Functions") { can_Message_t rxmsg; rxmsg.buf[0] = INPUT_MODE_MIX_CHANNELS; rxmsg.buf[1] = INPUT_MODE_PASSTHROUGH; - CHECK(static_cast(can_getSignal(rxmsg, 0, 8, true, 1, 0)) == INPUT_MODE_MIX_CHANNELS); - CHECK(static_cast(can_getSignal(rxmsg, 8, 8, true, 1, 0)) == INPUT_MODE_PASSTHROUGH); + CHECK(static_cast(can_getSignal(rxmsg, 0, 8, true, 1, 0)) == INPUT_MODE_MIX_CHANNELS); + CHECK(static_cast(can_getSignal(rxmsg, 8, 8, true, 1, 0)) == INPUT_MODE_PASSTHROUGH); } } \ No newline at end of file diff --git a/Firmware/communication/ascii_protocol.cpp b/Firmware/communication/ascii_protocol.cpp index f0ee9f9c..628dea63 100644 --- a/Firmware/communication/ascii_protocol.cpp +++ b/Firmware/communication/ascii_protocol.cpp @@ -97,7 +97,7 @@ void ASCII_protocol_process_line(const uint8_t* buffer, size_t len, StreamSink& respond(response_channel, use_checksum, "invalid motor %u", motor_number); } else { Axis* axis = axes[motor_number]; - axis->controller_.config_.control_mode = Controller::CTRL_MODE_POSITION_CONTROL; + axis->controller_.config_.control_mode = Controller::CONTROL_MODE_POSITION_CONTROL; axis->controller_.input_pos_ = pos_setpoint; if (numscan >= 3) axis->controller_.input_vel_ = vel_feed_forward; @@ -117,7 +117,7 @@ void ASCII_protocol_process_line(const uint8_t* buffer, size_t len, StreamSink& respond(response_channel, use_checksum, "invalid motor %u", motor_number); } else { Axis* axis = axes[motor_number]; - axis->controller_.config_.control_mode = Controller::CTRL_MODE_POSITION_CONTROL; + axis->controller_.config_.control_mode = Controller::CONTROL_MODE_POSITION_CONTROL; axis->controller_.input_pos_ = pos_setpoint; if (numscan >= 3) axis->controller_.config_.vel_limit = vel_limit; @@ -137,7 +137,7 @@ void ASCII_protocol_process_line(const uint8_t* buffer, size_t len, StreamSink& respond(response_channel, use_checksum, "invalid motor %u", motor_number); } else { Axis* axis = axes[motor_number]; - axis->controller_.config_.control_mode = Controller::CTRL_MODE_VELOCITY_CONTROL; + axis->controller_.config_.control_mode = Controller::CONTROL_MODE_VELOCITY_CONTROL; axis->controller_.input_vel_ = vel_setpoint; if (numscan >= 3) axis->controller_.input_current_ = current_feed_forward; @@ -154,7 +154,7 @@ void ASCII_protocol_process_line(const uint8_t* buffer, size_t len, StreamSink& respond(response_channel, use_checksum, "invalid motor %u", motor_number); } else { Axis* axis = axes[motor_number]; - axis->controller_.config_.control_mode = Controller::CTRL_MODE_CURRENT_CONTROL; + axis->controller_.config_.control_mode = Controller::CONTROL_MODE_CURRENT_CONTROL; axis->controller_.input_current_ = current_setpoint; axis->watchdog_feed(); } diff --git a/Firmware/communication/can_simple.cpp b/Firmware/communication/can_simple.cpp index da22bb70..5ff8009a 100644 --- a/Firmware/communication/can_simple.cpp +++ b/Firmware/communication/can_simple.cpp @@ -295,8 +295,8 @@ void CANSimple::set_input_current_callback(Axis* axis, can_Message_t& msg) { } void CANSimple::set_controller_modes_callback(Axis* axis, can_Message_t& msg) { - axis->controller_.config_.control_mode = static_cast(can_getSignal(msg, 0, 32, true)); - axis->controller_.config_.input_mode = static_cast(can_getSignal(msg, 32, 32, true)); + axis->controller_.config_.control_mode = static_cast(can_getSignal(msg, 0, 32, true)); + axis->controller_.config_.input_mode = static_cast(can_getSignal(msg, 32, 32, true)); } void CANSimple::set_vel_limit_callback(Axis* axis, can_Message_t& msg) { @@ -308,12 +308,12 @@ void CANSimple::start_anticogging_callback(Axis* axis, can_Message_t& msg) { } void CANSimple::set_traj_vel_limit_callback(Axis* axis, can_Message_t& msg) { - axis->trap_.config_.vel_limit = can_getSignal(msg, 0, 32, true); + axis->trap_traj_.config_.vel_limit = can_getSignal(msg, 0, 32, true); } void CANSimple::set_traj_accel_limits_callback(Axis* axis, can_Message_t& msg) { - axis->trap_.config_.accel_limit = can_getSignal(msg, 0, 32, true); - axis->trap_.config_.decel_limit = can_getSignal(msg, 32, 32, true); + axis->trap_traj_.config_.accel_limit = can_getSignal(msg, 0, 32, true); + axis->trap_traj_.config_.decel_limit = can_getSignal(msg, 32, 32, true); } void CANSimple::set_traj_A_per_css_callback(Axis* axis, can_Message_t& msg) { diff --git a/Firmware/communication/interface_can.cpp b/Firmware/communication/interface_can.cpp index bf7f9ff0..339ef9ff 100644 --- a/Firmware/communication/interface_can.cpp +++ b/Firmware/communication/interface_can.cpp @@ -57,7 +57,7 @@ static void can_server_thread_wrapper(void *ctx) { bool ODriveCAN::start_can_server() { HAL_StatusTypeDef status; - set_baud_rate(config_.baud); + set_baud_rate(config_.baud_rate); status = HAL_CAN_Init(handle_); @@ -136,25 +136,25 @@ void ODriveCAN::set_baud_rate(uint32_t baudRate) { switch (baudRate) { case CAN_BAUD_125K: handle_->Init.Prescaler = 16; // 21 TQ's - config_.baud = baudRate; + config_.baud_rate = baudRate; reinit_can(); break; case CAN_BAUD_250K: handle_->Init.Prescaler = 8; // 21 TQ's - config_.baud = baudRate; + config_.baud_rate = baudRate; reinit_can(); break; case CAN_BAUD_500K: handle_->Init.Prescaler = 4; // 21 TQ's - config_.baud = baudRate; + config_.baud_rate = baudRate; reinit_can(); break; case CAN_BAUD_1000K: handle_->Init.Prescaler = 2; // 21 TQ's - config_.baud = baudRate; + config_.baud_rate = baudRate; reinit_can(); break; @@ -172,7 +172,7 @@ void ODriveCAN::reinit_can() { status = HAL_CAN_ActivateNotification(handle_, CAN_IT_RX_FIFO0_MSG_PENDING); } -void ODriveCAN::set_error(Error_t error) { +void ODriveCAN::set_error(Error error) { error_ |= error; } // This function is called by each axis. diff --git a/Firmware/communication/interface_can.hpp b/Firmware/communication/interface_can.hpp index ffb29bb7..fca28822 100644 --- a/Firmware/communication/interface_can.hpp +++ b/Firmware/communication/interface_can.hpp @@ -26,11 +26,11 @@ enum CAN_Protocol_t { class ODriveCAN { public: struct Config_t { - uint32_t baud = CAN_BAUD_250K; + uint32_t baud_rate = CAN_BAUD_250K; CAN_Protocol_t protocol = CAN_PROTOCOL_SIMPLE; }; - enum Error_t { + enum Error { ERROR_NONE = 0x00, ERROR_DUPLICATE_CAN_IDS = 0x01 }; @@ -40,7 +40,7 @@ class ODriveCAN { // Thread Relevant Data osThreadId thread_id_; const uint32_t stack_size_ = 1024; // Bytes - Error_t error_ = ERROR_NONE; + Error error_ = ERROR_NONE; volatile bool thread_id_valid_ = false; bool start_can_server(); @@ -48,7 +48,7 @@ class ODriveCAN { void send_heartbeat(Axis *axis); void reinit_can(); - void set_error(Error_t error); + void set_error(Error error); // I/O Functions uint32_t available(); @@ -60,7 +60,7 @@ class ODriveCAN { return make_protocol_member_list( make_protocol_property("error", &error_), make_protocol_object("config", - make_protocol_ro_property("baud_rate", &config_.baud)), + make_protocol_ro_property("baud_rate", &config_.baud_rate)), make_protocol_property("can_protocol", &config_.protocol), make_protocol_function("set_baud_rate", *this, &ODriveCAN::set_baud_rate, "baudRate")); } @@ -73,6 +73,6 @@ class ODriveCAN { }; -DEFINE_ENUM_FLAG_OPERATORS(ODriveCAN::Error_t) +DEFINE_ENUM_FLAG_OPERATORS(ODriveCAN::Error) #endif // __INTERFACE_CAN_HPP diff --git a/docs/commands.md b/docs/commands.md index 33f7eefc..24fb3fdc 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -55,10 +55,10 @@ See [state machine](#state-machine) for a description of each state. The default control mode is position control. If you want a different mode, you can change `.controller.config.control_mode`. Possible values are: -* `CTRL_MODE_POSITION_CONTROL` -* `CTRL_MODE_VELOCITY_CONTROL` -* `CTRL_MODE_CURRENT_CONTROL` -* `CTRL_MODE_VOLTAGE_CONTROL` - this one is not normally used. +* `CONTROL_MODE_POSITION_CONTROL` +* `CONTROL_MODE_VELOCITY_CONTROL` +* `CONTROL_MODE_CURRENT_CONTROL` +* `CONTROL_MODE_VOLTAGE_CONTROL` - this one is not normally used. ### Input Mode The default input mode is `INPUT_MODE_PASSTHROUGH`. diff --git a/docs/getting-started.md b/docs/getting-started.md index 05fd2f94..3287dcab 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -337,20 +337,20 @@ Note that in this mode `encoder.pos_cpr` is used for feedback in stead of `encod If you try to increment the axis with a large step in one go that exceeds `cpr/2` steps, the motor will go to the same angle around the wrong way. This is also the case if there is a large disturbance. If you have an application where you would like to handle larger steps, you can use a virtual CPR that is an integer times larger than your encoder's actual CPR. Set `encoder.config.cpr = N * your_enc_cpr`, where N is some integer. Choose N to give you an appropriate circular space for your application. ### Velocity control -Set `axis.controller.config.control_mode = CTRL_MODE_VELOCITY_CONTROL`.
+Set `axis.controller.config.control_mode = CONTROL_MODE_VELOCITY_CONTROL`.
You can now control the velocity with `axis.controller.input_vel = 5000` [count/s]. ### Ramped velocity control -Set `axis.controller.config.control_mode = CTRL_MODE_VELOCITY_CONTROL`.
+Set `axis.controller.config.control_mode = CONTROL_MODE_VELOCITY_CONTROL`.
Set the velocity ramp rate (acceleration): `axis.controller.config.vel_ramp_rate = 2000` [counts/s^2]
Activate the ramped velocity mode: `axis.controller.config.input_mode = INPUT_MODE_VEL_RAMP`.
You can now control the velocity with `axis.controller.input_vel = 5000` [count/s]. ### Current control -Set `axis.controller.config.control_mode = CTRL_MODE_CURRENT_CONTROL`.
+Set `axis.controller.config.control_mode = CONTROL_MODE_CURRENT_CONTROL`.
You can now control the current with `axis.controller.input_current = 3` [A]. -Note: If you exceed `vel_limit` in current control mode, the current is reduced. To disable this, set `axis.controller.enable_current_vel_limit = False`. +Note: If you exceed `vel_limit` in current control mode, the current is reduced. To disable this, set `axis.controller.enable_current_mode_vel_limit = False`. ## Watchdog Timer Each axis has a configurable watchdog timer that can stop the motors if the diff --git a/docs/hoverboard.md b/docs/hoverboard.md index 91b50764..93938613 100644 --- a/docs/hoverboard.md +++ b/docs/hoverboard.md @@ -52,7 +52,7 @@ odrv0.axis0.controller.config.pos_gain = 1 odrv0.axis0.controller.config.vel_gain = 0.02 odrv0.axis0.controller.config.vel_integrator_gain = 0.1 odrv0.axis0.controller.config.vel_limit = 1000 -odrv0.axis0.controller.config.control_mode = CTRL_MODE_VELOCITY_CONTROL +odrv0.axis0.controller.config.control_mode = CONTROL_MODE_VELOCITY_CONTROL ``` In the next step we are going to start powering the motor and so we want to make sure that some of the above settings that require a reboot are applied first. diff --git a/docs/input_modes.md b/docs/input_modes.md index 32d7df86..be449e24 100644 --- a/docs/input_modes.md +++ b/docs/input_modes.md @@ -30,10 +30,10 @@ Pass `input_xxx` through to `xxx_setpoint` directly. * `input_current` ### Valid Control modes: -* `CTRL_MODE_VOLTAGE_CONTROL` -* `CTRL_MODE_CURRENT_CONTROL` -* `CTRL_MODE_VELOCITY_CONTROL` -* `CTRL_MODE_POSITION_CONTROL` +* `CONTROL_MODE_VOLTAGE_CONTROL` +* `CONTROL_MODE_CURRENT_CONTROL` +* `CONTROL_MODE_VELOCITY_CONTROL` +* `CONTROL_MODE_POSITION_CONTROL` ## INPUT_MODE_VEL_RAMP Ramps a velocity command from the current value to the target value. @@ -46,7 +46,7 @@ Ramps a velocity command from the current value to the target value. * `input_vel` ### Valid Control Modes: -* `CTRL_MODE_VELOCITY_CONTROL` +* `CONTROL_MODE_VELOCITY_CONTROL` ## INPUT_MODE_POS_FILTER Implements a 2nd order position tracking filter. Inteded for use with step/dir interface, but can also be used with position-only commands. @@ -62,7 +62,7 @@ Result of a step command from 1000 to 0 * `input_pos` ### Valid Control modes: -* `CTRL_MODE_POSITION_CONTROL` +* `CONTROL_MODE_POSITION_CONTROL` ## INPUT_MODE_MIX_CHANNELS Not Implemented. @@ -83,7 +83,7 @@ Implementes an online trapezoidal trajectory planner. * `input_pos` ### Valid Control Modes: -* `CTRL_MODE_POSITION_CONTROL` +* `CONTROL_MODE_POSITION_CONTROL` ## INPUT_MODE_CURRENT_RAMP Ramp a current command from the current value to the target value. @@ -95,7 +95,7 @@ Ramp a current command from the current value to the target value. * `input_current` ### Valid Control Modes: -* `CTRL_MODE_CURRENT_CONTROL` +* `CONTROL_MODE_CURRENT_CONTROL` ## INPUT_MODE_MIRROR Implements "electronic mirroring". This is like electronic camming, but you can only mirror exactly the movements of the other motor, according to a fixed ratio @@ -110,4 +110,4 @@ Implements "electronic mirroring". This is like electronic camming, but you can * None. Inputs are taken directly from the other axis encoder estimates ### Valid Control modes -* `CTRL_MODE_POSITION_CONTROL` +* `CONTROL_MODE_POSITION_CONTROL` diff --git a/tools/odrive/enums.py b/tools/odrive/enums.py index 1198272e..e3d3ebd0 100644 --- a/tools/odrive/enums.py +++ b/tools/odrive/enums.py @@ -75,10 +75,10 @@ MOTOR_TYPE_HIGH_CURRENT = 0 #MOTOR_TYPE_LOW_CURRENT = 1 MOTOR_TYPE_GIMBAL = 2 -CTRL_MODE_VOLTAGE_CONTROL = 0 -CTRL_MODE_CURRENT_CONTROL = 1 -CTRL_MODE_VELOCITY_CONTROL = 2 -CTRL_MODE_POSITION_CONTROL = 3 +CONTROL_MODE_VOLTAGE_CONTROL = 0 +CONTROL_MODE_CURRENT_CONTROL = 1 +CONTROL_MODE_VELOCITY_CONTROL = 2 +CONTROL_MODE_POSITION_CONTROL = 3 INPUT_MODE_INACTIVE = 0 INPUT_MODE_PASSTHROUGH = 1 diff --git a/tools/odrive/tests/can_test.py b/tools/odrive/tests/can_test.py index e69a8bd9..164dd251 100644 --- a/tools/odrive/tests/can_test.py +++ b/tools/odrive/tests/can_test.py @@ -171,13 +171,13 @@ class TestSimpleCAN(): test_assert_eq(axis.controller.input_vel, 2.0, range=0.01) test_assert_eq(axis.controller.input_current, 3.0, range=0.001) - axis.controller.config.control_mode = CTRL_MODE_VELOCITY_CONTROL + axis.controller.config.control_mode = CONTROL_MODE_VELOCITY_CONTROL my_cmd('set_input_vel', input_vel=-10.0, cur_ff=30.1234) fence() test_assert_eq(axis.controller.input_vel, -10.0, range=0.01) test_assert_eq(axis.controller.input_current, 30.1234, range=0.01) - axis.controller.config.control_mode = CTRL_MODE_CURRENT_CONTROL + axis.controller.config.control_mode = CONTROL_MODE_CURRENT_CONTROL my_cmd('set_input_current', input_current=3.1415) fence() test_assert_eq(axis.controller.input_current, 3.1415, range=0.01) diff --git a/tools/odrive/tests/closed_loop_test.py b/tools/odrive/tests/closed_loop_test.py index cc07ab2a..01cc003a 100644 --- a/tools/odrive/tests/closed_loop_test.py +++ b/tools/odrive/tests/closed_loop_test.py @@ -80,7 +80,7 @@ class TestClosedLoopControl(TestClosedLoopControlBase): nominal_vel = float(enc_ctx.yaml['cpr']) * nominal_rps logger.debug(f'Testing closed loop velocity control at {nominal_rps} rounds/s...') - axis_ctx.handle.controller.config.control_mode = CTRL_MODE_VELOCITY_CONTROL + axis_ctx.handle.controller.config.control_mode = CONTROL_MODE_VELOCITY_CONTROL axis_ctx.handle.controller.config.input_mode = INPUT_MODE_PASSTHROUGH axis_ctx.handle.controller.input_vel = 0 @@ -107,7 +107,7 @@ class TestClosedLoopControl(TestClosedLoopControlBase): logger.debug(f'Testing closed loop position control...') - axis_ctx.handle.controller.config.control_mode = CTRL_MODE_POSITION_CONTROL + axis_ctx.handle.controller.config.control_mode = CONTROL_MODE_POSITION_CONTROL axis_ctx.handle.controller.input_pos = 0 axis_ctx.handle.controller.config.vel_limit = float(enc_ctx.yaml['cpr']) * 5.0 # max 5 rps axis_ctx.handle.encoder.set_linear_count(0) @@ -181,7 +181,7 @@ class TestRegenProtection(TestClosedLoopControlBase): logger.debug(f'Brake control test from {nominal_rps} rounds/s...') axis_ctx.handle.controller.config.vel_limit = float(enc_ctx.yaml['cpr']) * 10.0 # max 10 rps - axis_ctx.handle.controller.config.control_mode = CTRL_MODE_VELOCITY_CONTROL + axis_ctx.handle.controller.config.control_mode = CONTROL_MODE_VELOCITY_CONTROL axis_ctx.handle.controller.config.input_mode = INPUT_MODE_PASSTHROUGH request_state(axis_ctx, AXIS_STATE_CLOSED_LOOP_CONTROL) @@ -231,7 +231,7 @@ class TestVelLimitInCurrentControl(TestClosedLoopControlBase): axis_ctx.handle.controller.config.vel_limit = max_vel axis_ctx.handle.controller.config.vel_limit_tolerance = inf # disable hard limit on velocity axis_ctx.handle.motor.config.current_lim = max_current - axis_ctx.handle.controller.config.control_mode = CTRL_MODE_CURRENT_CONTROL + axis_ctx.handle.controller.config.control_mode = CONTROL_MODE_CURRENT_CONTROL # Returns the expected limited setpoint for a given velocity and current def get_expected_setpoint(input_setpoint, velocity): diff --git a/tools/odrive/tests/endstop_test.py b/tools/odrive/tests/endstop_test.py index a823549c..fd07ae8d 100644 --- a/tools/odrive/tests/endstop_test.py +++ b/tools/odrive/tests/endstop_test.py @@ -7,7 +7,7 @@ odrv0 = odrive.find_any() print('Odrive found') odrv0.axis1.controller.config.vel_limit = 50000 -odrv0.axis1.controller.config.control_mode = CTRL_MODE_POSITION_CONTROL +odrv0.axis1.controller.config.control_mode = CONTROL_MODE_POSITION_CONTROL odrv0.axis1.controller.config.input_mode = INPUT_MODE_PASSTHROUGH odrv0.axis1.encoder.config.cpr = 2400 odrv0.axis1.encoder.config.bandwidth = 1000 diff --git a/tools/odrive/tests/uart_ascii_test.py b/tools/odrive/tests/uart_ascii_test.py index 91d61709..17cf85f8 100644 --- a/tools/odrive/tests/uart_ascii_test.py +++ b/tools/odrive/tests/uart_ascii_test.py @@ -96,7 +96,7 @@ class TestUartAscii(): ser.write(b'c 0 12.5\n') test_assert_eq(ser.readline(), b'') test_assert_eq(odrive.handle.axis0.controller.input_current, 12.5, accuracy=0.001) - test_assert_eq(odrive.handle.axis0.controller.config.control_mode, CTRL_MODE_CURRENT_CONTROL) + test_assert_eq(odrive.handle.axis0.controller.config.control_mode, CONTROL_MODE_CURRENT_CONTROL) odrive.handle.axis0.controller.input_vel = 0 odrive.handle.axis0.controller.input_current = 0 @@ -104,7 +104,7 @@ class TestUartAscii(): test_assert_eq(ser.readline(), b'') test_assert_eq(odrive.handle.axis0.controller.input_vel, 567.8, accuracy=0.001) test_assert_eq(odrive.handle.axis0.controller.input_current, 12.5, accuracy=0.001) - test_assert_eq(odrive.handle.axis0.controller.config.control_mode, CTRL_MODE_VELOCITY_CONTROL) + test_assert_eq(odrive.handle.axis0.controller.config.control_mode, CONTROL_MODE_VELOCITY_CONTROL) odrive.handle.axis0.controller.input_pos = 0 odrive.handle.axis0.controller.input_vel = 0 @@ -114,7 +114,7 @@ class TestUartAscii(): test_assert_eq(odrive.handle.axis0.controller.input_pos, 123.4, accuracy=0.001) test_assert_eq(odrive.handle.axis0.controller.input_vel, 567.8, accuracy=0.001) test_assert_eq(odrive.handle.axis0.controller.input_current, 12.5, accuracy=0.001) - test_assert_eq(odrive.handle.axis0.controller.config.control_mode, CTRL_MODE_POSITION_CONTROL) + test_assert_eq(odrive.handle.axis0.controller.config.control_mode, CONTROL_MODE_POSITION_CONTROL) odrive.handle.axis0.controller.input_pos = 0 odrive.handle.axis0.controller.config.vel_limit = 0 @@ -124,7 +124,7 @@ class TestUartAscii(): test_assert_eq(odrive.handle.axis0.controller.input_pos, 123.4, accuracy=0.001) test_assert_eq(odrive.handle.axis0.controller.config.vel_limit, 567.8, accuracy=0.001) test_assert_eq(odrive.handle.axis0.motor.config.current_lim, 12.5, accuracy=0.001) - test_assert_eq(odrive.handle.axis0.controller.config.control_mode, CTRL_MODE_POSITION_CONTROL) + test_assert_eq(odrive.handle.axis0.controller.config.control_mode, CONTROL_MODE_POSITION_CONTROL) ser.write(b'f 0\n') response = ser.readline().strip() diff --git a/tools/setup_hall_as_index.py b/tools/setup_hall_as_index.py index c01de4ff..3c21f2cb 100644 --- a/tools/setup_hall_as_index.py +++ b/tools/setup_hall_as_index.py @@ -30,7 +30,7 @@ for ax in axes: ax.encoder.config.find_idx_on_lockin_only = True ax.encoder.config.idx_search_unidirectional = True - ax.controller.config.control_mode = CTRL_MODE_VELOCITY_CONTROL + ax.controller.config.control_mode = CONTROL_MODE_VELOCITY_CONTROL ax.controller.config.vel_limit = 10000 ax.controller.config.vel_gain = 0.002205736003816127 ax.controller.config.vel_integrator_gain = 0.022057360038161278 From e4d70a22b70cf4270b97ef89673087284a1a023d Mon Sep 17 00:00:00 2001 From: Samuel Sadok Date: Thu, 14 May 2020 11:31:30 +0200 Subject: [PATCH 02/28] Initial implementation of interface autogenerator. TODO: - write to endpoints from float (for PWM/analog input) - ascii protocol --- Firmware/.gitignore | 1 + Firmware/MotorControl/axis.cpp | 4 +- Firmware/MotorControl/axis.hpp | 127 +- Firmware/MotorControl/controller.hpp | 90 +- Firmware/MotorControl/encoder.hpp | 86 +- Firmware/MotorControl/endstop.hpp | 21 +- Firmware/MotorControl/low_level.cpp | 32 +- Firmware/MotorControl/main.cpp | 81 +- Firmware/MotorControl/motor.cpp | 2 +- Firmware/MotorControl/motor.hpp | 147 +-- Firmware/MotorControl/odrive_main.h | 91 +- .../MotorControl/sensorless_estimator.hpp | 26 +- Firmware/MotorControl/trapTraj.hpp | 10 - Firmware/Tupfile.lua | 16 +- Firmware/ascii_type_info_template.j2 | 90 ++ Firmware/build.lua | 2 +- Firmware/communication/ascii_protocol.cpp | 17 +- Firmware/communication/can_simple.cpp | 2 +- Firmware/communication/communication.cpp | 161 +-- Firmware/communication/communication.h | 4 - Firmware/communication/interface_can.cpp | 10 +- Firmware/communication/interface_can.hpp | 35 +- Firmware/communication/interface_usb.cpp | 2 +- Firmware/fibre/cpp/endpoints_template.j2 | 71 + Firmware/fibre/cpp/function_stubs_template.j2 | 40 + Firmware/fibre/cpp/include/fibre/bufptr.hpp | 93 ++ .../fibre/cpp/include/fibre/cpp_utils.hpp | 1145 ++++++++++++++++- Firmware/fibre/cpp/include/fibre/protocol.hpp | 748 ++--------- .../fibre/cpp/include/fibre/simple_serdes.hpp | 77 ++ Firmware/fibre/cpp/interfaces_template.j2 | 67 + Firmware/fibre/cpp/protocol.cpp | 91 +- Firmware/interface_generator.py | 589 +++++++++ Firmware/odrive-interface.yaml | 662 ++++++++++ docs/interface-definition-file.md | 133 ++ 34 files changed, 3321 insertions(+), 1452 deletions(-) create mode 100644 Firmware/ascii_type_info_template.j2 create mode 100644 Firmware/fibre/cpp/endpoints_template.j2 create mode 100644 Firmware/fibre/cpp/function_stubs_template.j2 create mode 100644 Firmware/fibre/cpp/include/fibre/bufptr.hpp create mode 100644 Firmware/fibre/cpp/include/fibre/simple_serdes.hpp create mode 100644 Firmware/fibre/cpp/interfaces_template.j2 create mode 100644 Firmware/interface_generator.py create mode 100644 Firmware/odrive-interface.yaml create mode 100644 docs/interface-definition-file.md diff --git a/Firmware/.gitignore b/Firmware/.gitignore index 496462db..a4c86dc2 100644 --- a/Firmware/.gitignore +++ b/Firmware/.gitignore @@ -1,5 +1,6 @@ #build folder +autogen/ build/ deploy/ .dep/ diff --git a/Firmware/MotorControl/axis.cpp b/Firmware/MotorControl/axis.cpp index c1d11a4b..71edd5f1 100644 --- a/Firmware/MotorControl/axis.cpp +++ b/Firmware/MotorControl/axis.cpp @@ -161,9 +161,9 @@ bool Axis::do_checks() { if ((current_state_ != AXIS_STATE_IDLE) && (motor_.armed_state_ == Motor::ARMED_STATE_DISARMED)) // motor got disarmed in something other than the idle loop error_ |= ERROR_MOTOR_DISARMED; - if (!(vbus_voltage >= board_config.dc_bus_undervoltage_trip_level)) + if (!(vbus_voltage >= odrv.config_.dc_bus_undervoltage_trip_level)) error_ |= ERROR_DC_BUS_UNDER_VOLTAGE; - if (!(vbus_voltage <= board_config.dc_bus_overvoltage_trip_level)) + if (!(vbus_voltage <= odrv.config_.dc_bus_overvoltage_trip_level)) error_ |= ERROR_DC_BUS_OVER_VOLTAGE; // Sub-components should use set_error which will propegate to this error_ diff --git a/Firmware/MotorControl/axis.hpp b/Firmware/MotorControl/axis.hpp index 63925753..31cc118e 100644 --- a/Firmware/MotorControl/axis.hpp +++ b/Firmware/MotorControl/axis.hpp @@ -5,43 +5,8 @@ #error "This file should not be included directly. Include odrive_main.h instead." #endif -class Axis { +class Axis : public AxisIntf { public: - enum Error { - ERROR_NONE = 0x00, - ERROR_INVALID_STATE = 0x01, //decode_step_dir_pins(); } + void set_dir_gpio_pin(uint16_t value) { dir_gpio_pin = value; parent->decode_step_dir_pins(); } }; struct Homing_t { @@ -98,13 +68,6 @@ public: M_SIGNAL_PH_CURRENT_MEAS = 1u << 0 }; - enum LockinState_t { - LOCKIN_STATE_INACTIVE, - LOCKIN_STATE_RAMP, - LOCKIN_STATE_ACCELERATE, - LOCKIN_STATE_CONST_VEL, - }; - Axis(int axis_num, const AxisHardwareConfig_t& hw_config, Config_t& config, @@ -143,7 +106,7 @@ public: sensorless_estimator_.error_ = SensorlessEstimator::ERROR_NONE; encoder_.error_ = Encoder::ERROR_NONE; - error_ = Axis::ERROR_NONE; + error_ = ERROR_NONE; } // True if there are no errors @@ -250,85 +213,17 @@ public: GPIO_TypeDef* dir_port_; uint16_t dir_pin_; - State_t requested_state_ = AXIS_STATE_STARTUP_SEQUENCE; - std::array task_chain_ = { AXIS_STATE_UNDEFINED }; - State_t& current_state_ = task_chain_.front(); + AxisState requested_state_ = AXIS_STATE_STARTUP_SEQUENCE; + std::array task_chain_ = { AXIS_STATE_UNDEFINED }; + AxisState& current_state_ = task_chain_.front(); uint32_t loop_counter_ = 0; - LockinState_t lockin_state_ = LOCKIN_STATE_INACTIVE; + LockinState lockin_state_ = LOCKIN_STATE_INACTIVE; Homing_t homing_; uint32_t last_heartbeat_ = 0; // watchdog uint32_t watchdog_current_value_= 0; - - // Communication protocol definitions - auto make_protocol_definitions() { - return make_protocol_member_list( - make_protocol_property("error", &error_), - make_protocol_ro_property("step_dir_active", &step_dir_active_), - make_protocol_ro_property("current_state", ¤t_state_), - make_protocol_property("requested_state", &requested_state_), - make_protocol_ro_property("loop_counter", &loop_counter_), - make_protocol_ro_property("lockin_state", &lockin_state_), - make_protocol_property("is_homed", &homing_.is_homed), - make_protocol_object("config", - make_protocol_property("startup_motor_calibration", &config_.startup_motor_calibration), - make_protocol_property("startup_encoder_index_search", &config_.startup_encoder_index_search), - make_protocol_property("startup_encoder_offset_calibration", &config_.startup_encoder_offset_calibration), - make_protocol_property("startup_closed_loop_control", &config_.startup_closed_loop_control), - make_protocol_property("startup_sensorless_control", &config_.startup_sensorless_control), - make_protocol_property("startup_homing", &config_.startup_homing), - make_protocol_property("enable_step_dir", &config_.enable_step_dir), - make_protocol_property("step_dir_always_on", &config_.step_dir_always_on), - make_protocol_property("counts_per_step", &config_.counts_per_step), - make_protocol_property("watchdog_timeout", &config_.watchdog_timeout), - make_protocol_property("enable_watchdog", &config_.enable_watchdog), - make_protocol_property("step_gpio_pin", &config_.step_gpio_pin, - [](void* ctx) { static_cast(ctx)->decode_step_dir_pins(); }, this), - make_protocol_property("dir_gpio_pin", &config_.dir_gpio_pin, - [](void* ctx) { static_cast(ctx)->decode_step_dir_pins(); }, this), - make_protocol_object("calibration_lockin", - make_protocol_property("current", &config_.calibration_lockin.current), - make_protocol_property("ramp_time", &config_.calibration_lockin.ramp_time), - make_protocol_property("ramp_distance", &config_.calibration_lockin.ramp_distance), - make_protocol_property("accel", &config_.calibration_lockin.accel), - make_protocol_property("vel", &config_.calibration_lockin.vel)), - make_protocol_object("sensorless_ramp", - make_protocol_property("current", &config_.sensorless_ramp.current), - make_protocol_property("ramp_time", &config_.sensorless_ramp.ramp_time), - make_protocol_property("ramp_distance", &config_.sensorless_ramp.ramp_distance), - make_protocol_property("accel", &config_.sensorless_ramp.accel), - make_protocol_property("vel", &config_.sensorless_ramp.vel), - make_protocol_property("finish_distance", &config_.sensorless_ramp.finish_distance), - make_protocol_property("finish_on_vel", &config_.sensorless_ramp.finish_on_vel), - make_protocol_property("finish_on_distance", &config_.sensorless_ramp.finish_on_distance), - make_protocol_property("finish_on_enc_idx", &config_.sensorless_ramp.finish_on_enc_idx)), - make_protocol_object("general_lockin", - make_protocol_property("current", &config_.general_lockin.current), - make_protocol_property("ramp_time", &config_.general_lockin.ramp_time), - make_protocol_property("ramp_distance", &config_.general_lockin.ramp_distance), - make_protocol_property("accel", &config_.general_lockin.accel), - make_protocol_property("vel", &config_.general_lockin.vel), - make_protocol_property("finish_distance", &config_.general_lockin.finish_distance), - make_protocol_property("finish_on_vel", &config_.general_lockin.finish_on_vel), - make_protocol_property("finish_on_distance", &config_.general_lockin.finish_on_distance), - make_protocol_property("finish_on_enc_idx", &config_.general_lockin.finish_on_enc_idx)), - make_protocol_property("can_node_id", &config_.can_node_id), - make_protocol_property("can_heartbeat_rate_ms", &config_.can_heartbeat_rate_ms)), - make_protocol_object("motor", motor_.make_protocol_definitions()), - make_protocol_object("controller", controller_.make_protocol_definitions()), - make_protocol_object("encoder", encoder_.make_protocol_definitions()), - make_protocol_object("sensorless_estimator", sensorless_estimator_.make_protocol_definitions()), - make_protocol_object("trap_traj", trap_traj_.make_protocol_definitions()), - make_protocol_object("min_endstop", min_endstop_.make_protocol_definitions()), - make_protocol_object("max_endstop", max_endstop_.make_protocol_definitions()), - make_protocol_function("watchdog_feed", *this, &Axis::watchdog_feed), - make_protocol_function("clear_errors", *this, &Axis::clear_errors) - ); - } }; -DEFINE_ENUM_FLAG_OPERATORS(Axis::Error) - #endif /* __AXIS_HPP */ diff --git a/Firmware/MotorControl/controller.hpp b/Firmware/MotorControl/controller.hpp index 293b892a..f1529d96 100644 --- a/Firmware/MotorControl/controller.hpp +++ b/Firmware/MotorControl/controller.hpp @@ -5,38 +5,8 @@ #error "This file should not be included directly. Include odrive_main.h instead." #endif -class Controller { +class Controller : public ControllerIntf { public: - enum Error { - ERROR_NONE = 0, - ERROR_OVERSPEED = 0x01, - ERROR_INVALID_INPUT_MODE = 0x02, - ERROR_UNSTABLE_GAIN = 0x04, - ERROR_INVALID_MIRROR_AXIS = 0x08, - ERROR_INVALID_LOAD_ENCODER = 0x10, - ERROR_INVALID_ESTIMATE = 0x20, - }; - - // Note: these should be sorted from lowest level of control to - // highest level of control, to allow "<" style comparisons. - enum ControlMode{ - CONTROL_MODE_VOLTAGE_CONTROL = 0, - CONTROL_MODE_CURRENT_CONTROL = 1, - CONTROL_MODE_VELOCITY_CONTROL = 2, - CONTROL_MODE_POSITION_CONTROL = 3 - }; - - enum InputMode{ - INPUT_MODE_INACTIVE, - INPUT_MODE_PASSTHROUGH, - INPUT_MODE_VEL_RAMP, - INPUT_MODE_POS_FILTER, - INPUT_MODE_MIX_CHANNELS, - INPUT_MODE_TRAP_TRAJ, - INPUT_MODE_CURRENT_RAMP, - INPUT_MODE_MIRROR, - }; - typedef struct { uint32_t index = 0; float cogging_map[3600]; @@ -48,7 +18,7 @@ public: bool anticogging_enabled = true; } Anticogging_t; - struct Config_t { + struct Config_t : ConfigIntf { ControlMode control_mode = CONTROL_MODE_POSITION_CONTROL; //see: ControlMode InputMode input_mode = INPUT_MODE_PASSTHROUGH; //see: InputMode float pos_gain = 20.0f; // [(counts/s) / counts] @@ -72,6 +42,10 @@ public: uint8_t axis_to_mirror = -1; float mirror_ratio = 1.0f; uint8_t load_encoder_axis = -1; // default depends on Axis number and is set in load_configuration() + + // custom setters + Controller* parent; + void set_input_filter_bandwidth(float value) { input_filter_bandwidth = value; parent->update_filter_gains(); } }; explicit Controller(Config_t& config); @@ -121,56 +95,8 @@ public: bool anticogging_valid_ = false; - // Communication protocol definitions - auto make_protocol_definitions() { - return make_protocol_member_list( - make_protocol_property("error", &error_), - make_protocol_property("input_pos", &input_pos_, - [](void* ctx) { static_cast(ctx)->input_pos_updated(); }, this), - make_protocol_property("input_vel", &input_vel_), - make_protocol_property("input_current", &input_current_), - make_protocol_ro_property("pos_setpoint", &pos_setpoint_), - make_protocol_ro_property("vel_setpoint", &vel_setpoint_), - make_protocol_ro_property("current_setpoint", ¤t_setpoint_), - make_protocol_ro_property("trajectory_done", &trajectory_done_), - make_protocol_property("vel_integrator_current", &vel_integrator_current_), - make_protocol_property("anticogging_valid", &anticogging_valid_), - make_protocol_property("gain_scheduling_width", &config_.gain_scheduling_width), - make_protocol_object("config", - make_protocol_property("enable_vel_limit", &config_.enable_vel_limit), - make_protocol_property("enable_current_mode_vel_limit", &config_.enable_current_mode_vel_limit), - make_protocol_property("enable_gain_scheduling", &config_.enable_gain_scheduling), - make_protocol_property("enable_overspeed_error", &config_.enable_overspeed_error), - make_protocol_property("control_mode", &config_.control_mode), - make_protocol_property("input_mode", &config_.input_mode), - make_protocol_property("pos_gain", &config_.pos_gain), - make_protocol_property("vel_gain", &config_.vel_gain), - make_protocol_property("vel_integrator_gain", &config_.vel_integrator_gain), - make_protocol_property("vel_limit", &config_.vel_limit), - make_protocol_property("vel_limit_tolerance", &config_.vel_limit_tolerance), - make_protocol_property("vel_ramp_rate", &config_.vel_ramp_rate), - make_protocol_property("current_ramp_rate", &config_.current_ramp_rate), - make_protocol_property("homing_speed", &config_.homing_speed), - make_protocol_property("inertia", &config_.inertia), - make_protocol_property("axis_to_mirror", &config_.axis_to_mirror), - make_protocol_property("mirror_ratio", &config_.mirror_ratio), - make_protocol_property("load_encoder_axis", &config_.load_encoder_axis), - make_protocol_property("input_filter_bandwidth", &config_.input_filter_bandwidth, - [](void* ctx) { static_cast(ctx)->update_filter_gains(); }, this), - make_protocol_object("anticogging", - make_protocol_ro_property("index", &config_.anticogging.index), - make_protocol_property("pre_calibrated", &config_.anticogging.pre_calibrated), - make_protocol_ro_property("calib_anticogging", &config_.anticogging.calib_anticogging), - make_protocol_property("calib_pos_threshold", &config_.anticogging.calib_pos_threshold), - make_protocol_property("calib_vel_threshold", &config_.anticogging.calib_vel_threshold), - make_protocol_ro_property("cogging_ratio", &config_.anticogging.cogging_ratio), - make_protocol_property("anticogging_enabled", &config_.anticogging.anticogging_enabled))), - make_protocol_function("move_incremental", *this, &Controller::move_incremental, "displacement", "from_goal_point"), - make_protocol_function("start_anticogging_calibration", *this, &Controller::start_anticogging_calibration) - ); - } + // custom setters + void set_input_pos(float value) { input_pos_ = value; input_pos_updated(); } }; -DEFINE_ENUM_FLAG_OPERATORS(Controller::Error) - #endif // __CONTROLLER_HPP diff --git a/Firmware/MotorControl/encoder.hpp b/Firmware/MotorControl/encoder.hpp index 77ad84b1..04471e62 100644 --- a/Firmware/MotorControl/encoder.hpp +++ b/Firmware/MotorControl/encoder.hpp @@ -5,33 +5,12 @@ #error "This file should not be included directly. Include odrive_main.h instead." #endif -class Encoder { +class Encoder : public EncoderIntf { public: - enum Error { - ERROR_NONE = 0, - ERROR_UNSTABLE_GAIN = 0x01, - ERROR_CPR_POLEPAIRS_MISMATCH = 0x02, - ERROR_NO_RESPONSE = 0x04, - ERROR_UNSUPPORTED_ENCODER_MODE = 0x08, - ERROR_ILLEGAL_HALL_STATE = 0x10, - ERROR_INDEX_NOT_FOUND_YET = 0x20, - ERROR_ABS_SPI_TIMEOUT = 0x40, - ERROR_ABS_SPI_COM_FAIL = 0x80, - ERROR_ABS_SPI_NOT_READY = 0x100, - }; - - enum Mode_t { - MODE_INCREMENTAL, - MODE_HALL, - MODE_SINCOS, - MODE_SPI_ABS_CUI = 0x100, //!< compatible with CUI AMT23xx - MODE_SPI_ABS_AMS = 0x101, //!< compatible with AMS AS5047P, AS5048A/AS5048B (no daisy chain support) - MODE_SPI_ABS_AEAT = 0x102, //!< not yet implemented - }; const uint32_t MODE_FLAG_ABS = 0x100; - struct Config_t { - Encoder::Mode_t mode = Encoder::MODE_INCREMENTAL; + struct Config_t : EncoderIntf::ConfigIntf { + Mode mode = MODE_INCREMENTAL; bool use_index = false; bool pre_calibrated = false; // If true, this means the offset stored in // configuration is valid and does not need @@ -53,6 +32,14 @@ public: uint16_t abs_spi_cs_gpio_pin = 1; uint16_t sincos_gpio_pin_sin = 3; uint16_t sincos_gpio_pin_cos = 4; + + // custom setters + Encoder* parent = nullptr; + void set_use_index(bool value) { use_index = value; parent->set_idx_subscribe(); } + void set_find_idx_on_lockin_only(bool value) { find_idx_on_lockin_only = value; parent->set_idx_subscribe(); } + void set_abs_spi_cs_gpio_pin(uint16_t value) { abs_spi_cs_gpio_pin = value; parent->abs_spi_cs_pin_init(); } + void set_pre_calibrated(bool value) { pre_calibrated = value; parent->check_pre_calibrated(); } + void set_bandwidth(float value) { bandwidth = value; parent->update_pll_gains(); } }; Encoder(const EncoderHardwareConfig_t& hw_config, @@ -113,7 +100,7 @@ public: uint16_t abs_spi_dma_tx_[1] = {0xFFFF}; uint16_t abs_spi_dma_rx_[1]; bool abs_spi_pos_updated_ = false; - Mode_t mode_ = MODE_INCREMENTAL; + Mode mode_ = MODE_INCREMENTAL; GPIO_TypeDef* abs_spi_cs_port_; uint16_t abs_spi_cs_pin_; uint32_t abs_spi_cr1; @@ -122,55 +109,6 @@ public: constexpr float getCoggingRatio(){ return config_.cpr / 3600.0f; } - - // Communication protocol definitions - auto make_protocol_definitions() { - return make_protocol_member_list( - make_protocol_property("error", &error_), - make_protocol_ro_property("is_ready", &is_ready_), - make_protocol_ro_property("index_found", const_cast(&index_found_)), - make_protocol_ro_property("shadow_count", &shadow_count_), - make_protocol_ro_property("count_in_cpr", &count_in_cpr_), - make_protocol_ro_property("interpolation", &interpolation_), - make_protocol_ro_property("phase", &phase_), - make_protocol_ro_property("pos_estimate", &pos_estimate_), - make_protocol_ro_property("pos_cpr", &pos_cpr_), - make_protocol_ro_property("hall_state", &hall_state_), - make_protocol_ro_property("vel_estimate", &vel_estimate_), - make_protocol_ro_property("calib_scan_response", &calib_scan_response_), - make_protocol_property("pos_abs", &pos_abs_), - make_protocol_ro_property("spi_error_rate", &spi_error_rate_), - - make_protocol_object("config", - make_protocol_property("mode", &config_.mode), - make_protocol_property("use_index", &config_.use_index, - [](void* ctx) { static_cast(ctx)->set_idx_subscribe(); }, this), - make_protocol_property("find_idx_on_lockin_only", &config_.find_idx_on_lockin_only, - [](void* ctx) { static_cast(ctx)->set_idx_subscribe(); }, this), - make_protocol_property("abs_spi_cs_gpio_pin", &config_.abs_spi_cs_gpio_pin, - [](void* ctx) { static_cast(ctx)->abs_spi_cs_pin_init(); }, this), - make_protocol_property("zero_count_on_find_idx", &config_.zero_count_on_find_idx), - make_protocol_property("cpr", &config_.cpr), - make_protocol_property("offset", &config_.offset), - make_protocol_property("pre_calibrated", &config_.pre_calibrated, - [](void* ctx) { static_cast(ctx)->check_pre_calibrated(); }, this), - make_protocol_property("offset_float", &config_.offset_float), - make_protocol_property("enable_phase_interpolation", &config_.enable_phase_interpolation), - make_protocol_property("bandwidth", &config_.bandwidth, - [](void* ctx) { static_cast(ctx)->update_pll_gains(); }, this), - make_protocol_property("calib_range", &config_.calib_range), - make_protocol_property("calib_scan_distance", &config_.calib_scan_distance), - make_protocol_property("calib_scan_omega", &config_.calib_scan_omega), - make_protocol_property("idx_search_unidirectional", &config_.idx_search_unidirectional), - make_protocol_property("ignore_illegal_hall_state", &config_.ignore_illegal_hall_state), - make_protocol_property("sincos_gpio_pin_sin", &config_.sincos_gpio_pin_sin), - make_protocol_property("sincos_gpio_pin_cos", &config_.sincos_gpio_pin_cos) - ), - make_protocol_function("set_linear_count", *this, &Encoder::set_linear_count, "count") - ); - } }; -DEFINE_ENUM_FLAG_OPERATORS(Encoder::Error) - #endif // __ENCODER_HPP diff --git a/Firmware/MotorControl/endstop.hpp b/Firmware/MotorControl/endstop.hpp index 6983fa6c..e9412f89 100644 --- a/Firmware/MotorControl/endstop.hpp +++ b/Firmware/MotorControl/endstop.hpp @@ -11,6 +11,12 @@ class Endstop { bool enabled = false; bool is_active_high = false; bool pullup = true; + + // custom setters + Endstop* parent = nullptr + void set_gpio_num(uint16_t value) { gpio_num = value; parent->update_config(); } + void set_enabled(uint32_t value) { enabled = value; parent->update_config(); } + void set_debounce_ms(uint32_t value) { debounce_ms = value; parent->update_config(); } }; explicit Endstop(Endstop::Config_t& config); @@ -26,21 +32,6 @@ class Endstop { bool endstop_state_ = false; - auto make_protocol_definitions() { - return make_protocol_member_list( - make_protocol_ro_property("endstop_state", &endstop_state_), - make_protocol_object("config", - make_protocol_property("gpio_num", &config_.gpio_num, - [](void* ctx) { static_cast(ctx)->update_config(); }, this), - make_protocol_property("enabled", &config_.enabled, - [](void* ctx) { static_cast(ctx)->update_config(); }, this), - make_protocol_property("offset", &config_.offset), - make_protocol_property("is_active_high", &config_.is_active_high), - make_protocol_property("pullup", &config_.pullup), - make_protocol_property("debounce_ms", &config_.debounce_ms, - [](void* ctx) { static_cast(ctx)->update_config(); }, this))); - } - private: bool pin_state_ = false; float pos_when_pressed_ = 0.0f; diff --git a/Firmware/MotorControl/low_level.cpp b/Firmware/MotorControl/low_level.cpp index f0e32f0c..5c2886cf 100644 --- a/Firmware/MotorControl/low_level.cpp +++ b/Firmware/MotorControl/low_level.cpp @@ -613,11 +613,11 @@ void update_brake_current() { } // Don't start braking until -Ibus > regen_current_allowed - float brake_current = -Ibus_sum - board_config.max_regen_current; - float brake_duty = brake_current * board_config.brake_resistance / vbus_voltage; + float brake_current = -Ibus_sum - odrv.config_.max_regen_current; + float brake_duty = brake_current * odrv.config_.brake_resistance / vbus_voltage; - if (board_config.enable_dc_bus_overvoltage_ramp && (board_config.brake_resistance > 0.0f) && (board_config.dc_bus_overvoltage_ramp_start < board_config.dc_bus_overvoltage_ramp_end)) { - brake_duty += std::fmax((vbus_voltage - board_config.dc_bus_overvoltage_ramp_start) / (board_config.dc_bus_overvoltage_ramp_end - board_config.dc_bus_overvoltage_ramp_start), 0.0f); + if (odrv.config_.enable_dc_bus_overvoltage_ramp && (odrv.config_.brake_resistance > 0.0f) && (odrv.config_.dc_bus_overvoltage_ramp_start < odrv.config_.dc_bus_overvoltage_ramp_end)) { + brake_duty += std::fmax((vbus_voltage - odrv.config_.dc_bus_overvoltage_ramp_start) / (odrv.config_.dc_bus_overvoltage_ramp_end - odrv.config_.dc_bus_overvoltage_ramp_start), 0.0f); } if (std::isnan(brake_duty)) { @@ -634,15 +634,15 @@ void update_brake_current() { brake_duty = std::clamp(brake_duty, 0.0f, 0.95f); // Special handling to avoid the case 0.0/0.0 == NaN. - Ibus_sum += brake_duty ? (brake_duty * vbus_voltage / board_config.brake_resistance) : 0.0f; + Ibus_sum += brake_duty ? (brake_duty * vbus_voltage / odrv.config_.brake_resistance) : 0.0f; ibus_ = Ibus_sum; - if (Ibus_sum > board_config.dc_max_positive_current) { + if (Ibus_sum > odrv.config_.dc_max_positive_current) { low_level_fault(Motor::ERROR_DC_BUS_OVER_CURRENT); return; } - if (Ibus_sum < board_config.dc_max_negative_current) { + if (Ibus_sum < odrv.config_.dc_max_negative_current) { low_level_fault(Motor::ERROR_DC_BUS_OVER_REGEN_CURRENT); return; } @@ -715,7 +715,7 @@ void pwm_in_init() { #else int gpio_num = 4; { #endif - if (is_endpoint_ref_valid(board_config.pwm_mappings[gpio_num - 1].endpoint)) { + if (fibre::is_endpoint_ref_valid(odrv.config_.pwm_mappings[gpio_num - 1].endpoint)) { GPIO_InitStruct.Pin = get_gpio_pin_by_pin(gpio_num); HAL_GPIO_DeInit(get_gpio_port_by_pin(gpio_num), get_gpio_pin_by_pin(gpio_num)); HAL_GPIO_Init(get_gpio_port_by_pin(gpio_num), &GPIO_InitStruct); @@ -742,14 +742,10 @@ void handle_pulse(int gpio_num, uint32_t high_time) { if (high_time > PWM_MAX_HIGH_TIME) high_time = PWM_MAX_HIGH_TIME; float fraction = (float)(high_time - PWM_MIN_HIGH_TIME) / (float)(PWM_MAX_HIGH_TIME - PWM_MIN_HIGH_TIME); - float value = board_config.pwm_mappings[gpio_num - 1].min + - (fraction * (board_config.pwm_mappings[gpio_num - 1].max - board_config.pwm_mappings[gpio_num - 1].min)); + float value = odrv.config_.pwm_mappings[gpio_num - 1].min + + (fraction * (odrv.config_.pwm_mappings[gpio_num - 1].max - odrv.config_.pwm_mappings[gpio_num - 1].min)); - Endpoint* endpoint = get_endpoint(board_config.pwm_mappings[gpio_num - 1].endpoint); - if (!endpoint) - return; - - endpoint->set_from_float(value); + fibre::set_endpoint_from_float(odrv.config_.pwm_mappings[gpio_num - 1].endpoint, value); } void pwm_in_cb(int channel, uint32_t timestamp) { @@ -780,16 +776,16 @@ static void update_analog_endpoint(const struct PWMMapping_t *map, int gpio) { float fraction = get_adc_voltage(get_gpio_port_by_pin(gpio), get_gpio_pin_by_pin(gpio)) / 3.3f; float value = map->min + (fraction * (map->max - map->min)); - get_endpoint(map->endpoint)->set_from_float(value); + fibre::set_endpoint_from_float(map->endpoint, value); } static void analog_polling_thread(void *) { while (true) { for (int i = 0; i < GPIO_COUNT; i++) { - struct PWMMapping_t *map = &board_config.analog_mappings[i]; + struct PWMMapping_t *map = &odrv.config_.analog_mappings[i]; - if (is_endpoint_ref_valid(map->endpoint)) + if (fibre::is_endpoint_ref_valid(map->endpoint)) update_analog_endpoint(map, i + 1); } osDelay(10); diff --git a/Firmware/MotorControl/main.cpp b/Firmware/MotorControl/main.cpp index d3864ed0..5ea0a827 100644 --- a/Firmware/MotorControl/main.cpp +++ b/Firmware/MotorControl/main.cpp @@ -10,7 +10,6 @@ #include #include -BoardConfig_t board_config; ODriveCAN::Config_t can_config; Encoder::Config_t encoder_configs[AXIS_COUNT]; SensorlessEstimator::Config_t sensorless_configs[AXIS_COUNT]; @@ -20,12 +19,10 @@ Axis::Config_t axis_configs[AXIS_COUNT]; TrapezoidalTrajectory::Config_t trap_configs[AXIS_COUNT]; Endstop::Config_t min_endstop_configs[AXIS_COUNT]; Endstop::Config_t max_endstop_configs[AXIS_COUNT]; -bool user_config_loaded_; - -SystemStats_t system_stats_; std::array axes; ODriveCAN *odCAN = nullptr; +ODrive odrv{}; typedef Config< BoardConfig_t, @@ -39,9 +36,9 @@ typedef Config< Endstop::Config_t[AXIS_COUNT], Axis::Config_t[AXIS_COUNT]> ConfigFormat; -void save_configuration(void) { +void ODrive::save_configuration(void) { if (ConfigFormat::safe_store_config( - &board_config, + &odrv.config_, &can_config, &encoder_configs, &sensorless_configs, @@ -53,7 +50,7 @@ void save_configuration(void) { &axis_configs)) { printf("saving configuration failed\r\n"); osDelay(5); } else { - user_config_loaded_ = true; + odrv.user_config_loaded_ = true; } } @@ -61,7 +58,7 @@ extern "C" int load_configuration(void) { // Try to load configs if (NVM_init() || ConfigFormat::safe_load_config( - &board_config, + &odrv.config_, &can_config, &encoder_configs, &sensorless_configs, @@ -72,7 +69,7 @@ extern "C" int load_configuration(void) { &max_endstop_configs, &axis_configs)) { //If loading failed, restore defaults - board_config = BoardConfig_t(); + odrv.config_ = BoardConfig_t(); can_config = ODriveCAN::Config_t(); for (size_t i = 0; i < AXIS_COUNT; ++i) { encoder_configs[i] = Encoder::Config_t(); @@ -89,12 +86,12 @@ extern "C" int load_configuration(void) { controller_configs[i].load_encoder_axis = i; } } else { - user_config_loaded_ = true; + odrv.user_config_loaded_ = true; } - return user_config_loaded_; + return odrv.user_config_loaded_; } -void erase_configuration(void) { +void ODrive::erase_configuration(void) { NVM_erase(); // FIXME: this reboot is a workaround because we don't want the next save_configuration @@ -105,8 +102,8 @@ void erase_configuration(void) { NVIC_SystemReset(); } -void enter_dfu_mode() { - if ((hw_version_major == 3) && (hw_version_minor >= 5)) { +void ODrive::enter_dfu_mode() { + if ((hw_version_major_ == 3) && (hw_version_minor_ >= 5)) { __asm volatile ("CPSID I\n\t":::"memory"); // disable interrupts _reboot_cookie = 0xDEADBEEF; NVIC_SystemReset(); @@ -125,7 +122,7 @@ void enter_dfu_mode() { extern "C" int construct_objects(){ #if HW_VERSION_MAJOR == 3 && HW_VERSION_MINOR >= 3 - if (board_config.enable_i2c_instead_of_can) { + if (odrv.config_.enable_i2c_instead_of_can) { // Set up the direction GPIO as input GPIO_InitTypeDef GPIO_InitStruct; GPIO_InitStruct.Mode = GPIO_MODE_INPUT; @@ -149,7 +146,7 @@ extern "C" int construct_objects(){ MX_CAN1_Init(); HAL_UART_DeInit(&huart4); - huart4.Init.BaudRate = board_config.uart_baudrate; + huart4.Init.BaudRate = odrv.config_.uart_baudrate; HAL_UART_Init(&huart4); // Init general user ADC on some GPIOs. @@ -170,7 +167,7 @@ extern "C" int construct_objects(){ #endif // Construct all objects. - odCAN = new ODriveCAN(&hcan1, can_config); + odCAN = new ODriveCAN(can_config, &hcan1); for (size_t i = 0; i < AXIS_COUNT; ++i) { Encoder *encoder = new Encoder(hw_configs[i].encoder_config, encoder_configs[i], motor_configs[i]); @@ -184,8 +181,14 @@ extern "C" int construct_objects(){ Endstop *max_endstop = new Endstop(max_endstop_configs[i]); axes[i] = new Axis(i, hw_configs[i].axis_config, axis_configs[i], *encoder, *sensorless_estimator, *controller, *motor, *trap, *min_endstop, *max_endstop); + + controller_configs[i].parent = controller; + encoder_configs[i].parent = encoder; + motor_configs[i].parent = motor; + min_endstop_configs[i].parent = min_endstop; + max_endstop_configs[i].parent = max_endstop; + axis_configs[i].parent = axes[i]; } - initTree(); return 0; } @@ -199,27 +202,27 @@ void vApplicationStackOverflowHook(xTaskHandle *pxTask, signed portCHAR *pcTaskN for (;;); // TODO: safe action } void vApplicationIdleHook(void) { - if (system_stats_.fully_booted) { - system_stats_.uptime = xTaskGetTickCount(); - system_stats_.min_heap_space = xPortGetMinimumEverFreeHeapSize(); - system_stats_.min_stack_space_comms = uxTaskGetStackHighWaterMark(comm_thread) * sizeof(StackType_t); - system_stats_.min_stack_space_axis0 = uxTaskGetStackHighWaterMark(axes[0]->thread_id_) * sizeof(StackType_t); - system_stats_.min_stack_space_axis1 = uxTaskGetStackHighWaterMark(axes[1]->thread_id_) * sizeof(StackType_t); - system_stats_.min_stack_space_usb = uxTaskGetStackHighWaterMark(usb_thread) * sizeof(StackType_t); - system_stats_.min_stack_space_uart = uxTaskGetStackHighWaterMark(uart_thread) * sizeof(StackType_t); - system_stats_.min_stack_space_usb_irq = uxTaskGetStackHighWaterMark(usb_irq_thread) * sizeof(StackType_t); - system_stats_.min_stack_space_startup = uxTaskGetStackHighWaterMark(defaultTaskHandle) * sizeof(StackType_t); - system_stats_.min_stack_space_can = uxTaskGetStackHighWaterMark(odCAN->thread_id_) * sizeof(StackType_t); + if (odrv.system_stats_.fully_booted) { + odrv.system_stats_.uptime = xTaskGetTickCount(); + odrv.system_stats_.min_heap_space = xPortGetMinimumEverFreeHeapSize(); + odrv.system_stats_.min_stack_space_comms = uxTaskGetStackHighWaterMark(comm_thread) * sizeof(StackType_t); + odrv.system_stats_.min_stack_space_axis0 = uxTaskGetStackHighWaterMark(axes[0]->thread_id_) * sizeof(StackType_t); + odrv.system_stats_.min_stack_space_axis1 = uxTaskGetStackHighWaterMark(axes[1]->thread_id_) * sizeof(StackType_t); + odrv.system_stats_.min_stack_space_usb = uxTaskGetStackHighWaterMark(usb_thread) * sizeof(StackType_t); + odrv.system_stats_.min_stack_space_uart = uxTaskGetStackHighWaterMark(uart_thread) * sizeof(StackType_t); + odrv.system_stats_.min_stack_space_usb_irq = uxTaskGetStackHighWaterMark(usb_irq_thread) * sizeof(StackType_t); + odrv.system_stats_.min_stack_space_startup = uxTaskGetStackHighWaterMark(defaultTaskHandle) * sizeof(StackType_t); + odrv.system_stats_.min_stack_space_can = uxTaskGetStackHighWaterMark(odCAN->thread_id_) * sizeof(StackType_t); // Actual usage, in bytes, so we don't have to math - system_stats_.stack_usage_axis0 = axes[0]->stack_size_ - system_stats_.min_stack_space_axis0; - system_stats_.stack_usage_axis1 = axes[1]->stack_size_ - system_stats_.min_stack_space_axis1; - system_stats_.stack_usage_comms = stack_size_comm_thread - system_stats_.min_stack_space_comms; - system_stats_.stack_usage_usb = stack_size_usb_thread - system_stats_.min_stack_space_usb; - system_stats_.stack_usage_uart = stack_size_uart_thread - system_stats_.min_stack_space_uart; - system_stats_.stack_usage_usb_irq = stack_size_usb_irq_thread - system_stats_.min_stack_space_usb_irq; - system_stats_.stack_usage_startup = stack_size_default_task - system_stats_.min_stack_space_startup; - system_stats_.stack_usage_can = odCAN->stack_size_ - system_stats_.min_stack_space_can; + odrv.system_stats_.stack_usage_axis0 = axes[0]->stack_size_ - odrv.system_stats_.min_stack_space_axis0; + odrv.system_stats_.stack_usage_axis1 = axes[1]->stack_size_ - odrv.system_stats_.min_stack_space_axis1; + odrv.system_stats_.stack_usage_comms = stack_size_comm_thread - odrv.system_stats_.min_stack_space_comms; + odrv.system_stats_.stack_usage_usb = stack_size_usb_thread - odrv.system_stats_.min_stack_space_usb; + odrv.system_stats_.stack_usage_uart = stack_size_uart_thread - odrv.system_stats_.min_stack_space_uart; + odrv.system_stats_.stack_usage_usb_irq = stack_size_usb_irq_thread - odrv.system_stats_.min_stack_space_usb_irq; + odrv.system_stats_.stack_usage_startup = stack_size_default_task - odrv.system_stats_.min_stack_space_startup; + odrv.system_stats_.stack_usage_can = odCAN->stack_size_ - odrv.system_stats_.min_stack_space_can; } } } @@ -230,7 +233,7 @@ int odrive_main(void) { // TODO: make dynamically reconfigurable #if HW_VERSION_MAJOR == 3 && HW_VERSION_MINOR >= 3 - if (board_config.enable_uart) { + if (odrv.config_.enable_uart) { SetGPIO12toUART(); } #endif @@ -271,6 +274,6 @@ int odrive_main(void) { start_analog_thread(); - system_stats_.fully_booted = true; + odrv.system_stats_.fully_booted = true; return 0; } diff --git a/Firmware/MotorControl/motor.cpp b/Firmware/MotorControl/motor.cpp index 6b8c8d0f..f924f13f 100644 --- a/Firmware/MotorControl/motor.cpp +++ b/Firmware/MotorControl/motor.cpp @@ -127,7 +127,7 @@ bool Motor::check_DRV_fault() { GPIO_PinState nFAULT_state = HAL_GPIO_ReadPin(gate_driver_config_.nFAULT_port, gate_driver_config_.nFAULT_pin); if (nFAULT_state == GPIO_PIN_RESET) { // Update DRV Fault Code - drv_fault_ = DRV8301_getFaultType(&gate_driver_); + gate_driver_exported_.drv_fault = (GateDriverIntf::DrvFault)DRV8301_getFaultType(&gate_driver_); // Update/Cache all SPI device registers // DRV_SPI_8301_Vars_t* local_regs = &gate_driver_regs_; // local_regs->RcvCmd = true; diff --git a/Firmware/MotorControl/motor.hpp b/Firmware/MotorControl/motor.hpp index 67a0751d..88a735e3 100644 --- a/Firmware/MotorControl/motor.hpp +++ b/Firmware/MotorControl/motor.hpp @@ -7,35 +7,8 @@ #include "drv8301.h" -class Motor { +class Motor : public MotorIntf { public: - enum Error { - ERROR_NONE = 0, - ERROR_PHASE_RESISTANCE_OUT_OF_RANGE = 0x0001, - ERROR_PHASE_INDUCTANCE_OUT_OF_RANGE = 0x0002, - ERROR_ADC_FAILED = 0x0004, - ERROR_DRV_FAULT = 0x0008, - ERROR_CONTROL_DEADLINE_MISSED = 0x0010, - ERROR_NOT_IMPLEMENTED_MOTOR_TYPE = 0x0020, - ERROR_BRAKE_CURRENT_OUT_OF_RANGE = 0x0040, - ERROR_MODULATION_MAGNITUDE = 0x0080, - ERROR_BRAKE_DEADTIME_VIOLATION = 0x0100, - ERROR_UNEXPECTED_TIMER_CALLBACK = 0x0200, - ERROR_CURRENT_SENSE_SATURATION = 0x0400, - ERROR_INVERTER_OVER_TEMP = 0x0800, - ERROR_CURRENT_LIMIT_VIOLATION = 0x1000, - ERROR_BRAKE_DUTY_CYCLE_NAN = 0x2000, - ERROR_DC_BUS_OVER_REGEN_CURRENT = 0x4000, // too much current pushed into the power supply - ERROR_DC_BUS_OVER_CURRENT = 0x8000, // too much current pulled out of the power supply - }; - - enum MotorType_t { - MOTOR_TYPE_HIGH_CURRENT = 0, - // MOTOR_TYPE_LOW_CURRENT = 1, //Not yet implemented - MOTOR_TYPE_GIMBAL = 2, - MOTOR_TYPE_ACIM = 3, - }; - struct Iph_BC_t { float phB; float phC; @@ -65,7 +38,7 @@ public: // NOTE: for gimbal motors, all units of A are instead V. // example: vel_gain is [V/(count/s)] instead of [A/(count/s)] // example: current_lim and calibration_current will instead determine the maximum voltage applied to the motor. - struct Config_t { + struct Config_t : public ConfigIntf { bool pre_calibrated = false; // can be set to true to indicate that all values here are valid int32_t pole_pairs = 7; float calibration_current = 10.0f; // [A] @@ -73,7 +46,7 @@ public: float phase_inductance = 0.0f; // to be set by measure_phase_inductance float phase_resistance = 0.0f; // to be set by measure_phase_resistance int32_t direction = 0; // 1 or -1 (0 = unspecified) - MotorType_t motor_type = MOTOR_TYPE_HIGH_CURRENT; + MotorType motor_type = MOTOR_TYPE_HIGH_CURRENT; // Read out max_allowed_current to see max supported value for current_lim. // float current_lim = 70.0f; //[A] float current_lim = 10.0f; //[A] @@ -89,6 +62,16 @@ public: bool acim_autoflux_enable = false; float acim_autoflux_attack_gain = 10.0f; float acim_autoflux_decay_gain = 1.0f; + + // custom property setters + Motor* parent = nullptr; + void set_pre_calibrated(bool value) { + pre_calibrated = value; + parent->is_calibrated_ = parent->is_calibrated_ || parent->config_.pre_calibrated; + } + void set_phase_inductance(float value) { phase_inductance = value; parent->update_current_controller_gains(); } + void set_phase_resistance(float value) { phase_resistance = value; parent->update_current_controller_gains(); } + void set_current_control_bandwidth(float value) { current_control_bandwidth = value; parent->update_current_controller_gains(); } }; enum TimingLog_t { @@ -107,13 +90,6 @@ public: TIMING_LOG_NUM_SLOTS }; - enum ArmedState_t { - ARMED_STATE_DISARMED, - ARMED_STATE_WAITING_FOR_TIMINGS, - ARMED_STATE_WAITING_FOR_UPDATE, - ARMED_STATE_ARMED, - }; - Motor(const MotorHardwareConfig_t& hw_config, const GateDriverHardwareConfig_t& gate_driver_config, Config_t& config); @@ -160,13 +136,17 @@ public: bool next_timings_valid_ = false; uint16_t last_cpu_time_ = 0; int timing_log_index_ = 0; - uint16_t timing_log_[TIMING_LOG_NUM_SLOTS] = { 0 }; + struct { + uint16_t& operator[](size_t idx) { return content[idx]; } + uint16_t& get(size_t idx) { return content[idx]; } + uint16_t content[TIMING_LOG_NUM_SLOTS]; + } timing_log_; // variables exposed on protocol Error error_ = ERROR_NONE; // Do not write to this variable directly! // It is for exclusive use by the safety_critical_... functions. - ArmedState_t armed_state_ = ARMED_STATE_DISARMED; + ArmedState armed_state_ = ARMED_STATE_DISARMED; bool is_calibrated_ = config_.pre_calibrated; Iph_BC_t current_meas_ = {0.0f, 0.0f}; Iph_BC_t DC_calib_ = {0.0f, 0.0f}; @@ -190,95 +170,12 @@ public: .async_phase_vel = 0.0f, .async_phase_offset = 0.0f, }; - DRV8301_FaultType_e drv_fault_ = DRV8301_FaultType_NoFault; + struct : GateDriverIntf { + DrvFault drv_fault = DRV_FAULT_NO_FAULT; + } gate_driver_exported_; DRV_SPI_8301_Vars_t gate_driver_regs_; //Local view of DRV registers (initialized by DRV8301_setup) float thermal_current_lim_ = 10.0f; //[A] float inverter_temp_ = 20.0f; - - // Communication protocol definitions - auto make_protocol_definitions() { - return make_protocol_member_list( - make_protocol_property("error", &error_), - make_protocol_ro_property("armed_state", &armed_state_), - make_protocol_ro_property("is_calibrated", &is_calibrated_), - make_protocol_ro_property("current_meas_phB", ¤t_meas_.phB), - make_protocol_ro_property("current_meas_phC", ¤t_meas_.phC), - make_protocol_property("DC_calib_phB", &DC_calib_.phB), - make_protocol_property("DC_calib_phC", &DC_calib_.phC), - make_protocol_property("phase_current_rev_gain", &phase_current_rev_gain_), - make_protocol_ro_property("thermal_current_lim", &thermal_current_lim_), - make_protocol_ro_property("inverter_temp", &inverter_temp_), - make_protocol_object("current_control", - make_protocol_property("p_gain", ¤t_control_.p_gain), - make_protocol_property("i_gain", ¤t_control_.i_gain), - make_protocol_property("v_current_control_integral_d", ¤t_control_.v_current_control_integral_d), - make_protocol_property("v_current_control_integral_q", ¤t_control_.v_current_control_integral_q), - make_protocol_property("Ibus", ¤t_control_.Ibus), - make_protocol_property("final_v_alpha", ¤t_control_.final_v_alpha), - make_protocol_property("final_v_beta", ¤t_control_.final_v_beta), - make_protocol_property("Id_setpoint", ¤t_control_.Id_setpoint), - make_protocol_ro_property("Iq_setpoint", ¤t_control_.Iq_setpoint), - make_protocol_property("Iq_measured", ¤t_control_.Iq_measured), - make_protocol_property("Id_measured", ¤t_control_.Id_measured), - make_protocol_property("I_measured_report_filter_k", ¤t_control_.I_measured_report_filter_k), - make_protocol_ro_property("max_allowed_current", ¤t_control_.max_allowed_current), - make_protocol_ro_property("overcurrent_trip_level", ¤t_control_.overcurrent_trip_level), - make_protocol_property("acim_rotor_flux", ¤t_control_.acim_rotor_flux), - make_protocol_ro_property("async_phase_vel", ¤t_control_.async_phase_vel), - make_protocol_property("async_phase_offset", ¤t_control_.async_phase_offset) - ), - make_protocol_object("gate_driver", - make_protocol_ro_property("drv_fault", &drv_fault_) - // make_protocol_ro_property("status_reg_1", &gate_driver_regs_.Stat_Reg_1_Value), - // make_protocol_ro_property("status_reg_2", &gate_driver_regs_.Stat_Reg_2_Value), - // make_protocol_ro_property("ctrl_reg_1", &gate_driver_regs_.Ctrl_Reg_1_Value), - // make_protocol_ro_property("ctrl_reg_2", &gate_driver_regs_.Ctrl_Reg_2_Value) - ), - make_protocol_object("timing_log", - make_protocol_ro_property("TIMING_LOG_GENERAL", &timing_log_[TIMING_LOG_GENERAL]), - make_protocol_ro_property("TIMING_LOG_ADC_CB_I", &timing_log_[TIMING_LOG_ADC_CB_I]), - make_protocol_ro_property("TIMING_LOG_ADC_CB_DC", &timing_log_[TIMING_LOG_ADC_CB_DC]), - make_protocol_ro_property("TIMING_LOG_MEAS_R", &timing_log_[TIMING_LOG_MEAS_R]), - make_protocol_ro_property("TIMING_LOG_MEAS_L", &timing_log_[TIMING_LOG_MEAS_L]), - make_protocol_ro_property("TIMING_LOG_ENC_CALIB", &timing_log_[TIMING_LOG_ENC_CALIB]), - make_protocol_ro_property("TIMING_LOG_IDX_SEARCH", &timing_log_[TIMING_LOG_IDX_SEARCH]), - make_protocol_ro_property("TIMING_LOG_FOC_VOLTAGE", &timing_log_[TIMING_LOG_FOC_VOLTAGE]), - make_protocol_ro_property("TIMING_LOG_FOC_CURRENT", &timing_log_[TIMING_LOG_FOC_CURRENT]), - make_protocol_ro_property("TIMING_LOG_SPI_START", &timing_log_[TIMING_LOG_SPI_START]), - make_protocol_ro_property("TIMING_LOG_SAMPLE_NOW", &timing_log_[TIMING_LOG_SAMPLE_NOW]), - make_protocol_ro_property("TIMING_LOG_SPI_END", &timing_log_[TIMING_LOG_SPI_END]) - ), - make_protocol_object("config", - make_protocol_property("pre_calibrated", &config_.pre_calibrated, - [](void* ctx) { static_cast(ctx)->is_calibrated_ = - static_cast(ctx)->is_calibrated_ || static_cast(ctx)->config_.pre_calibrated; }, this), - make_protocol_property("pole_pairs", &config_.pole_pairs), - make_protocol_property("calibration_current", &config_.calibration_current), - make_protocol_property("resistance_calib_max_voltage", &config_.resistance_calib_max_voltage), - make_protocol_property("phase_inductance", &config_.phase_inductance, - [](void* ctx) { static_cast(ctx)->update_current_controller_gains(); }, this), - make_protocol_property("phase_resistance", &config_.phase_resistance, - [](void* ctx) { static_cast(ctx)->update_current_controller_gains(); }, this), - make_protocol_property("direction", &config_.direction), - make_protocol_property("motor_type", &config_.motor_type), - make_protocol_property("current_lim", &config_.current_lim), - make_protocol_property("current_lim_margin", &config_.current_lim_margin), - make_protocol_property("inverter_temp_limit_lower", &config_.inverter_temp_limit_lower), - make_protocol_property("inverter_temp_limit_upper", &config_.inverter_temp_limit_upper), - make_protocol_property("requested_current_range", &config_.requested_current_range), - make_protocol_property("current_control_bandwidth", &config_.current_control_bandwidth, - [](void* ctx) { static_cast(ctx)->update_current_controller_gains(); }, this), - make_protocol_property("acim_slip_velocity", &config_.acim_slip_velocity), - make_protocol_property("acim_gain_min_flux", &config_.acim_gain_min_flux), - make_protocol_property("acim_autoflux_min_Id", &config_.acim_autoflux_min_Id), - make_protocol_property("acim_autoflux_enable", &config_.acim_autoflux_enable), - make_protocol_property("acim_autoflux_attack_gain", &config_.acim_autoflux_attack_gain), - make_protocol_property("acim_autoflux_decay_gain", &config_.acim_autoflux_decay_gain) - ) - ); - } }; -DEFINE_ENUM_FLAG_OPERATORS(Motor::Error) - #endif // __MOTOR_HPP diff --git a/Firmware/MotorControl/odrive_main.h b/Firmware/MotorControl/odrive_main.h index 6ae7594e..a3eb4447 100644 --- a/Firmware/MotorControl/odrive_main.h +++ b/Firmware/MotorControl/odrive_main.h @@ -10,6 +10,8 @@ #ifdef __cplusplus #include +#include +#include extern "C" { #endif @@ -41,11 +43,13 @@ static const int current_meas_hz = CURRENT_MEAS_HZ; // extern const float elec_rad_per_enc; extern uint32_t _reboot_cookie; -extern bool user_config_loaded_; extern uint64_t serial_number; extern char serial_number_str[13]; +#ifdef __cplusplus +} + typedef struct { bool fully_booted; uint32_t uptime; // [ms] @@ -67,11 +71,10 @@ typedef struct { uint32_t stack_usage_usb_irq; uint32_t stack_usage_startup; uint32_t stack_usage_can; -} SystemStats_t; -extern SystemStats_t system_stats_; -#ifdef __cplusplus -} + USBStats_t& usb = usb_stats_; + I2CStats_t& i2c = i2c_stats_; +} SystemStats_t; struct PWMMapping_t { endpoint_ref_t endpoint; @@ -148,8 +151,6 @@ struct BoardConfig_t { */ uint32_t uart_baudrate = 115200; }; -extern BoardConfig_t board_config; -extern bool user_config_loaded_; // Forward Declarations class Axis; @@ -176,6 +177,7 @@ inline ENUMTYPE &operator &= (ENUMTYPE &a, ENUMTYPE b) { return reinterpret_cast inline ENUMTYPE &operator ^= (ENUMTYPE &a, ENUMTYPE b) { return reinterpret_cast(reinterpret_cast&>(a) ^= static_cast>(b)); } \ inline ENUMTYPE operator ~ (ENUMTYPE a) { return static_cast(~static_cast>(a)); } +#include "autogen/interfaces.hpp" // ODrive specific includes #include @@ -190,12 +192,79 @@ inline ENUMTYPE operator ~ (ENUMTYPE a) { return static_cast(~static_c #include #include -#endif // __cplusplus +#include "autogen/version.h" // general system functions defined in main.cpp -void save_configuration(void); -void erase_configuration(void); -void enter_dfu_mode(void); +class ODrive : public OdriveIntf { +public: + void save_configuration() override; + void erase_configuration() override; + void reboot() override { NVIC_SystemReset(); } + void enter_dfu_mode() override; + + float get_oscilloscope_val(uint32_t index) override { + return oscilloscope[index]; + } + + float get_adc_voltage(uint32_t gpio) override { + return ::get_adc_voltage(get_gpio_port_by_pin(gpio), get_gpio_pin_by_pin(gpio)); + } + + int32_t test_function(int32_t delta) override { + static int cnt = 0; + return cnt += delta; + } + + Axis& get_axis(int num) { return *axes[num]; } + ODriveCAN& get_can() { return *odCAN; } + + float& vbus_voltage_ = ::vbus_voltage; // TODO: make this the actual variable + float& ibus_ = ::ibus_; // TODO: make this the actual variable + + const uint64_t& serial_number_ = ::serial_number; + +#if HW_VERSION_MAJOR == 3 + // Determine start address of the OTP struct: + // The OTP is organized into 16-byte blocks. + // If the first block starts with "0xfe" we use the first block. + // If the first block starts with "0x00" and the second block starts with "0xfe", + // we use the second block. This gives the user the chance to screw up once. + // If none of the above is the case, we consider the OTP invalid (otp_ptr will be NULL). + const uint8_t* otp_ptr = + (*(uint8_t*)FLASH_OTP_BASE == 0xfe) ? (uint8_t*)FLASH_OTP_BASE : + (*(uint8_t*)FLASH_OTP_BASE != 0x00) ? NULL : + (*(uint8_t*)(FLASH_OTP_BASE + 0x10) != 0xfe) ? NULL : + (uint8_t*)(FLASH_OTP_BASE + 0x10); + + // Read hardware version from OTP if available, otherwise fall back + // to software defined version. + const uint8_t hw_version_major_ = otp_ptr ? otp_ptr[3] : HW_VERSION_MAJOR; + const uint8_t hw_version_minor_ = otp_ptr ? otp_ptr[4] : HW_VERSION_MINOR; + const uint8_t hw_version_variant_ = otp_ptr ? otp_ptr[5] : HW_VERSION_VOLTAGE; +#else +#error "not implemented" +#endif + + // the corresponding macros are defined in the autogenerated version.h + const uint8_t fw_version_major_ = FW_VERSION_MAJOR; + const uint8_t fw_version_minor_ = FW_VERSION_MINOR; + const uint8_t fw_version_revision_ = FW_VERSION_REVISION; + const uint8_t fw_version_unreleased_ = FW_VERSION_UNRELEASED; // 0 for official releases, 1 otherwise + + bool& brake_resistor_armed_ = ::brake_resistor_armed; // TODO: make this the actual variable + bool& brake_resistor_saturated_ = ::brake_resistor_saturated; // TODO: make this the actual variable + + SystemStats_t system_stats_; + + BoardConfig_t config_; + bool user_config_loaded_; + + uint32_t test_property_ = 0; +}; + +extern ODrive odrv; // defined in main.cpp + +#endif // __cplusplus #endif /* __ODRIVE_MAIN_H */ diff --git a/Firmware/MotorControl/sensorless_estimator.hpp b/Firmware/MotorControl/sensorless_estimator.hpp index dd48c705..99c339be 100644 --- a/Firmware/MotorControl/sensorless_estimator.hpp +++ b/Firmware/MotorControl/sensorless_estimator.hpp @@ -1,13 +1,8 @@ #ifndef __SENSORLESS_ESTIMATOR_HPP #define __SENSORLESS_ESTIMATOR_HPP -class SensorlessEstimator { +class SensorlessEstimator : public SensorlessEstimatorIntf { public: - enum Error { - ERROR_NONE = 0, - ERROR_UNSTABLE_GAIN = 0x01, - }; - struct Config_t { float observer_gain = 1000.0f; // [rad/s] float pll_bandwidth = 1000.0f; // [rad/s] @@ -32,25 +27,6 @@ public: float flux_state_[2] = {0.0f, 0.0f}; // [Vs] float V_alpha_beta_memory_[2] = {0.0f, 0.0f}; // [V] bool estimator_good_ = false; - - // Communication protocol definitions - auto make_protocol_definitions() { - return make_protocol_member_list( - make_protocol_property("error", &error_), - make_protocol_property("phase", &phase_), - make_protocol_property("pll_pos", &pll_pos_), - make_protocol_property("vel_estimate", &vel_estimate_), - // make_protocol_property("pll_kp", &pll_kp_), - // make_protocol_property("pll_ki", &pll_ki_), - make_protocol_object("config", - make_protocol_property("observer_gain", &config_.observer_gain), - make_protocol_property("pll_bandwidth", &config_.pll_bandwidth), - make_protocol_property("pm_flux_linkage", &config_.pm_flux_linkage) - ) - ); - } }; -DEFINE_ENUM_FLAG_OPERATORS(SensorlessEstimator::Error) - #endif /* __SENSORLESS_ESTIMATOR_HPP */ diff --git a/Firmware/MotorControl/trapTraj.hpp b/Firmware/MotorControl/trapTraj.hpp index fd142da5..c3df57b9 100644 --- a/Firmware/MotorControl/trapTraj.hpp +++ b/Firmware/MotorControl/trapTraj.hpp @@ -20,16 +20,6 @@ public: float Vmax, float Amax, float Dmax); Step_t eval(float t); - auto make_protocol_definitions() { - return make_protocol_member_list( - make_protocol_object("config", - make_protocol_property("vel_limit", &config_.vel_limit), - make_protocol_property("accel_limit", &config_.accel_limit), - make_protocol_property("decel_limit", &config_.decel_limit) - ) - ); - } - Axis* axis_ = nullptr; // set by Axis constructor Config_t& config_; diff --git a/Firmware/Tupfile.lua b/Firmware/Tupfile.lua index 4577bde1..dec3547d 100644 --- a/Firmware/Tupfile.lua +++ b/Firmware/Tupfile.lua @@ -1,6 +1,17 @@ tup.include('build.lua') +tup.frule{inputs={'fibre/cpp/interfaces_template.j2'}, command='python3 interface_generator.py --definitions odrive-interface.yaml --template %f --output %o', outputs='autogen/interfaces.hpp'} +tup.frule{inputs={'fibre/cpp/function_stubs_template.j2'}, command='python3 interface_generator.py --definitions odrive-interface.yaml --template %f --output %o', outputs='autogen/function_stubs.hpp'} +tup.frule{inputs={'fibre/cpp/endpoints_template.j2'}, command='python3 interface_generator.py --definitions odrive-interface.yaml --template %f --output %o', outputs='autogen/endpoints.hpp'} +tup.frule{inputs={'ascii_type_info_template.j2'}, command='python3 interface_generator.py --definitions odrive-interface.yaml --template %f --output %o', outputs='autogen/ascii_type_info.hpp'} + +tup.frule{ + command='python ../tools/odrive/version.py --output %o', + outputs={'autogen/version.h'} +} + + -- Switch between board versions boardversion = tup.getconfig("BOARD_VERSION") if boardversion == "v3.1" then @@ -86,7 +97,6 @@ if tup.getconfig("STRICT") == "true" then FLAGS += '-Werror' end - -- C-specific flags FLAGS += '-D__weak="__attribute__((weak))"' FLAGS += '-D__packed="__attribute__((__packed__))"' @@ -145,10 +155,6 @@ build{ includes=stm_includes } -tup.frule{ - command='python ../tools/odrive/version.py --output %o', - outputs={'build/version.h'} -} build{ name='ODriveFirmware', diff --git a/Firmware/ascii_type_info_template.j2 b/Firmware/ascii_type_info_template.j2 new file mode 100644 index 00000000..200eb1ba --- /dev/null +++ b/Firmware/ascii_type_info_template.j2 @@ -0,0 +1,90 @@ +/*[# This is the original template, thus the warning below does not apply to this file #] + * ============================ WARNING ============================ + * ==== This is an autogenerated file. ==== + * ==== Any changes to this file will be lost when recompiling. ==== + * ================================================================= + * + * This file contains support functions for the ODrive ASCII protocol. + * + * TODO: might generalize this as an approach to runtime introspection. + */ + + +class TypeInfo; + +struct PropertyInfo { + const char * name; + void*(*getter)(void*); + TypeInfo* type_info; +}; + +class TypeInfo { +public: + TypeInfo(const PropertyInfo* property_table, size_t property_table_length) + : property_table_(property_table), property_table_length_(property_table_length) {} + + //virtual bool read_string(void* ctx) { return false; }; + //virtual bool write_string(void* ctx) { return false; }; + + const PropertyInfo* get_property_info(const char * name, size_t length) { + for (const PropertyInfo* prop = property_table_; prop < (property_table_ + property_table_length_); ++prop) { + if (!strncmp(name, prop->name, length)) { + return prop; + } + } + return nullptr; + } + +private: + const PropertyInfo* property_table_; + size_t property_table_length_; +}; + + +class Introspectable { +public: + Introspectable(void* obj, TypeInfo* type_info) : obj_(obj), type_info_(type_info) {} + + Introspectable get_child(const char * path, size_t length) { + Introspectable current = *this; + + const char * begin = path; + const char * end = path + length; + + while ((begin < end) && current.obj_ && current.type_info_) { + const char * end_of_token = std::find(begin, end, '.'); + const PropertyInfo* prop_info = current.type_info_->get_property_info(begin, end_of_token - begin); + if (prop_info) { + current = Introspectable{(*prop_info->getter)(obj_), prop_info->type_info}; + } else { + current = Introspectable{nullptr, nullptr}; + } + begin = std::min(end, end_of_token + 1); + } + + return current; + }; + +private: + void* obj_; + TypeInfo* type_info_; +}; + +[% for intf in interfaces.values() %] + +template +struct [[intf.fullname | to_pascal_case]]TypeInfo : TypeInfo { + static const PropertyInfo property_table[]; + static const TypeInfo singleton; +}; + +template +const PropertyInfo [[intf.name | to_pascal_case]]TypeInfo::property_table[] = { +[%- for property in intf.attributes.values() %] + {"[[property.name]]", [](void* obj){ return (void*)static_cast<[[property.type.c_type]]*>(&((T*)obj)->[[property.name | to_snake_case]]); }, [[property.type.fullname | to_pascal_case]]TypeInfo().[[property.name | to_snake_case]])>::singleton}, +[%- endfor %] +}; +template +const TypeInfo [[intf.fullname | to_pascal_case]]TypeInfo::singleton{[[intf.fullname | to_pascal_case]]TypeInfo::property_table, sizeof([[intf.fullname | to_pascal_case]]TypeInfo::property_table) / sizeof([[intf.fullname | to_pascal_case]]TypeInfo::property_table[0])}; + +[% endfor %] diff --git a/Firmware/build.lua b/Firmware/build.lua index 016f6d8e..0f91697f 100644 --- a/Firmware/build.lua +++ b/Firmware/build.lua @@ -72,7 +72,7 @@ function GCCToolchain(prefix, builddir, compiler_flags, linker_flags) else extra_outputs = {} end - if src == 'communication/communication.cpp' then extra_inputs = 'build/version.h' end -- TODO: fix hack + extra_inputs = {'autogen/version.h', 'autogen/interfaces.hpp', 'autogen/function_stubs.hpp', 'autogen/endpoints.hpp'} -- TODO: fix hack tup.frule{ inputs= { src, extra_inputs=extra_inputs }, command=compiler..' -c %f '.. diff --git a/Firmware/communication/ascii_protocol.cpp b/Firmware/communication/ascii_protocol.cpp index 628dea63..4eaaf611 100644 --- a/Firmware/communication/ascii_protocol.cpp +++ b/Firmware/communication/ascii_protocol.cpp @@ -8,12 +8,15 @@ /* Includes ------------------------------------------------------------------*/ #include "odrive_main.h" -#include "../build/version.h" // autogenerated based on Git state +#include "../autogen/version.h" // autogenerated based on Git state #include "communication.h" #include "ascii_protocol.hpp" #include #include +//#include "autogen/interfaces.hpp" +//#include "autogen/ascii_type_info.hpp" + /* Private macros ------------------------------------------------------------*/ /* Private typedef -----------------------------------------------------------*/ /* Global constant data ------------------------------------------------------*/ @@ -208,19 +211,20 @@ void ASCII_protocol_process_line(const uint8_t* buffer, size_t len, StreamSink& // respond(response_channel, use_checksum, "Signature: %#x", STM_ID_GetSignature()); // respond(response_channel, use_checksum, "Revision: %#x", STM_ID_GetRevision()); // respond(response_channel, use_checksum, "Flash Size: %#x KiB", STM_ID_GetFlashSize()); - respond(response_channel, use_checksum, "Hardware version: %d.%d-%dV", HW_VERSION_MAJOR, HW_VERSION_MINOR, HW_VERSION_VOLTAGE); - respond(response_channel, use_checksum, "Firmware version: %d.%d.%d", FW_VERSION_MAJOR, FW_VERSION_MINOR, FW_VERSION_REVISION); + respond(response_channel, use_checksum, "Hardware version: %d.%d-%dV", odrv.hw_version_major_, odrv.hw_version_minor_, odrv.hw_version_variant_); + respond(response_channel, use_checksum, "Firmware version: %d.%d.%d", odrv.fw_version_major_, odrv.fw_version_minor_, odrv.fw_version_revision_); respond(response_channel, use_checksum, "Serial number: %s", serial_number_str); } else if (cmd[0] == 's'){ // System if(cmd[1] == 's') { // Save config - save_configuration(); + odrv.save_configuration(); } else if (cmd[1] == 'e'){ // Erase config - erase_configuration(); + odrv.erase_configuration(); } else if (cmd[1] == 'r'){ // Reboot - NVIC_SystemReset(); + odrv.reboot(); } +#if 0 } else if (cmd[0] == 'r') { // read property char name[MAX_LINE_LENGTH]; int numscan = sscanf(cmd, "r %255s", name); @@ -256,6 +260,7 @@ void ASCII_protocol_process_line(const uint8_t* buffer, size_t len, StreamSink& respond(response_channel, use_checksum, "not implemented"); } } +#endif } else if (cmd[0] == 'u') { // Update axis watchdog. unsigned motor_number; diff --git a/Firmware/communication/can_simple.cpp b/Firmware/communication/can_simple.cpp index 5ff8009a..4ec91660 100644 --- a/Firmware/communication/can_simple.cpp +++ b/Firmware/communication/can_simple.cpp @@ -188,7 +188,7 @@ void CANSimple::set_axis_nodeid_callback(Axis* axis, can_Message_t& msg) { } void CANSimple::set_axis_requested_state_callback(Axis* axis, can_Message_t& msg) { - axis->requested_state_ = static_cast(can_getSignal(msg, 0, 16, true)); + axis->requested_state_ = static_cast(can_getSignal(msg, 0, 16, true)); } void CANSimple::set_axis_startup_config_callback(Axis* axis, can_Message_t& msg) { // Not Implemented diff --git a/Firmware/communication/communication.cpp b/Firmware/communication/communication.cpp index c318040b..4e732a9c 100644 --- a/Firmware/communication/communication.cpp +++ b/Firmware/communication/communication.cpp @@ -13,7 +13,7 @@ #include "utils.hpp" #include "gpio_utils.hpp" -#include "../build/version.h" // autogenerated based on Git state +#include "../autogen/version.h" // autogenerated based on Git state #include #include @@ -36,50 +36,11 @@ char serial_number_str[13]; // 12 digits + null termination /* Private constant data -----------------------------------------------------*/ /* Private variables ---------------------------------------------------------*/ -#if HW_VERSION_MAJOR == 3 -// Determine start address of the OTP struct: -// The OTP is organized into 16-byte blocks. -// If the first block starts with "0xfe" we use the first block. -// If the first block starts with "0x00" and the second block starts with "0xfe", -// we use the second block. This gives the user the chance to screw up once. -// If none of the above is the case, we consider the OTP invalid (otp_ptr will be NULL). -const uint8_t* otp_ptr = - (*(uint8_t*)FLASH_OTP_BASE == 0xfe) ? (uint8_t*)FLASH_OTP_BASE : - (*(uint8_t*)FLASH_OTP_BASE != 0x00) ? NULL : - (*(uint8_t*)(FLASH_OTP_BASE + 0x10) != 0xfe) ? NULL : - (uint8_t*)(FLASH_OTP_BASE + 0x10); - -// Read hardware version from OTP if available, otherwise fall back -// to software defined version. -const uint8_t hw_version_major = otp_ptr ? otp_ptr[3] : HW_VERSION_MAJOR; -const uint8_t hw_version_minor = otp_ptr ? otp_ptr[4] : HW_VERSION_MINOR; -const uint8_t hw_version_variant = otp_ptr ? otp_ptr[5] : HW_VERSION_VOLTAGE; -#else -#error "not implemented" -#endif - -// the corresponding macros are defined in the autogenerated version.h -const uint8_t fw_version_major = FW_VERSION_MAJOR; -const uint8_t fw_version_minor = FW_VERSION_MINOR; -const uint8_t fw_version_revision = FW_VERSION_REVISION; -const uint8_t fw_version_unreleased = FW_VERSION_UNRELEASED; // 0 for official releases, 1 otherwise - osThreadId comm_thread; const uint32_t stack_size_comm_thread = 4096; // Bytes volatile bool endpoint_list_valid = false; -static uint32_t test_property = 0; - /* Private function prototypes -----------------------------------------------*/ - -auto make_protocol_definitions(PWMMapping_t& mapping) { - return make_protocol_member_list( - make_protocol_property("endpoint", &mapping.endpoint), - make_protocol_property("min", &mapping.min), - make_protocol_property("max", &mapping.max) - ); -} - /* Function implementations --------------------------------------------------*/ void init_communication(void) { @@ -96,120 +57,6 @@ void init_communication(void) { float oscilloscope[OSCILLOSCOPE_SIZE] = {0}; size_t oscilloscope_pos = 0; -// Helper class because the protocol library doesn't yet -// support non-member functions -// TODO: make this go away -class StaticFunctions { -public: - void save_configuration_helper() { save_configuration(); } - void erase_configuration_helper() { erase_configuration(); } - void NVIC_SystemReset_helper() { NVIC_SystemReset(); } - void enter_dfu_mode_helper() { enter_dfu_mode(); } - float get_oscilloscope_val(uint32_t index) { return oscilloscope[index]; } - float get_adc_voltage_(uint32_t gpio) { return get_adc_voltage(get_gpio_port_by_pin(gpio), get_gpio_pin_by_pin(gpio)); } - int32_t test_function(int32_t delta) { static int cnt = 0; return cnt += delta; } -} static_functions; - -// When adding new functions/variables to the protocol, be careful not to -// blow the communication stack. You can check comm_stack_info to see -// how much headroom you have. -static inline auto make_obj_tree() { - return make_protocol_member_list( - make_protocol_ro_property("vbus_voltage", &vbus_voltage), - make_protocol_ro_property("ibus", &ibus_), - make_protocol_ro_property("serial_number", &serial_number), - make_protocol_ro_property("hw_version_major", &hw_version_major), - make_protocol_ro_property("hw_version_minor", &hw_version_minor), - make_protocol_ro_property("hw_version_variant", &hw_version_variant), - make_protocol_ro_property("fw_version_major", &fw_version_major), - make_protocol_ro_property("fw_version_minor", &fw_version_minor), - make_protocol_ro_property("fw_version_revision", &fw_version_revision), - make_protocol_ro_property("fw_version_unreleased", &fw_version_unreleased), - make_protocol_ro_property("user_config_loaded", const_cast(&user_config_loaded_)), - make_protocol_ro_property("brake_resistor_armed", &brake_resistor_armed), - make_protocol_property("brake_resistor_saturated", &brake_resistor_saturated), - make_protocol_object("system_stats", - make_protocol_ro_property("uptime", &system_stats_.uptime), - make_protocol_ro_property("min_heap_space", &system_stats_.min_heap_space), - make_protocol_ro_property("min_stack_space_axis0", &system_stats_.min_stack_space_axis0), - make_protocol_ro_property("min_stack_space_axis1", &system_stats_.min_stack_space_axis1), - make_protocol_ro_property("min_stack_space_comms", &system_stats_.min_stack_space_comms), - make_protocol_ro_property("min_stack_space_usb", &system_stats_.min_stack_space_usb), - make_protocol_ro_property("min_stack_space_uart", &system_stats_.min_stack_space_uart), - make_protocol_ro_property("min_stack_space_can", &system_stats_.min_stack_space_can), - make_protocol_ro_property("min_stack_space_usb_irq", &system_stats_.min_stack_space_usb_irq), - make_protocol_ro_property("min_stack_space_startup", &system_stats_.min_stack_space_startup), - make_protocol_ro_property("stack_usage_axis0", &system_stats_.stack_usage_axis0), - make_protocol_ro_property("stack_usage_axis1", &system_stats_.stack_usage_axis1), - make_protocol_ro_property("stack_usage_comms", &system_stats_.stack_usage_comms), - make_protocol_ro_property("stack_usage_usb", &system_stats_.stack_usage_usb), - make_protocol_ro_property("stack_usage_uart", &system_stats_.stack_usage_uart), - make_protocol_ro_property("stack_usage_usb_irq", &system_stats_.stack_usage_usb_irq), - make_protocol_ro_property("stack_usage_startup", &system_stats_.stack_usage_startup), - make_protocol_ro_property("stack_usage_can", &system_stats_.stack_usage_can), - make_protocol_object("usb", - make_protocol_ro_property("rx_cnt", &usb_stats_.rx_cnt), - make_protocol_ro_property("tx_cnt", &usb_stats_.tx_cnt), - make_protocol_ro_property("tx_overrun_cnt", &usb_stats_.tx_overrun_cnt) - ), - make_protocol_object("i2c", - make_protocol_ro_property("addr", &i2c_stats_.addr), - make_protocol_ro_property("addr_match_cnt", &i2c_stats_.addr_match_cnt), - make_protocol_ro_property("rx_cnt", &i2c_stats_.rx_cnt), - make_protocol_ro_property("error_cnt", &i2c_stats_.error_cnt) - ) - ), - make_protocol_object("config", - make_protocol_property("brake_resistance", &board_config.brake_resistance), - make_protocol_property("max_regen_current", &board_config.max_regen_current), - // TODO: changing this currently requires a reboot - fix this - make_protocol_property("enable_uart", &board_config.enable_uart), - make_protocol_property("uart_baudrate", &board_config.uart_baudrate), // requires a reboot - make_protocol_property("enable_i2c_instead_of_can" , &board_config.enable_i2c_instead_of_can), // requires a reboot - make_protocol_property("enable_ascii_protocol_on_usb", &board_config.enable_ascii_protocol_on_usb), - make_protocol_property("dc_bus_undervoltage_trip_level", &board_config.dc_bus_undervoltage_trip_level), - make_protocol_property("dc_bus_overvoltage_trip_level", &board_config.dc_bus_overvoltage_trip_level), - make_protocol_property("enable_dc_bus_overvoltage_ramp", &board_config.enable_dc_bus_overvoltage_ramp), - make_protocol_property("dc_bus_overvoltage_ramp_start", &board_config.dc_bus_overvoltage_ramp_start), - make_protocol_property("dc_bus_overvoltage_ramp_end", &board_config.dc_bus_overvoltage_ramp_end), - make_protocol_property("dc_max_negative_current", &board_config.dc_max_negative_current), - make_protocol_property("dc_max_positive_current", &board_config.dc_max_positive_current), -#if HW_VERSION_MAJOR == 3 && HW_VERSION_MINOR >= 3 - make_protocol_object("gpio1_pwm_mapping", make_protocol_definitions(board_config.pwm_mappings[0])), - make_protocol_object("gpio2_pwm_mapping", make_protocol_definitions(board_config.pwm_mappings[1])), - make_protocol_object("gpio3_pwm_mapping", make_protocol_definitions(board_config.pwm_mappings[2])), -#endif - make_protocol_object("gpio4_pwm_mapping", make_protocol_definitions(board_config.pwm_mappings[3])), - - make_protocol_object("gpio3_analog_mapping", make_protocol_definitions(board_config.analog_mappings[2])), - make_protocol_object("gpio4_analog_mapping", make_protocol_definitions(board_config.analog_mappings[3])) - ), - make_protocol_object("axis0", axes[0]->make_protocol_definitions()), - make_protocol_object("axis1", axes[1]->make_protocol_definitions()), - make_protocol_object("can", odCAN->make_protocol_definitions()), - make_protocol_property("test_property", &test_property), - make_protocol_function("test_function", static_functions, &StaticFunctions::test_function, "delta"), - make_protocol_function("get_oscilloscope_val", static_functions, &StaticFunctions::get_oscilloscope_val, "index"), - make_protocol_function("get_adc_voltage", static_functions, &StaticFunctions::get_adc_voltage_, "gpio"), - make_protocol_function("save_configuration", static_functions, &StaticFunctions::save_configuration_helper), - make_protocol_function("erase_configuration", static_functions, &StaticFunctions::erase_configuration_helper), - make_protocol_function("reboot", static_functions, &StaticFunctions::NVIC_SystemReset_helper), - make_protocol_function("enter_dfu_mode", static_functions, &StaticFunctions::enter_dfu_mode_helper) - ); -} - -using tree_type = decltype(make_obj_tree()); -uint8_t tree_buffer[sizeof(tree_type)]; - - -void initTree(){ - // TODO: this is supposed to use the move constructor, but currently - // the compiler uses the copy-constructor instead. Thus the make_obj_tree - // ends up with a stupid stack size of around 8000 bytes. Fix this. - auto tree_ptr = new (tree_buffer) tree_type(make_obj_tree()); - fibre_publish(*tree_ptr); -} - // Thread to handle deffered processing of USB interrupt, and // read commands out of the UART DMA circular buffer void communication_task(void * ctx) { @@ -220,7 +67,7 @@ void communication_task(void * ctx) { start_uart_server(); start_usb_server(); - if (board_config.enable_i2c_instead_of_can) { + if (odrv.config_.enable_i2c_instead_of_can) { start_i2c_server(); } else { odCAN->start_can_server(); @@ -245,3 +92,7 @@ int _write(int file, const char* data, int len) { #endif return len; } + + +#include "../autogen/function_stubs.hpp" +#include "../autogen/endpoints.hpp" diff --git a/Firmware/communication/communication.h b/Firmware/communication/communication.h index 85519b39..6987aa11 100644 --- a/Firmware/communication/communication.h +++ b/Firmware/communication/communication.h @@ -17,10 +17,6 @@ extern "C" { extern osThreadId comm_thread; extern const uint32_t stack_size_comm_thread; -extern const uint8_t hw_version_major; -extern const uint8_t hw_version_minor; -extern const uint8_t hw_version_variant; - void init_communication(void); void initTree(); void communication_task(void * ctx); diff --git a/Firmware/communication/interface_can.cpp b/Firmware/communication/interface_can.cpp index 339ef9ff..659958c7 100644 --- a/Firmware/communication/interface_can.cpp +++ b/Firmware/communication/interface_can.cpp @@ -16,9 +16,9 @@ // std::unordered_map ctxMap; // Constructor is called by communication.cpp and the handle is assigned appropriately -ODriveCAN::ODriveCAN(CAN_HandleTypeDef *handle, ODriveCAN::Config_t &config) - : handle_{handle}, - config_{config} { +ODriveCAN::ODriveCAN(ODriveCAN::Config_t &config, CAN_HandleTypeDef *handle) + : config_{config}, + handle_{handle} { // ctxMap[handle_] = this; } @@ -32,7 +32,7 @@ void ODriveCAN::can_server_thread() { while (available()) { read(rxmsg); switch (config_.protocol) { - case CAN_PROTOCOL_SIMPLE: + case Config_t::PROTOCOL_SIMPLE: CANSimple::handle_can_message(rxmsg); break; } @@ -183,7 +183,7 @@ void ODriveCAN::send_heartbeat(Axis *axis) { uint32_t now = osKernelSysTick(); if ((now - axis->last_heartbeat_) >= axis->config_.can_heartbeat_rate_ms) { switch (config_.protocol) { - case CAN_PROTOCOL_SIMPLE: + case Config_t::PROTOCOL_SIMPLE: CANSimple::send_heartbeat(axis); break; } diff --git a/Firmware/communication/interface_can.hpp b/Firmware/communication/interface_can.hpp index fca28822..44791a33 100644 --- a/Firmware/communication/interface_can.hpp +++ b/Firmware/communication/interface_can.hpp @@ -19,23 +19,14 @@ enum { CAN_BAUD_1M = 1000000 }; -enum CAN_Protocol_t { - CAN_PROTOCOL_SIMPLE -}; - -class ODriveCAN { +class ODriveCAN : public OdriveIntf::CanIntf { public: - struct Config_t { + struct Config_t : ConfigIntf { uint32_t baud_rate = CAN_BAUD_250K; - CAN_Protocol_t protocol = CAN_PROTOCOL_SIMPLE; + Protocol protocol = PROTOCOL_SIMPLE; }; - enum Error { - ERROR_NONE = 0x00, - ERROR_DUPLICATE_CAN_IDS = 0x01 - }; - - ODriveCAN(CAN_HandleTypeDef *handle, ODriveCAN::Config_t &config); + ODriveCAN(ODriveCAN::Config_t &config, CAN_HandleTypeDef *handle); // Thread Relevant Data osThreadId thread_id_; @@ -55,24 +46,12 @@ class ODriveCAN { uint32_t write(can_Message_t &txmsg); bool read(can_Message_t &rxmsg); - // Communication Protocol Handling - auto make_protocol_definitions() { - return make_protocol_member_list( - make_protocol_property("error", &error_), - make_protocol_object("config", - make_protocol_ro_property("baud_rate", &config_.baud_rate)), - make_protocol_property("can_protocol", &config_.protocol), - make_protocol_function("set_baud_rate", *this, &ODriveCAN::set_baud_rate, "baudRate")); - } - - private: - CAN_HandleTypeDef *handle_ = nullptr; ODriveCAN::Config_t &config_; +private: + CAN_HandleTypeDef *handle_ = nullptr; + void set_baud_rate(uint32_t baudRate); }; - -DEFINE_ENUM_FLAG_OPERATORS(ODriveCAN::Error) - #endif // __INTERFACE_CAN_HPP diff --git a/Firmware/communication/interface_usb.cpp b/Firmware/communication/interface_usb.cpp index 21273dee..83632bcd 100644 --- a/Firmware/communication/interface_usb.cpp +++ b/Firmware/communication/interface_usb.cpp @@ -127,7 +127,7 @@ static void usb_server_thread(void * ctx) { // CDC Interface if (CDC_interface.data_pending) { CDC_interface.data_pending = false; - if (board_config.enable_ascii_protocol_on_usb) { + if (odrv.config_.enable_ascii_protocol_on_usb) { ASCII_protocol_parse_stream(CDC_interface.rx_buf, CDC_interface.rx_len, usb_stream_output); } else { diff --git a/Firmware/fibre/cpp/endpoints_template.j2 b/Firmware/fibre/cpp/endpoints_template.j2 new file mode 100644 index 00000000..98c5eb1d --- /dev/null +++ b/Firmware/fibre/cpp/endpoints_template.j2 @@ -0,0 +1,71 @@ +/*[# This is the original template, thus the warning below does not apply to this file #] + * ============================ WARNING ============================ + * ==== This is an autogenerated file. ==== + * ==== Any changes to this file will be lost when recompiling. ==== + * ================================================================= + * + * This file contains the toplevel handler for Fibre v0.1 endpoint operations. + * + * This endpoint-oriented approach will be deprecated in Fibre v0.2 in favor of + * a function-oriented approach and a more powerful object model. + * + */ +#ifndef __FIBRE_INTERFACES_HPP +#define __FIBRE_INTERFACES_HPP + +namespace fibre { + +const unsigned char embedded_json[] = [[embedded_endpoint_definitions | to_c_string]]; +const size_t embedded_json_length = sizeof(embedded_json) - 1; +const uint16_t json_crc_ = calc_crc16(PROTOCOL_VERSION, embedded_json, embedded_json_length); +const uint32_t json_version_id_ = (json_crc_ << 16) | calc_crc16(json_crc_, embedded_json, embedded_json_length); + +// Note: with -Og this function reserves a huge amount of stack space because it +// reserves separate space for the stack frame of each of the inlined functions. +// The minimum known set of flags to prevent this is `-O1 -fipa-sra`. +// `-O2` is a superset of this so that's what we use here. +bool endpoint_handler(int idx, cbufptr_t* input_buffer, bufptr_t* output_buffer) __attribute__ ((optimize(2))); + +bool endpoint_handler(int idx, cbufptr_t* input_buffer, bufptr_t* output_buffer) { + switch (idx) { +[%- for endpoint in endpoints %] + case [[endpoint.id]]: { return [[endpoint.function.fullname | to_snake_case]]([% for k, arg in endpoint.function.in.items() %][% if k in endpoint.in_bindings %]static_cast<[[arg.type.c_type]]>([[endpoint.in_bindings[k]]])[% else %]std::nullopt[% endif %], [% endfor %][% for k, arg in endpoint.function.out.items() %][% if k in endpoint.out_bindings %]static_cast<[[arg.type.c_type]]*>([[endpoint.out_bindings[k]]])[% else %]nullptr[% endif %], [% endfor %]input_buffer, output_buffer); } break; +[%- endfor %] + default: return false; + } +} + +bool is_endpoint_ref_valid(endpoint_ref_t endpoint_ref) { + if (endpoint_ref.json_crc != json_crc_) { + return false; + } + + switch (endpoint_ref.endpoint_id) { +[%- for endpoint in endpoints %] + case [[endpoint.id]]: return true; +[%- endfor %] + default: return false; + } +} + +bool set_endpoint_from_float(endpoint_ref_t endpoint_ref, float value) { + if (endpoint_ref.json_crc != json_crc_) { + return false; + } + + return false; + // TODO: implement + /*cbufptr_t input_buffer{}; + bufptr_t output_buffer{}; + + switch (idx) { +[%- for endpoint in endpoints %] + case [[endpoint.id]]: return [[endpoint.function.fullname | to_snake_case]]([% for k, arg in endpoint.function.in.items() %][% if k in endpoint.in_bindings %]static_cast<[[arg.type.c_type]]>([[endpoint.in_bindings[k]]])[% else %]std::nullopt[% endif %], [% endfor %]&input_buffer, &output_buffer); +[%- endfor %] + default: return false; + }*/ +} + +} + +#endif // __FIBRE_INTERFACES_HPP \ No newline at end of file diff --git a/Firmware/fibre/cpp/function_stubs_template.j2 b/Firmware/fibre/cpp/function_stubs_template.j2 new file mode 100644 index 00000000..759b49eb --- /dev/null +++ b/Firmware/fibre/cpp/function_stubs_template.j2 @@ -0,0 +1,40 @@ +/*[# This is the original template, thus the warning below does not apply to this file #] + * ============================ WARNING ============================ + * ==== This is an autogenerated file. ==== + * ==== Any changes to this file will be lost when recompiling. ==== + * ================================================================= + * + * This file contains serializing/deserializing stubs for the functions defined + * in your interface file. + * + */ + +#include + +[% for intf in interfaces.values() %] +[% for func in intf.functions.values() %] +static inline bool [[func.fullname | to_snake_case]]([% for arg in func.in.values() %]std::optional<[[arg.type.c_type]]> in_[[arg.name]], [% endfor %][% for arg in func.out.values() %][[arg.type.c_type]]* out_[[arg.name]], [% endfor %]fibre::cbufptr_t* input_buffer, fibre::bufptr_t* output_buffer) { +[%- if func.in %] + bool success = [% for arg in func.in.values() %](in_[[arg.name]].has_value() || (in_[[arg.name]] = fibre::Codec<[[arg.type.c_type]]>::decode(input_buffer)).has_value()[% if arg.optional %] || true[% endif %])[% if not loop.last %] + && [% endif %][% endfor %]; +[%- else %] + bool success = true; +[%- endif %] + if (!success) { + return false; + } +[%- if func.implementation %] + [% if func.out %]std::tuple<[% for arg in func.out.values() %][[arg.type.c_type]][[', ' if not loop.last]][% endfor %]> ret = [% endif %][[func.implementation]]([% for arg in func.in.values() %]in_[[arg.name]][% if not arg.optional %].value()[% endif %][[', ' if not loop.last]][% endfor %]); +[%- else %] + [% if func.out %]std::tuple<[% for arg in func.out.values() %][[arg.type.c_type]][[', ' if not loop.last]][% endfor %]> ret = [% endif %]in_[[(func.in.values() | first).name]].value()->[[func.name]]([% for arg in func.in.values() | skip_first %]in_[[arg.name]][% if not arg.optional %].value()[% endif %][[', ' if not loop.last]][% endfor %]); +[%- endif %] +[%- if func.out %] + return [% for arg in func.out.values() %]((out_[[arg.name]] && ((*out_[[arg.name]] = std::get<[[loop.index0]]>(ret)), true)) || fibre::Codec<[[arg.type.c_type]]>::encode(std::get<[[loop.index0]]>(ret), output_buffer))[% if not loop.last %] + && [% endif %][% endfor %]; +[%- else %] + return true; +[%- endif %] +} +[% endfor %] +[% endfor %] + diff --git a/Firmware/fibre/cpp/include/fibre/bufptr.hpp b/Firmware/fibre/cpp/include/fibre/bufptr.hpp new file mode 100644 index 00000000..2ce3fefb --- /dev/null +++ b/Firmware/fibre/cpp/include/fibre/bufptr.hpp @@ -0,0 +1,93 @@ +#ifndef __FIBRE_BUFPTR_HPP +#define __FIBRE_BUFPTR_HPP + +namespace fibre { + +static inline bool soft_assert(bool expr) { return expr; } // TODO: implement + +/** + * @brief Holds a reference to a buffer and a length. + * Since this class implements begin() and end(), you can use it with many + * standard algorithms that operate on iterable objects. + */ +template +struct generic_bufptr_t { + using iterator = T*; + using const_iterator = const T*; + + generic_bufptr_t(T* begin, size_t length) : begin_(begin), end_(begin + length) {} + + generic_bufptr_t(T* begin, T* end) : begin_(begin), end_(end) {} + + generic_bufptr_t() : begin_(nullptr), end_(nullptr) {} + + template + generic_bufptr_t(T (&begin)[I]) : generic_bufptr_t(begin, I) {} + + generic_bufptr_t(const std::vector>& vector) + : generic_bufptr_t(vector.data(), vector.size()) {} + + generic_bufptr_t(const generic_bufptr_t>& other) + : generic_bufptr_t(other.begin_, other.end_) {} + + generic_bufptr_t& operator+=(size_t num) { + if (!soft_assert(num <= size())) { + num = size(); + } + begin_ += num; + return *this; + } + + generic_bufptr_t operator++(int) { + generic_bufptr_t result = *this; + *this += 1; + return result; + } + + T& operator*() { + return *begin_; + } + + generic_bufptr_t take(size_t num) const { + if (!soft_assert(num <= size())) { + num = size(); + } + generic_bufptr_t result = {begin_, num}; + return result; + } + + generic_bufptr_t skip(size_t num, size_t* processed_bytes = nullptr) const { + if (!soft_assert(num <= size())) { + num = size(); + } + if (processed_bytes) + (*processed_bytes) += num; + return {begin_ + num, end_}; + } + + size_t size() const { + return end_ - begin_; + } + + bool empty() const { + return size() == 0; + } + + T*& begin() { return begin_; } + T*& end() { return end_; } + T* const & begin() const { return begin_; } + T* const & end() const { return end_; } + T& front() const { return *begin(); } + T& back() const { return *(end() - 1); } + T& operator[](size_t idx) { return *(begin() + idx); } + + T* begin_; + T* end_; +}; + +using cbufptr_t = generic_bufptr_t; +using bufptr_t = generic_bufptr_t; + +} + +#endif // __FIBRE_BUFPTR_HPP diff --git a/Firmware/fibre/cpp/include/fibre/cpp_utils.hpp b/Firmware/fibre/cpp/include/fibre/cpp_utils.hpp index 4b97f367..b2d84256 100644 --- a/Firmware/fibre/cpp/include/fibre/cpp_utils.hpp +++ b/Firmware/fibre/cpp/include/fibre/cpp_utils.hpp @@ -1,6 +1,3 @@ -#ifndef __CPP_UTILS_HPP -#define __CPP_UTILS_HPP - /* ## Advanced C++ Topics @@ -78,8 +75,20 @@ public: */ -// Backport definitions from C++14 -#if __cplusplus <= 201103L +#ifndef __CPP_UTILS_HPP +#define __CPP_UTILS_HPP + +#include +#include +#include +#include +//#include +#include +#include + +/* Backport features from C++14 and C++17 ------------------------------------*/ + +#if __cplusplus < 201402L namespace std { template< class T > using underlying_type_t = typename underlying_type::type; @@ -87,9 +96,377 @@ namespace std { // source: http://en.cppreference.com/w/cpp/types/enable_if template< bool B, class T = void > using enable_if_t = typename enable_if::type; + + // source: https://en.cppreference.com/w/cpp/types/conditional + template< bool B, class T, class F > + using conditional_t = typename conditional::type; + + // source: http://en.cppreference.com/w/cpp/utility/tuple/tuple_element + template + using tuple_element_t = typename tuple_element::type; + + // source: https://en.cppreference.com/w/cpp/types/remove_cv + template< class T > + using remove_cv_t = typename remove_cv::type; + template< class T > + using remove_const_t = typename remove_const::type; + template< class T > + using remove_volatile_t = typename remove_volatile::type; + template< class T > + using remove_reference_t = typename remove_reference::type; + + template< class T > + using decay_t = typename decay::type; + + // integer_sequence implementation adapted from + // https://stackoverflow.com/questions/17424477/implementation-c14-make-integer-sequence + + /// Class template integer_sequence + template + struct integer_sequence { + using type = integer_sequence; + typedef _Tp value_type; + static constexpr size_t size() noexcept { return sizeof...(_Idx); } + }; + + template + struct _merge_and_renumber; + + template + struct _merge_and_renumber, integer_sequence<_Tp, I2...>> + : integer_sequence<_Tp, I1..., (sizeof...(I1)+I2)...> + { }; + + template + struct make_integer_sequence + : _merge_and_renumber::type, + typename make_integer_sequence<_Tp, N - N/2>::type> + { }; + + template struct make_integer_sequence<_Tp, 0> : integer_sequence<_Tp> { }; + template struct make_integer_sequence<_Tp, 1> : integer_sequence<_Tp, 0> { }; + + /// Alias template index_sequence + template + using index_sequence = integer_sequence; + + /// Alias template make_index_sequence + template + using make_index_sequence = typename make_integer_sequence::type; } #endif +namespace fibre { + // Creates the index sequence { IFrom, IFrom + 1, IFrom + 2, ..., ITo - 1 } + template + struct make_integer_sequence_from_to_impl { + using type = typename make_integer_sequence_from_to_impl<_Tp, IFrom, ITo - 1, ITo - 1, I...>::type; + }; + + template + struct make_integer_sequence_from_to_impl<_Tp, IFrom, IFrom, I...> { + using type = std::index_sequence; + }; + + template + using make_integer_sequence_from_to = typename make_integer_sequence_from_to_impl<_Tp, IFrom, ITo>::type; +} + +#if __cplusplus < 201703L +namespace std { +//template>{}, int> = 0> +//using enable_ + +template struct invoke_result_impl; + +template +struct invoke_result_impl>{}>, + Fn, Args...> { + typedef decltype(std::mem_fn(std::declval())(std::declval()...)) type; +}; + +template +struct invoke_result_impl>{}>, + Fn, Args...> { + typedef decltype(std::declval()(std::declval()...)) type; +}; + +template +using invoke_result = invoke_result_impl; + +template +using invoke_result_t = typename invoke_result::type; + +template>{}, int> = 0 > +constexpr invoke_result_t invoke(Fn&& f, Args&&... args) + noexcept(noexcept(std::mem_fn(f)(std::forward(args)...))) +{ + return std::mem_fn(f)(std::forward(args)...); +} + +template>{}, int> = 0> +constexpr invoke_result_t invoke(Fn&& f, Args&&... args) + noexcept(noexcept(std::forward(f)(std::forward(args)...))) +{ + return std::forward(f)(std::forward(args)...); +} +} + +namespace std { +namespace detail { +template +struct apply_result_impl; + +// TODO: apply_result is not part of C++17, therefore we should move this out of +// the #if block +template +struct apply_result_impl> { + //typedef std::invoke_result_t...> type; + typedef std::invoke_result_t(std::declval()))...> type; +}; + +template +using apply_result = apply_result_impl>::value>>; + +template +using apply_result_t = typename apply_result::type; + +template +constexpr apply_result_t apply_impl( F&& f, Tuple&& t, std::index_sequence ) +{ + return std::invoke(std::forward(f), std::get(std::forward(t))...); +} +} // namespace detail + +template +constexpr detail::apply_result_t apply(F&& f, Tuple&& t) +{ + return detail::apply_impl(std::forward(f), std::forward(t), + std::make_index_sequence>::value>{}); +} +} + + +namespace std { + +template +struct identity { using type = T; }; + +template +struct overload_resolver; + +template<> +struct overload_resolver<> { void operator()() const; }; + +template +struct overload_resolver : overload_resolver { + using overload_resolver::operator(); + identity operator()(T) const; +}; + +template +struct index_of : integral_constant::value + 1)> {}; + +template +struct index_of : integral_constant {}; + +/** + * @brief Heavily simplified version of the C++17 std::variant. + * Whatever compiles should work as one would expect from the C++17 variant. + */ +template +class variant; + +// Empty variant is ill-formed. Only used for clean recursion here. +template<> +class variant<> { +public: + using storage_t = char[0]; + storage_t content_; + + static void selective_destructor(char* storage, size_t index) { + throw; + } + + static void selective_copy_constuctor(char* target, const char* source, size_t index) { + throw; + } + + static bool selective_eq(const char* lhs, const char* rhs, size_t index) { + throw; + } + + static bool selective_neq(const char* lhs, const char* rhs, size_t index) { + throw; + } + + template + static void selective_invoke_const(const char* content, size_t index, TFunc functor, TArgs&&... args) { + throw; + } + + template + static void selective_invoke(const char* content, size_t index, TFunc functor, TArgs&&... args) { + throw; + } +}; + +template +class variant { +public: + using storage_t = char[sizeof(T) > sizeof(typename variant::storage_t) ? sizeof(T) : sizeof(typename variant::storage_t)]; + + static void selective_copy_constuctor(char* target, const char* source, size_t index) { + if (index == 0) { + new ((T*)target) T{*(T*)source}; // in-place construction using first type's copy constructor + } else { + variant::selective_copy_constuctor(target, source, index - 1); + } + } + + static void selective_destructor(char* storage, size_t index) { + if (index == 0) { + ((T*)storage)->~T(); + } else { + variant::selective_destructor(storage, index - 1); + } + } + + static bool selective_eq(const char* lhs, const char* rhs, size_t index) { + if (index == 0) { + return ((*(T*)lhs) == (*(T*)rhs)); + } else { + return variant::selective_eq(lhs, rhs, index - 1); + } + } + + static bool selective_neq(const char* lhs, const char* rhs, size_t index) { + if (index == 0) { + return ((*(T*)lhs) != (*(T*)rhs)); + } else { + return variant::selective_neq(lhs, rhs, index - 1); + } + } + + template + static void selective_invoke_const(const char* content, size_t index, TFunc functor, TArgs&&... args) { + if (index == 0) { + functor(*(T*)content, std::forward(args)...); + } else { + variant::selective_invoke_const(content, index - 1, functor, std::forward(args)...); + } + } + + template + static void selective_invoke(char* content, size_t index, TFunc functor, TArgs&&... args) { + if (index == 0) { + functor(*(T*)content, std::forward(args)...); + } else { + variant::selective_invoke(content, index - 1, functor, std::forward(args)...); + } + } + + variant() : index_(0) { + new ((T*)content_) T{}; // in-place construction using first type's default constructor + } + + variant(const variant & other) : index_(other.index_) { + selective_copy_constuctor(content_, other.content_, index_); + } + + variant(variant&& other) : index_(other.index_) { + // TODO: implement + selective_copy_constuctor(content_, other.content_, index_); + } + + // Find the best match out of `T, Ts...` with `TArg` as the argument. + template + using best_match = decltype(overload_resolver()(std::declval())); + + template::type> //, typename=typename std::enable_if_t, variant>::value)>, typename TTarget=decltype(indicator_func(std::forward(std::declval()))), typename TIndex=index_of> + variant(TArg&& arg) { + new ((TTarget*)content_) TTarget{std::forward(arg)}; + index_ = index_of::value; + } + + ~variant() { + selective_destructor(content_, index_); + } + + inline variant& operator=(const variant & other) { + selective_destructor(content_, index_); + index_ = other.index_; + selective_copy_constuctor(content_, other.content_, index_); + return *this; + } + + inline bool operator==(const variant& rhs) const { + return (index_ == rhs.index_) && selective_eq(this->content_, rhs.content_, index_); + } + + inline bool operator!=(const variant& rhs) const { + return (index_ != rhs.index_) || selective_neq(this->content_, rhs.content_, index_); + } + + template + void invoke(TFunc functor, TArgs&&... args) const { + selective_invoke_const(content_, index_, functor, std::forward(args)...); + } + + template + void invoke(TFunc functor, TArgs&&... args) { + selective_invoke(content_, index_, functor, std::forward(args)...); + } + + storage_t content_; + size_t index_; + + size_t index() const { return index_; } +}; + +template +std::tuple_element_t>& get(std::variant& val) { + if (val.index() != I) + throw; + using T = std::tuple_element_t>; + return *((T*)val.content_); +} + +template +T& get(std::variant& val) { + constexpr size_t index = std::index_of::value; + return std::get(val); +} + +} // namespace std + +#endif + +/* Stuff that should be in the STL but isn't ---------------------------------*/ + +// source: https://en.cppreference.com/w/cpp/experimental/to_array +namespace detail { +template +constexpr std::array, N> + to_array_impl(T (&a)[N], std::index_sequence) +{ + return { {a[I]...} }; +} + +template +constexpr std::array, N> to_array(T (&a)[N]) +{ + return detail::to_array_impl(a, std::make_index_sequence{}); +} +} + + + +/* Custom utils --------------------------------------------------------------*/ + // @brief Supports various queries on a list of types template class TypeChecker; @@ -112,6 +489,7 @@ public: return std::is_base_of::value && TypeChecker::template all_are(); } + constexpr static const size_t count = TypeChecker::count + 1; }; template<> @@ -125,11 +503,17 @@ public: constexpr static inline bool all_are() { return std::true_type::value; } + constexpr static const size_t count = 0; }; +template +TypeChecker make_type_checker(Ts ...) { + return TypeChecker(); +} + #include #define ENABLE_IF(...) \ - typename = std::enable_if_t<__VA_ARGS__> + typename = typename std::enable_if_t<__VA_ARGS__> #define ENABLE_IF_SAME(a, b, type) \ template typename std::enable_if_t::value, type> @@ -151,15 +535,83 @@ class function_traits { public: template static TRet invoke(TObj& obj, TRet(TObj::*func_ptr)(TArgs...), std::tuple packed_args, TUnpackedArgs ... args) { - return invoke(obj, func_ptr, packed_args, args..., std::get(packed_args)); + return invoke(obj, func_ptr, packed_args, std::forward(args)..., std::get(packed_args)); } template static TRet invoke(TObj& obj, TRet(TObj::*func_ptr)(TArgs...), std::tuple packed_args, TArgs ... args) { - return (obj.*func_ptr)(args...); + return (obj.*func_ptr)(std::forward(args)...); } }; + +/* @brief return_type::type represents the C++ native return type +* of a function returning 0 or more arguments. +* +* For an empty TypeList, the return type is void. For a list with +* one type, the return type is equal to that type. For a list with +* more than one items, the return type is a tuple. +*/ +template +struct return_type; + +template<> +struct return_type<> { typedef void type; }; +template +struct return_type { typedef T type; }; +template +struct return_type { typedef std::tuple type; }; + + + +template +struct static_function_traits; + +// TODO: All invoke-related functions should be superseeded by a proper std::apply implementation +#if 0 +template +struct static_function_traits, std::tuple> { + using TRet = typename return_type::type; + + //template + //static std::tuple invoke(std::tuple packed_args, TUnpackedInputs ... args) { + // return invoke(packed_args, args..., std::get(packed_args)); + //} + + template + static std::tuple invoke(std::tuple& packed_args) { + return invoke_impl(packed_args, std::make_index_sequence()); + } + + template + static std::tuple invoke_impl(std::tuple packed_args, std::index_sequence) { + return invoke_impl_2(std::get(packed_args)...); + } + + //template + //static std::enable_if_t<(sizeof...(TOutputs) == 0), std::tuple> + template>*/> + static std::enable_if_t<(IOutputs == 0), std::tuple> + invoke_impl_2(TInputs ... args) { + Function(args...); + return std::make_tuple<>(); + } + + //template + //static std::enable_if_t<(sizeof...(TOutputs) == 1), std::tuple> + template>*/> + static std::enable_if_t<(IOutputs == 1), std::tuple> + invoke_impl_2(TInputs ... args) { + return std::make_tuple(Function(args...)); + } +// +// template= 2)> +// static /* std::enable_if_t= 2, */ std::tuple //> +// invoke_impl_2(std::tuple packed_args, TInputs ... args) { +// return Function(args...); +// } +}; + /* @brief Invoke a class member function with a variable number of arguments that are supplied as a tuple Example usage: @@ -180,4 +632,681 @@ TRet invoke_function_with_tuple(TObj& obj, TRet(TObj::*func_ptr)(TArgs...), std: return function_traits::template invoke<0>(obj, func_ptr, packed_args); } +template(*Function)(TIn...)> +std::tuple invoke_with_tuples(std::tuple inputs) { + static_function_traits::template invoke<0>(inputs); +} +#endif + + +template +struct sum_impl; +template +struct sum_impl { static constexpr TInt value = 0; }; +template +struct sum_impl { static constexpr TInt value = I + sum_impl::value; }; + +template +using sum = sum_impl; + + +// source: https://akrzemi1.wordpress.com/2017/05/18/asserts-in-constexpr-functions/ +#if defined NDEBUG +# define X_ASSERT(CHECK) void(0) +#else +# define X_ASSERT(CHECK) \ + ( (CHECK) ? void(0) : []{assert(!#CHECK);}() ) +#endif + +template +struct for_each_in_tuple_result_impl; + +template +struct for_each_in_tuple_result_impl> { + typedef std::tuple(std::declval())(std::get(std::declval())))...> type; +}; + +template +using for_each_in_tuple_result = for_each_in_tuple_result_impl>::value>>; + +template +using for_each_in_tuple_result_t = typename for_each_in_tuple_result::type; + +template +for_each_in_tuple_result_t for_each_in_tuple_impl(Fn&& f, Tuple&& t, std::index_sequence) { + return for_each_in_tuple_result_t(std::forward(f)(std::get(t))...); +} + +template +for_each_in_tuple_result_t for_each_in_tuple(Fn&& f, Tuple&& t) { + return for_each_in_tuple_impl(std::forward(f), std::forward(t), std::make_index_sequence>::value>{}); +} +//template +//for_each_in_tuple_result_t for_each_in_tuple(Fn&& f, Tuple&& t) { +// return 5; +//} + + +/* constexpr strings --------------------------------------------------------*/ +/* adapted from: +* https://akrzemi1.wordpress.com/2017/06/28/compile-time-string-concatenation/ +*/ + + +// TODO: the functionality +// sstring::substring, sstring::get_last_part and sstring::after_last_index_of and sstring::last_index_of +// was removed during refactoring. Add again if needed. + +/** + * @brief Represents a string that is known at compile time by encoding it as a + * type. + */ +template +struct sstring { + static constexpr const char chars[] = {CHARS..., 0}; + static constexpr const char* c_str() { return chars; } + static constexpr size_t size() { return sizeof...(CHARS); } + static constexpr std::array as_array() { return {CHARS...}; } + + template + constexpr bool operator==(const sstring & other) { + return as_array() == other.as_array(); + } +}; +template +constexpr const char sstring::chars[/*sizeof...(CHARS) + 1*/]; + +template +struct sstring_concat_impl; + +template +struct sstring_concat_impl, sstring> { + using type = sstring; +}; + +/** @brief Represents the result type of concatenating two static strings */ +template +using sstring_concat_t = typename sstring_concat_impl::type; + +/** @brief Concatenates two static strings */ +template +constexpr sstring operator+(sstring, sstring) { + return {}; +} + + +/** @brief Helper class for the MAKE_SSTRING macro */ +template +struct sstring_builder; + +template +struct sstring_builder<0, CHAR, CHARS...> { + using type = sstring<>; +}; + +template +struct sstring_builder { + using type = sstring_concat_t, typename sstring_builder::type>; +}; + +template +using sstring_builder_t = typename sstring_builder::type; + +#define MACRO_GET_1(str, i) \ + (sizeof(str) > (i) ? str[(i)] : 0) + +#define MACRO_GET_4(str, i) \ + MACRO_GET_1(str, i+0), \ + MACRO_GET_1(str, i+1), \ + MACRO_GET_1(str, i+2), \ + MACRO_GET_1(str, i+3) + +#define MACRO_GET_16(str, i) \ + MACRO_GET_4(str, i+0), \ + MACRO_GET_4(str, i+4), \ + MACRO_GET_4(str, i+8), \ + MACRO_GET_4(str, i+12) + +#define MACRO_GET_64(str, i) \ + MACRO_GET_16(str, i+0), \ + MACRO_GET_16(str, i+16), \ + MACRO_GET_16(str, i+32), \ + MACRO_GET_16(str, i+48) + +/** + * @brief Builds a compile-time string type from a string literal. + * + * Passing more than 64 characters will prune the string. + * + * Usage: + * MAKE_SSTRING("hello world") my_str{}; + * or + * auto my_str = MAKE_SSTRING("hello world"){}; + * + * Both examples create a compile-time variable "my_str" of which the type + * itself stores the content "hello world". + */ +#define MAKE_SSTRING(literal) sstring_builder_t + +namespace std { +template +static std::ostream& operator<<(std::ostream& stream, const sstring& val) { + stream << val.chars; + return stream; +} +} + +template +struct join_sstring_impl; + +template +struct join_sstring_impl> { + using type = sstring<>; +}; + +template +struct join_sstring_impl, sstring> { + using type = sstring; +}; + +template +struct join_sstring_impl, sstring, TStr...> { + using type = sstring_concat_t, typename join_sstring_impl, TStr...>::type>; +}; + +template +using join_sstring_t = typename join_sstring_impl::type; + +template +constexpr join_sstring_t join_sstring(const TDelimiter& delimiter, const TStr& ... str) { + return {}; +} + +template +using sstring_arr = std::tuple...>; + + +// source: https://stackoverflow.com/questions/40159732/return-other-value-if-key-not-found-in-the-map +template +TValue& get_or(std::unordered_map& m, const TKey& key, TValue& default_value) { + auto it = m.find(key); + if (it == m.end()) { + return default_value; + } else { + return it->second; + } +} +template +TValue* get_ptr(std::unordered_map& m, const TKey& key) { + auto it = m.find(key); + if (it == m.end()) + return nullptr; + else + return &(it->second); +} + +template +std::true_type is_complete_impl(T *); +std::false_type is_complete_impl(...); + +/** @brief is_complete resolves to std::true_type if T is complete + * and to std::false_type otherwise. This can be used to check if a certain template + * specialization exists. + **/ +template +using is_complete = decltype(is_complete_impl(std::declval())); + +template +struct dynamic_get_impl { + template + static TRet* get(size_t i, TTuple& t) { + if (i == I::value) + return &static_cast(std::get(t)); + else if (i > I::value) + return dynamic_get_impl, TRet, Ts...>::get(i, t); + return nullptr; // this should not happen + } +}; + +template +struct dynamic_get_impl, TRet, Ts...> { + static TRet* get(size_t i, const std::tuple& t) { + return nullptr; + } +}; + +template +TRet* dynamic_get(size_t i, std::tuple& t) { + return dynamic_get_impl, TRet, Ts...>::get(i, t); +} + +template +TRet* dynamic_get(size_t i, const std::tuple& t) { + return dynamic_get_impl, TRet, Ts...>::get(i, t); +} + +/* Hex to numbers ------------------------------------------------------------*/ + +template +constexpr size_t hex_digits() { + return (std::numeric_limits::digits + 3) / 4; +} + +/* @brief Converts a hexadecimal digit to a uint8_t. +* @param output If not null, the digit's value is stored in this output +* Returns true if the char is a valid hex digit, false otherwise +*/ +static bool hex_digit_to_byte(char ch, uint8_t* output) { + uint8_t nil_output = 0; + if (!output) + output = &nil_output; + if (ch >= '0' && ch <= '9') + return (*output) = ch - '0', true; + if (ch >= 'a' && ch <= 'f') + return (*output) = ch - 'a' + 10, true; + if (ch >= 'A' && ch <= 'F') + return (*output) = ch - 'A' + 10, true; + return false; +} + +/* @brief Converts a hex string to an integer +* @param output If not null, the result is stored in this output +* Returns true if the string represents a valid hex value, false otherwise. +*/ +template +bool hex_string_to_int(const char * str, size_t length, TInt* output) { + constexpr size_t N_DIGITS = hex_digits(); + TInt result = 0; + if (length > N_DIGITS) + length = N_DIGITS; + for (size_t i = 0; i < length && str[i]; i++) { + uint8_t digit = 0; + if (!hex_digit_to_byte(str[i], &digit)) + return false; + result <<= 4; + result += digit; + } + if (output) + *output = result; + return true; +} + +template +bool hex_string_to_int(const char * str, TInt* output) { + return hex_string_to_int(str, hex_digits(), output); +} + +template +bool hex_string_to_int_arr(const char * str, size_t length, TInt (&output)[ICount]) { + for (size_t i = 0; i < ICount; i++) { + if (!hex_string_to_int(&str[i * hex_digits()], &output[i])) + return false; + } + return true; +} + +template +bool hex_string_to_int_arr(const char * str, TInt (&output)[ICount]) { + return hex_string_to_int_arr(str, hex_digits() * ICount, output); +} + +namespace fibre { + +// TODO: move to print_utils.hpp +template +class HexPrinter { +public: + HexPrinter(T val, bool prefix) : val_(val) /*, prefix_(prefix)*/ { + const char digits[] = "0123456789abcdef"; + size_t prefix_length = prefix ? 2 : 0; + if (prefix) { + str[0] = '0'; + str[1] = 'x'; + } + str[prefix_length + hex_digits()] = '\0'; + + for (size_t i = 0; i < hex_digits(); ++i) { + str[prefix_length + hex_digits() - i - 1] = digits[val & 0xf]; + val >>= 4; + } + } + std::string to_string() const { return str; } + void to_string(char* buf) const { + for (size_t i = 0; (i < sizeof(str)) && str[i]; ++i) + buf[i] = str[i]; + } + + T val_; + //bool prefix_; + char str[hex_digits() + 3]; // 3 additional characters 0x and \0 +}; + +template +std::ostream& operator<<(std::ostream& stream, const HexPrinter& printer) { + // TODO: specialize for char + return stream << printer.to_string(); +} + +template +HexPrinter as_hex(T val, bool prefix = true) { return HexPrinter(val, prefix); } + +template +class HexArrayPrinter { +public: + HexArrayPrinter(T* ptr, size_t length) : ptr_(ptr), length_(length) {} + T* ptr_; + size_t length_; +}; + +template +std::ostream& operator<<(std::ostream& stream, const HexArrayPrinter& printer) { + for (size_t pos = 0; pos < printer.length_; ++pos) { + stream << " " << as_hex(printer.ptr_[pos]); + if (((pos + 1) % 16) == 0) + stream << std::endl; + } + return stream; +} + +template +HexArrayPrinter as_hex(T (&val)[ILength]) { return HexArrayPrinter(val, ILength); } + +} + + +template +class simple_iterator : std::iterator { + TDereferenceable *container_; + size_t i_; +public: + using reference = TResult; + explicit simple_iterator(TDereferenceable& container, size_t pos) : container_(&container), i_(pos) {} + simple_iterator& operator++() { ++i_; return *this; } + simple_iterator operator++(int) { simple_iterator retval = *this; ++(*this); return retval; } + bool operator==(simple_iterator other) const { return (container_ == other.container_) && (i_ == other.i_); } + bool operator!=(simple_iterator other) const { return !(*this == other); } + bool operator<(simple_iterator other) const { return i_ < other.i_; } + bool operator>(simple_iterator other) const { return i_ > other.i_; } + bool operator<=(simple_iterator other) const { return (*this < other) || (*this == other); } + bool operator>=(simple_iterator other) const { return (*this > other) || (*this == other); } + TResult operator*() const { return (*container_)[i_]; } +}; + + + +/** + * @brief Extracts the argument types of a function signature and provides them + * as a std::tuple. + * TODO: if an STL alternative exists, use that + */ +template +struct args_of; + +template +struct args_of { + using type = std::tuple; +}; + +template +struct args_of> { + using type = std::tuple; +}; + +template +struct args_of { + using type = std::tuple; +}; + +template +struct args_of : public args_of {}; + +template +using args_of_t = typename args_of::type; + +/** + * @brief Extracts the return type of a function signature + * + * This is provided because std::result_of is deprecated since C++17 + */ +template +struct result_of; + +template +struct result_of { + using type = TRet; +}; + +template +struct result_of { + using type = TRet; +}; + +template +struct result_of { + using type = TRet; +}; + +template +using result_of_t = typename result_of::type; + + +/** + * @brief Returns the type that results when concatenating multiple tuples + */ +template +using tuple_cat_t = decltype(std::tuple_cat(std::declval()...)); + +template +constexpr std::array array_cat_impl(std::array arr1, std::array arr2, std::index_sequence, std::index_sequence) { + return { arr1[PACK1]..., arr2[PACK2]... }; +} + +template +constexpr std::array array_cat(std::array arr1, std::array arr2) { + return array_cat_impl(arr1, arr2, std::make_index_sequence(), std::make_index_sequence()); +} + +/** + * @brief Returns the type that results when concatenating multiple tuples + */ +template +using tuple_cat_t = decltype(std::tuple_cat(std::declval()...)); + + +/** + * @brief Ensures that a given type is wrapped in a tuple + */ +template +struct as_tuple { + using type = std::tuple; +}; + +template<> +struct as_tuple { + using type = std::tuple<>; +}; + +template +struct as_tuple> { + using type = std::tuple; +}; + +template +using as_tuple_t = typename as_tuple::type; + +/** + * @brief Removes a reference OR pointer from the given type. + * + * This is similar to std::remove_reference, however it can also remove a + * pointer and it does not work for types that are neither a reference or + * a pointer. + */ +template +struct remove_ref_or_ptr { + static_assert(std::is_reference() || std::is_pointer(), "the type T is neither a reference or a pointer"); +}; + +template +struct remove_ref_or_ptr { using type = T; }; + +template +struct remove_ref_or_ptr { using type = T; }; + +template +using remove_ref_or_ptr_t = typename remove_ref_or_ptr::type; + +/** + * @brief Applies remove_ref_or_ptr_t to every type of a tuple type + */ +template +struct remove_refs_or_ptrs_from_tuple; + +template +struct remove_refs_or_ptrs_from_tuple> { + using type = std::tuple...>; +}; + +template +using remove_refs_or_ptrs_from_tuple_t = typename remove_refs_or_ptrs_from_tuple::type; + +/** + * @brief The convert(val) function returns a reference or a pointer to val + * depending on TTo. + * TODO: this could be a functor + */ +template +struct add_ref_or_ptr; + +template +struct add_ref_or_ptr { + static T& convert(T& value) { + return value; + } +}; + +template +struct add_ref_or_ptr { + static T* convert(T& value) { + return &value; + } +}; + + +/** + * @brief The convert() function turns a given tuple of values into a tuple of + * pointers or references based on the template argument TTo. + */ +template +struct add_ref_or_ptr_to_tuple; + +template +struct add_ref_or_ptr_to_tuple> { + template + static std::tuple convert_impl(std::tuple&& t, std::index_sequence) { + using to_type = std::tuple; + to_type result(add_ref_or_ptr>::convert(std::get(t))...); + return result; + } + + template + static std::tuple convert(std::tuple&& t) { + static_assert(sizeof...(TFrom) == sizeof...(TTo), "both tuples must have the same size"); + return convert_impl(std::forward>(t), std::make_index_sequence()); + } +}; + +template +struct add_ptrs_to_tuple_type; + +template +struct add_ptrs_to_tuple_type> { + using type = std::tuple; +}; + +template +using add_ptrs_to_tuple_t = typename add_ptrs_to_tuple_type::type; + +template +struct add_refs_to_tuple_type; + +template +struct add_refs_to_tuple_type> { + using type = std::tuple; +}; + +template +using add_refs_to_tuple_t = typename add_refs_to_tuple_type::type; + + +template struct is_tuple: std::false_type {}; +template struct is_tuple>: std::true_type {}; + + +template +struct tuple_select_type_impl; + +template +struct tuple_select_type_impl, TTuple> { + using type = std::tuple...>; +}; + +template +typename tuple_select_type_impl, TTuple>::type +tuple_select_impl(TTuple tuple, std::index_sequence) { + return typename tuple_select_type_impl, TTuple>::type(std::get(tuple)...); +}; + + +template +struct tuple_take_type { + static_assert(I <= std::tuple_size::value, "cannot take more elements than tuple size"); + using type = typename tuple_select_type_impl, TTuple>::type; +}; + +template +using tuple_take_t = typename tuple_take_type::type; + +/** + * @brief Returns the first I elements from the tuple as a tuple. + * The resulting type is tuple_take_t. + * See also: tuple_skip + */ +template +tuple_take_t tuple_take(TTuple tuple) { + return tuple_select_impl(tuple, std::make_index_sequence{}); +}; + + +template +struct tuple_skip_type { + static_assert(I <= std::tuple_size::value, "cannot skip more elements than tuple size"); + using type = typename tuple_select_type_impl::value>, TTuple>::type; +}; + +template +using tuple_skip_t = typename tuple_skip_type::type; + +/** + * @brief Returns all but the first I elements from the tuple as a tuple. + * The resulting type is tuple_skip_t. + * See also: tuple_take + */ +template +tuple_skip_t tuple_skip(TTuple tuple) { + return tuple_select_impl(tuple, fibre::make_integer_sequence_from_to::value>{}); +}; + +template +struct repeat_type_impl { + using type = typename repeat_type_impl::type; +}; + +template +struct repeat_type_impl<0, T, Ts...> { + using type = std::tuple; +}; + +template +using repeat_t = typename repeat_type_impl::type; + #endif // __CPP_UTILS_HPP diff --git a/Firmware/fibre/cpp/include/fibre/protocol.hpp b/Firmware/fibre/cpp/include/fibre/protocol.hpp index 883ba544..30a0b8a8 100644 --- a/Firmware/fibre/cpp/include/fibre/protocol.hpp +++ b/Firmware/fibre/cpp/include/fibre/protocol.hpp @@ -13,8 +13,12 @@ see protocol.md for the protocol specification #include //#include #include +#include +#include #include "crc.hpp" #include "cpp_utils.hpp" +#include "bufptr.hpp" +#include "simple_serdes.hpp" // Note that this option cannot be used to debug UART because it prints on UART //#define DEBUG_FIBRE @@ -63,9 +67,6 @@ struct ReceiverState { /*******************************************************/ - -#include - constexpr uint16_t PROTOCOL_VERSION = 1; // This value must not be larger than USB_TX_DATA_SIZE defined in usbd_cdc_if.h @@ -78,11 +79,22 @@ constexpr uint32_t PROTOCOL_SERVER_TIMEOUT_MS = 10; typedef struct { uint16_t json_crc = 0; - uint16_t node_id = 0; uint16_t endpoint_id = 0; } endpoint_ref_t; -#include + +namespace fibre { +// These symbols are defined in the autogenerated endpoints.hpp +extern const unsigned char embedded_json[]; +extern const size_t embedded_json_length; +extern const uint16_t json_crc_; +extern const uint32_t json_version_id_; +bool endpoint_handler(int idx, cbufptr_t* input_buffer, bufptr_t* output_buffer); +bool endpoint0_handler(cbufptr_t* input_buffer, bufptr_t* output_buffer); +bool is_endpoint_ref_valid(endpoint_ref_t endpoint_ref); +bool set_endpoint_from_float(endpoint_ref_t endpoint_ref, float value); +} + template::value>> inline size_t write_le(T value, uint8_t* buffer){ @@ -326,168 +338,65 @@ private: }; -// @brief Endpoint request handler -// -// When passed a valid endpoint context, implementing functions shall handle an -// endpoint read/write request by reading the provided input data and filling in -// output data. The exact semantics of this function depends on the corresponding -// endpoint's specification. -// -// @param input: pointer to the input data -// @param input_length: number of available input bytes -// @param output: The stream where to write the output to. Can be null. -// The handler shall abort as soon as the stream returns -// a non-zero error code on write. -typedef std::function EndpointHandler; - - -// @brief Default endpoint handler for const types -// @return: True if endpoint was written to, False otherwise -template -std::enable_if_t::value && std::is_const::value, bool> -default_readwrite_endpoint_handler(T* value, const uint8_t* input, size_t input_length, StreamSink* output) { - // If the old value was requested, call the corresponding little endian serialization function - if (output) { - // TODO: make buffer size dependent on the type - uint8_t buffer[sizeof(T)]; - size_t cnt = write_le(*value, buffer); - if (cnt <= output->get_free_space()) - output->process_bytes(buffer, cnt, nullptr); - } - return false; // We don't ever write to const types -} - -// @brief Default endpoint handler for non-const types -template -std::enable_if_t::value && !std::is_const::value, bool> -default_readwrite_endpoint_handler(T* value, const uint8_t* input, size_t input_length, StreamSink* output) { - // Read the endpoint value into output - default_readwrite_endpoint_handler(const_cast(value), input, input_length, output); - - // If a new value was passed, call the corresponding little endian deserialization function - uint8_t buffer[sizeof(T)] = { 0 }; // TODO: make buffer size dependent on the type - if (input_length >= sizeof(buffer)) { - read_le(value, input); - return true; - } else { - return false; - } -} - -// @brief Default endpoint handler for endpoint_ref_t types -template -bool default_readwrite_endpoint_handler(endpoint_ref_t* value, const uint8_t* input, size_t input_length, StreamSink* output) { - constexpr size_t size = sizeof(value->endpoint_id) + sizeof(value->json_crc); - if (output) { - // TODO: make buffer size dependent on the type - uint8_t buffer[size]; - size_t cnt = write_leendpoint_id)>(value->endpoint_id, buffer); - cnt += write_lejson_crc)>(value->json_crc, buffer + cnt); - if (cnt <= output->get_free_space()) - output->process_bytes(buffer, cnt, nullptr); - } - - // If a new value was passed, call the corresponding little endian deserialization function - if (input_length >= size) { - read_leendpoint_id)>(&value->endpoint_id, input); - read_lejson_crc)>(&value->json_crc, input + 2); - return true; - } else { - return false; - } -} - -template -static constexpr inline const char* get_default_json_modifier(); - -template<> -inline constexpr const char* get_default_json_modifier() { - return "\"type\":\"float\",\"access\":\"r\""; -} -template<> -inline constexpr const char* get_default_json_modifier() { - return "\"type\":\"float\",\"access\":\"rw\""; -} -template<> -inline constexpr const char* get_default_json_modifier() { - return "\"type\":\"int64\",\"access\":\"r\""; -} -template<> -inline constexpr const char* get_default_json_modifier() { - return "\"type\":\"int64\",\"access\":\"rw\""; -} -template<> -inline constexpr const char* get_default_json_modifier() { - return "\"type\":\"uint64\",\"access\":\"r\""; -} -template<> -inline constexpr const char* get_default_json_modifier() { - return "\"type\":\"uint64\",\"access\":\"rw\""; -} -template<> -inline constexpr const char* get_default_json_modifier() { - return "\"type\":\"int32\",\"access\":\"r\""; -} -template<> -inline constexpr const char* get_default_json_modifier() { - return "\"type\":\"int32\",\"access\":\"rw\""; -} -template<> -inline constexpr const char* get_default_json_modifier() { - return "\"type\":\"uint32\",\"access\":\"r\""; -} -template<> -inline constexpr const char* get_default_json_modifier() { - return "\"type\":\"uint32\",\"access\":\"rw\""; -} -template<> -inline constexpr const char* get_default_json_modifier() { - return "\"type\":\"uint32\",\"access\":\"r\""; // TODO: automatically detect size -} -template<> -inline constexpr const char* get_default_json_modifier() { - return "\"type\":\"uint32\",\"access\":\"rw\""; // TODO: automatically detect size -} -template<> -inline constexpr const char* get_default_json_modifier() { - return "\"type\":\"uint16\",\"access\":\"r\""; -} -template<> -inline constexpr const char* get_default_json_modifier() { - return "\"type\":\"uint16\",\"access\":\"rw\""; -} -template<> -inline constexpr const char* get_default_json_modifier() { - return "\"type\":\"uint8\",\"access\":\"r\""; -} -template<> -inline constexpr const char* get_default_json_modifier() { - return "\"type\":\"uint8\",\"access\":\"rw\""; -} -template<> -inline constexpr const char* get_default_json_modifier() { - return "\"type\":\"bool\",\"access\":\"r\""; -} -template<> -inline constexpr const char* get_default_json_modifier() { - return "\"type\":\"bool\",\"access\":\"rw\""; -} -template<> -inline constexpr const char* get_default_json_modifier() { - return "\"type\":\"endpoint_ref\",\"access\":\"rw\""; -} - -class Endpoint { -public: - //const char* const name_; - virtual void handle(const uint8_t* input, size_t input_length, StreamSink* output) = 0; - virtual bool get_string(char * output, size_t length) { return false; } - virtual bool set_string(char * buffer, size_t length) { return false; } - virtual bool set_from_float(float value) { return false; } +namespace fibre { +template +struct Codec { + static std::optional decode(cbufptr_t* buffer) { return std::nullopt; } }; -static inline int write_string(const char* str, StreamSink* output) { - return output->process_bytes(reinterpret_cast(str), strlen(str), nullptr); +template<> struct Codec { + static std::optional decode(cbufptr_t* buffer) { return (buffer->begin() == buffer->end()) ? std::nullopt : std::make_optional((bool)*(buffer->begin()++)); } + static bool encode(bool value, bufptr_t* buffer) { return SimpleSerializer::write(value, &(buffer->begin()), buffer->end()); } +}; +template<> struct Codec { + static std::optional decode(cbufptr_t* buffer) { return SimpleSerializer::read(&(buffer->begin()), buffer->end()); } + static bool encode(int8_t value, bufptr_t* buffer) { return SimpleSerializer::write(value, &(buffer->begin()), buffer->end()); } +}; +template<> struct Codec { + static std::optional decode(cbufptr_t* buffer) { return SimpleSerializer::read(&(buffer->begin()), buffer->end()); } + static bool encode(uint8_t value, bufptr_t* buffer) { return SimpleSerializer::write(value, &(buffer->begin()), buffer->end()); } +}; +template<> struct Codec { + static std::optional decode(cbufptr_t* buffer) { return SimpleSerializer::read(&(buffer->begin()), buffer->end()); } + static bool encode(int16_t value, bufptr_t* buffer) { return SimpleSerializer::write(value, &(buffer->begin()), buffer->end()); } +}; +template<> struct Codec { + static std::optional decode(cbufptr_t* buffer) { return SimpleSerializer::read(&(buffer->begin()), buffer->end()); } + static bool encode(uint16_t value, bufptr_t* buffer) { return SimpleSerializer::write(value, &(buffer->begin()), buffer->end()); } +}; +template<> struct Codec { + static std::optional decode(cbufptr_t* buffer) { return SimpleSerializer::read(&(buffer->begin()), buffer->end()); } + static bool encode(int32_t value, bufptr_t* buffer) { return SimpleSerializer::write(value, &(buffer->begin()), buffer->end()); } +}; +template<> struct Codec { + static std::optional decode(cbufptr_t* buffer) { return SimpleSerializer::read(&(buffer->begin()), buffer->end()); } + static bool encode(uint32_t value, bufptr_t* buffer) { return SimpleSerializer::write(value, &(buffer->begin()), buffer->end()); } +}; +template<> struct Codec { + static std::optional decode(cbufptr_t* buffer) { return SimpleSerializer::read(&(buffer->begin()), buffer->end()); } + static bool encode(int64_t value, bufptr_t* buffer) { return SimpleSerializer::write(value, &(buffer->begin()), buffer->end()); } +}; +template<> struct Codec { + static std::optional decode(cbufptr_t* buffer) { return SimpleSerializer::read(&(buffer->begin()), buffer->end()); } + static bool encode(uint64_t value, bufptr_t* buffer) { return SimpleSerializer::write(value, &(buffer->begin()), buffer->end()); } +}; +template<> struct Codec { + static std::optional decode(cbufptr_t* buffer) { + std::optional int_val = Codec::decode(buffer); + return int_val.has_value() ? std::optional(*reinterpret_cast(&int_val.value())) : std::nullopt; + } + static bool encode(float value, bufptr_t* buffer) { + return Codec::encode(*reinterpret_cast(&value), buffer); + } +}; +template +struct Codec::value>> { + static std::optional decode(cbufptr_t* buffer) { + std::optional int_val = SimpleSerializer::read(&(buffer->begin()), buffer->end()); + return int_val.has_value() ? std::make_optional(static_cast(int_val.value())) : std::nullopt; + } + static bool encode(T value, bufptr_t* buffer) { return SimpleSerializer::write(value, &(buffer->begin()), buffer->end()); } +}; } @@ -612,112 +521,6 @@ static bool from_string(const char * buffer, size_t length, T* property, ...) { } -/* Object tree ---------------------------------------------------------------*/ - -template -struct MemberList; - -template<> -struct MemberList<> { -public: - static constexpr size_t endpoint_count = 0; - static constexpr bool is_empty = true; - void write_json(size_t id, StreamSink* output) { - // no action - } - void register_endpoints(Endpoint** list, size_t id, size_t length) { - // no action - } - Endpoint* get_by_name(const char * name, size_t length) { - return nullptr; - } - std::tuple<> get_names_as_tuple() const { return std::tuple<>(); } -}; - -template -struct MemberList { -public: - static constexpr size_t endpoint_count = TMember::endpoint_count + MemberList::endpoint_count; - static constexpr bool is_empty = false; - - MemberList(TMember&& this_member, TMembers&&... subsequent_members) : - this_member_(std::forward(this_member)), - subsequent_members_(std::forward(subsequent_members)...) {} - - MemberList(TMember&& this_member, MemberList&& subsequent_members) : - this_member_(std::forward(this_member)), - subsequent_members_(std::forward>(subsequent_members)) {} - - // @brief Move constructor -/* MemberList(MemberList&& other) : - this_member_(std::move(other.this_member_)), - subsequent_members_(std::move(other.subsequent_members_)) {}*/ - - void write_json(size_t id, StreamSink* output) /*final*/ { - this_member_.write_json(id, output); - if (!MemberList::is_empty) - write_string(",", output); - subsequent_members_.write_json(id + TMember::endpoint_count, output); - } - - Endpoint* get_by_name(const char * name, size_t length) { - Endpoint* result = this_member_.get_by_name(name, length); - if (result) return result; - else return subsequent_members_.get_by_name(name, length); - } - - void register_endpoints(Endpoint** list, size_t id, size_t length) /*final*/ { - this_member_.register_endpoints(list, id, length); - subsequent_members_.register_endpoints(list, id + TMember::endpoint_count, length); - } - - TMember this_member_; - MemberList subsequent_members_; -}; - -template -MemberList make_protocol_member_list(TMembers&&... member_list) { - return MemberList(std::forward(member_list)...); -} - -template -class ProtocolObject { -public: - ProtocolObject(const char * name, TMembers&&... member_list) : - name_(name), - member_list_(std::forward(member_list)...) {} - - static constexpr size_t endpoint_count = MemberList::endpoint_count; - - void write_json(size_t id, StreamSink* output) { - write_string("{\"name\":\"", output); - write_string(name_, output); - write_string("\",\"type\":\"object\",\"members\":[", output); - member_list_.write_json(id, output), - write_string("]}", output); - } - - Endpoint* get_by_name(const char * name, size_t length) { - size_t segment_length = strlen(name); - if (!strncmp(name, name_, length)) - return member_list_.get_by_name(name + segment_length + 1, length - segment_length - 1); - else - return nullptr; - } - - void register_endpoints(Endpoint** list, size_t id, size_t length) { - member_list_.register_endpoints(list, id, length); - } - - const char * name_; - MemberList member_list_; -}; - -template -ProtocolObject make_protocol_object(const char * name, TMembers&&... member_list) { - return ProtocolObject(name, std::forward(member_list)...); -} - //template //bool set_from_float_ex(float value, T* property) { // return false; @@ -747,396 +550,39 @@ bool set_from_float(float value, T* property) { } } -//template -//bool set_from_float_ex<>(float value, T* property) { -// return false; -//} - -template -class ProtocolProperty : public Endpoint { -public: - static constexpr const char * json_modifier = get_default_json_modifier(); - static constexpr size_t endpoint_count = 1; - - ProtocolProperty(const char * name, TProperty* property, - void (*written_hook)(void*), void* ctx) - : name_(name), property_(property), written_hook_(written_hook), ctx_(ctx) - {} - -/* TODO: find out why the move constructor is not used when it could be - ProtocolProperty(const ProtocolProperty&) = delete; - // @brief Move constructor - ProtocolProperty(ProtocolProperty&& other) : - Endpoint(std::move(other)), - name_(std::move(other.name_)), - property_(other.property_) - {} - constexpr ProtocolProperty& operator=(const ProtocolProperty& other) = delete; - constexpr ProtocolProperty& operator=(const ProtocolProperty& other) { - //Endpoint(std::move(other)), - //name_(std::move(other.name_)), - //property_(other.property_) - name_ = other.name_; - property_ = other.property_; - return *this; - } - ProtocolProperty& operator=(ProtocolProperty&& other) - : name_(other.name_), property_(other.property_) - {} - ProtocolProperty& operator=(const ProtocolProperty& other) - : name_(other.name_), property_(other.property_) - {}*/ - - void write_json(size_t id, StreamSink* output) { - // write name - write_string("{\"name\":\"", output); - LOG_FIBRE("json: this at %x, name at %x is s\r\n", (uintptr_t)this, (uintptr_t)name_); - //LOG_FIBRE("json\r\n"); - write_string(name_, output); - - // write endpoint ID - write_string("\",\"id\":", output); - char id_buf[10]; - snprintf(id_buf, sizeof(id_buf), "%u", (unsigned)id); // TODO: get rid of printf - write_string(id_buf, output); - - // write additional JSON data - if (json_modifier && json_modifier[0]) { - write_string(",", output); - write_string(json_modifier, output); - } - - write_string("}", output); - } - - // special-purpose function - to be moved - Endpoint* get_by_name(const char * name, size_t length) { - if (!strncmp(name, name_, length)) - return this; - else - return nullptr; - } - - // special-purpose function - to be moved - bool get_string(char * buffer, size_t length) final { - return to_string(*property_, buffer, length, 0); - } - - // special-purpose function - to be moved - bool set_string(char * buffer, size_t length) final { - return from_string(buffer, length, property_, 0); - } - - bool set_from_float(float value) final { - return conversion::set_from_float(value, property_); - } - - void register_endpoints(Endpoint** list, size_t id, size_t length) { - if (id < length) - list[id] = this; - } - void handle(const uint8_t* input, size_t input_length, StreamSink* output) final { - bool wrote = default_readwrite_endpoint_handler(property_, input, input_length, output); - if (wrote && written_hook_ != nullptr) { - written_hook_(ctx_); - } - } - /*void handle(const uint8_t* input, size_t input_length, StreamSink* output) { - handle(input, input_length, output); - }*/ - - const char* name_; - TProperty* property_; - void (*written_hook_)(void*); +template +struct Property { + Property(void* ctx, T(*getter)(void*), void(*setter)(void*, T)) + : ctx_(ctx), getter_(getter), setter_(setter) {} + Property* operator->() { return this; } + void* ctx_; -}; + T(*getter_)(void*); + void(*setter_)(void*, T); -// Non-const non-enum types -template::value)> -ProtocolProperty make_protocol_property(const char * name, TProperty* property, - void (*written_hook)(void*) = nullptr, void* ctx = nullptr) { - return ProtocolProperty(name, property, written_hook, ctx); -}; - -// Const non-enum types -template::value)> -ProtocolProperty make_protocol_ro_property(const char * name, TProperty* property, - void (*written_hook)(void*) = nullptr, void* ctx = nullptr) { - return ProtocolProperty(name, property, written_hook, ctx); -}; - -// Non-const enum types -template::value)> -ProtocolProperty> make_protocol_property(const char * name, TProperty* property, - void (*written_hook)(void*) = nullptr, void* ctx = nullptr) { - return ProtocolProperty>( - name, reinterpret_cast*>(property), written_hook, ctx); -}; - -// Const enum types -template::value)> -ProtocolProperty> make_protocol_ro_property(const char * name, TProperty* property, - void (*written_hook)(void*) = nullptr, void* ctx = nullptr) { - return ProtocolProperty>( - name, reinterpret_cast*>(property), written_hook, ctx); -}; - - -template -struct PropertyListFactory; - -template<> -struct PropertyListFactory<> { - template - static MemberList<> make_property_list(std::array names, std::tuple& values) { - return MemberList<>(); - } -}; - -template -struct PropertyListFactory { - template - static MemberList, ProtocolProperty...> - make_property_list(std::array names, std::tuple& values) { - return MemberList, ProtocolProperty...>( - make_protocol_property(std::get(names), &std::get(values)), - PropertyListFactory::template make_property_list(names, values) - ); - } -}; - -/* @brief return_type::type represents the true return type -* of a function returning 0 or more arguments. -* -* For an empty TypeList, the return type is void. For a list with -* one type, the return type is equal to that type. For a list with -* more than one items, the return type is a tuple. -*/ -template -struct return_type; - -template<> -struct return_type<> { typedef void type; }; -template -struct return_type { typedef T type; }; -template -struct return_type { typedef std::tuple type; }; - - -template -class ProtocolFunction; - -template -class ProtocolFunction, std::tuple> : Endpoint { -public: - // @brief The return type of the function as written by a C++ programmer - using TRet = typename return_type::type; - - static constexpr size_t endpoint_count = 1 + MemberList...>::endpoint_count + MemberList...>::endpoint_count; - - ProtocolFunction(const char * name, TObj& obj, TRet(TObj::*func_ptr)(TInputs...), - std::array input_names, - std::array output_names) : - name_(name), obj_(&obj), func_ptr_(func_ptr), - input_names_{input_names}, output_names_{output_names}, - input_properties_(PropertyListFactory::template make_property_list<0>(input_names_, in_args_)), - output_properties_(PropertyListFactory::template make_property_list<0>(output_names_, out_args_)) - { - LOG_FIBRE("my tuple is at %x and of size %u\r\n", (uintptr_t)&in_args_, sizeof(in_args_)); - } - - // The custom copy constructor is needed because otherwise the - // input_properties_ and output_properties_ would point to memory - // locations of the old object. - ProtocolFunction(const ProtocolFunction& other) : - name_(other.name_), obj_(other.obj_), func_ptr_(other.func_ptr_), - input_names_{other.input_names_}, output_names_{other.output_names_}, - input_properties_(PropertyListFactory::template make_property_list<0>(input_names_, in_args_)), - output_properties_(PropertyListFactory::template make_property_list<0>(output_names_, out_args_)) - { - LOG_FIBRE("COPIED! my tuple is at %x and of size %u\r\n", (uintptr_t)&in_args_, sizeof(in_args_)); - } - - void write_json(size_t id, StreamSink* output) { - // write name - write_string("{\"name\":\"", output); - write_string(name_, output); - - // write endpoint ID - write_string("\",\"id\":", output); - char id_buf[10]; - snprintf(id_buf, sizeof(id_buf), "%u", (unsigned)id); // TODO: get rid of printf - write_string(id_buf, output); - - // write arguments - write_string(",\"type\":\"function\",\"inputs\":[", output); - input_properties_.write_json(id + 1, output), - write_string("],\"outputs\":[", output); - output_properties_.write_json(id + 1 + decltype(input_properties_)::endpoint_count, output), - write_string("]}", output); - } - - // special-purpose function - to be moved - Endpoint* get_by_name(const char * name, size_t length) { - return nullptr; // can't address functions by name - } - - void register_endpoints(Endpoint** list, size_t id, size_t length) { - if (id < length) - list[id] = this; - input_properties_.register_endpoints(list, id + 1, length); - output_properties_.register_endpoints(list, id + 1 + decltype(input_properties_)::endpoint_count, length); - } - - template std::enable_if_t - handle_ex() { - invoke_function_with_tuple(*obj_, func_ptr_, in_args_); - } - - template std::enable_if_t - handle_ex() { - std::get<0>(out_args_) = invoke_function_with_tuple(*obj_, func_ptr_, in_args_); - } - - template std::enable_if_t= 2> - handle_ex() { - out_args_ = invoke_function_with_tuple(*obj_, func_ptr_, in_args_); - } - - void handle(const uint8_t* input, size_t input_length, StreamSink* output) final { - (void) input; - (void) input_length; - (void) output; - LOG_FIBRE("tuple still at %x and of size %u\r\n", (uintptr_t)&in_args_, sizeof(in_args_)); - LOG_FIBRE("invoke function using %d and %.3f\r\n", std::get<0>(in_args_), std::get<1>(in_args_)); - handle_ex(); - } - - const char * name_; - TObj* obj_; - TRet(TObj::*func_ptr_)(TInputs...); - std::array input_names_; // TODO: remove - std::array output_names_; // TODO: remove - std::tuple in_args_; - std::tuple out_args_; - MemberList...> input_properties_; - MemberList...> output_properties_; -}; - -template> -ProtocolFunction, std::tuple<>> make_protocol_function(const char * name, TObj& obj, void(TObj::*func_ptr)(TArgs...), TNames ... names) { - return ProtocolFunction, std::tuple<>>(name, obj, func_ptr, {names...}, {}); -} - -template::value>> -ProtocolFunction, std::tuple> make_protocol_function(const char * name, TObj& obj, TRet(TObj::*func_ptr)(TArgs...), TNames ... names) { - return ProtocolFunction, std::tuple>(name, obj, func_ptr, {names...}, {"result"}); -} - - -#define FIBRE_EXPORTS(CLASS, ...) \ - struct fibre_export_t { \ - static CLASS* obj; \ - using type = decltype(make_protocol_member_list(__VA_ARGS__)); \ - }; \ - fibre_export_t::type make_fibre_definitions() { \ - CLASS* obj = this; \ - return make_protocol_member_list(__VA_ARGS__); \ - } \ - fibre_export_t::type fibre_definitions = make_fibre_definitions() - - - - - -class EndpointProvider { -public: - virtual size_t get_endpoint_count() = 0; - virtual void write_json(size_t id, StreamSink* output) = 0; - virtual Endpoint* get_by_name(char * name, size_t length) = 0; - virtual void register_endpoints(Endpoint** list, size_t id, size_t length) = 0; -}; - -template -class EndpointProvider_from_MemberList : public EndpointProvider { -public: - EndpointProvider_from_MemberList(T& member_list) : member_list_(member_list) {} - size_t get_endpoint_count() final { - return T::endpoint_count; - } - void write_json(size_t id, StreamSink* output) final { - return member_list_.write_json(id, output); - } - void register_endpoints(Endpoint** list, size_t id, size_t length) final { - return member_list_.register_endpoints(list, id, length); - } - Endpoint* get_by_name(char * name, size_t length) final { - for (size_t i = 0; i < length; i++) { - if (name[i] == '.') - name[i] = 0; + T exchange(std::optional value) { + T old_value = (*getter_)(ctx_); + if (value.has_value()) { + (*setter_)(ctx_, value.value()); } - name[length-1] = 0; - return member_list_.get_by_name(name, length); + return old_value; } - T& member_list_; }; - - -class JSONDescriptorEndpoint : Endpoint { -public: - static constexpr size_t endpoint_count = 1; - void write_json(size_t id, StreamSink* output); - void register_endpoints(Endpoint** list, size_t id, size_t length); - void handle(const uint8_t* input, size_t input_length, StreamSink* output); -}; - -// defined in protocol.cpp -extern Endpoint** endpoint_list_; -extern size_t n_endpoints_; -extern uint16_t json_crc_; -extern uint32_t json_version_id_; // exposed to hosts to facilitate cache lookup -extern JSONDescriptorEndpoint json_file_endpoint_; -extern EndpointProvider* application_endpoints_; - -bool is_endpoint_ref_valid(endpoint_ref_t endpoint_ref); -Endpoint* get_endpoint(endpoint_ref_t endpoint_ref); - -// @brief Registers the specified application object list using the provided endpoint table. -// This function should only be called once during the lifetime of the application. TODO: fix this. -// @param application_objects The application objects to be registred. template -int fibre_publish(T& application_objects) { - static constexpr size_t endpoint_list_size = 1 + T::endpoint_count; - static Endpoint* endpoint_list[endpoint_list_size]; - static auto endpoint_provider = EndpointProvider_from_MemberList(application_objects); - - json_file_endpoint_.register_endpoints(endpoint_list, 0, endpoint_list_size); - application_objects.register_endpoints(endpoint_list, 1, endpoint_list_size); - - // Update the global endpoint table - endpoint_list_ = endpoint_list; - n_endpoints_ = endpoint_list_size; - application_endpoints_ = &endpoint_provider; +struct Property { + Property(void* ctx, T(*getter)(void*)) + : ctx_(ctx), getter_(getter) {} + Property* operator->() { return this; } - // Calculate the CRC16 of the JSON file. - // The init value is the protocol version. - CRC16Calculator crc16_calculator(PROTOCOL_VERSION); + void* ctx_; + T(*getter_)(void*); - uint8_t offset[4] = { 0 }; - json_file_endpoint_.handle(offset, sizeof(offset), &crc16_calculator); - json_crc_ = crc16_calculator.get_crc16(); - - // Add entropy for fibre cache - json_file_endpoint_.handle(offset, sizeof(offset), &crc16_calculator); - json_version_id_ = (uint32_t) crc16_calculator.get_crc16(); - json_version_id_ += json_crc_ << 16; - - return 0; -} + T read() { + return (*getter_)(ctx_); + } +}; #endif diff --git a/Firmware/fibre/cpp/include/fibre/simple_serdes.hpp b/Firmware/fibre/cpp/include/fibre/simple_serdes.hpp new file mode 100644 index 00000000..32c09f27 --- /dev/null +++ b/Firmware/fibre/cpp/include/fibre/simple_serdes.hpp @@ -0,0 +1,77 @@ +#ifndef __FIBRE_SIMPLE_SERDES +#define __FIBRE_SIMPLE_SERDES + +//#include "stream.hpp" + + +template +struct SimpleSerializer; +template +using LittleEndianSerializer = SimpleSerializer; +template +using BigEndianSerializer = SimpleSerializer; + + +/* @brief Serializer/deserializer for arbitrary integral number types */ +// TODO: allow reading an arbitrary number of bits +template +struct SimpleSerializer::value>> { + static constexpr size_t BIT_WIDTH = std::numeric_limits::digits; + static constexpr size_t BYTE_WIDTH = (BIT_WIDTH + 7) / 8; + + template + static std::optional read(TIterator* begin, TIterator end = nullptr) { + T result = 0; + if (BigEndian) { + for (size_t i = BYTE_WIDTH; i > 0; (i++, (*begin)++)) { + if (end && !(*begin < end)) + return std::nullopt; + uint8_t byte = **begin; + result |= static_cast(byte) << ((i - 1) << 3); + } + } else { + for (size_t i = 0; i < BYTE_WIDTH; (i++, (*begin)++)) { + if (end && !(*begin < end)) + return std::nullopt; + uint8_t byte = **begin; + result |= static_cast(byte) << (i << 3); + } + } + return result; + } + + template + static bool write(T value, TIterator* begin, TIterator end = nullptr) { + if (BigEndian) { + for (size_t i = BYTE_WIDTH; i > 0; (i--, (*begin)++)) { + if (end && !(*begin < end)) + return false; + uint8_t byte = static_cast((value >> ((i - 1) << 3)) & 0xff); + **begin = byte; + } + } else { + for (size_t i = 0; i < BYTE_WIDTH; (i++, (*begin)++)) { + if (end && !(*begin < end)) + return false; + uint8_t byte = static_cast((value >> (i << 3)) & 0xff); + **begin = byte; + } + } + return true; + } +}; + +template +inline std::optional read_le(fibre::cbufptr_t* buffer) { + static_assert(is_complete>(), "no LittleEndianSerializer is defined for type T"); + return LittleEndianSerializer::read(&buffer->begin(), buffer->end()); +} + +template +inline bool write_le(T value, fibre::bufptr_t* buffer) { + static_assert(is_complete>(), "no LittleEndianSerializer is defined for type T"); + return LittleEndianSerializer::write(value, &buffer->begin(), buffer->end()); +} + + +#endif \ No newline at end of file diff --git a/Firmware/fibre/cpp/interfaces_template.j2 b/Firmware/fibre/cpp/interfaces_template.j2 new file mode 100644 index 00000000..76e0c90d --- /dev/null +++ b/Firmware/fibre/cpp/interfaces_template.j2 @@ -0,0 +1,67 @@ +/*[# This is the original template, thus the warning below does not apply to this file #] + * ============================ WARNING ============================ + * ==== This is an autogenerated file. ==== + * ==== Any changes to this file will be lost when recompiling. ==== + * ================================================================= + * + * This file contains base classes that correspond to the interfaces defined in + * your interface file. The objects you publish should inherit from these + * interfaces. + * + */ + +[%- macro rettype(func) %] +[%- if not func.out -%] +void +[%- elif func.out | length == 1 -%] +[[(func.out.values() | first).type.c_type]] +[%- else -%] +[% for arg in func.out.values() %][[arg.type]][[', ' if not loop.last]][% endfor %] +[%- endif -%] +[%- endmacro %] + +[%- macro render_interface(intf) %] +class [[intf.name | to_pascal_case]]Intf { +public: +[%- for intf in intf.interfaces -%] +[[render_interface(intf) | indent(4)]] +[%- endfor %] +[%- for enum in intf.enums %] + enum [[enum.name | to_pascal_case]] { +[%- for k, value in enum['values'].items() %] + [[((enum.name + k) | to_macro_case).ljust(32)]] = [% if enum.is_flags %]0x[['%08x' | format(value.value)]][% else %][[value.value]][% endif %], +[%- endfor %] + }; +[%- endfor %] +[%- for func in intf.functions.values() %] + virtual [[rettype(func)]] [[func.name | to_snake_case]]([% for in in func.in.values() %][% if loop.index0 %][[in.type.c_type]] [[in.name]][[', ' if not loop.last]][% endif %][% endfor %]) = 0; +[%- endfor %] +[%- for func in intf.functions.values() %] +[%- for k, arg in func.in.items() | skip_first %] + [[arg.type.c_type]] [[func.name | to_snake_case]]_in_[[arg.name]]_; // for internal use by Fibre +[%- endfor %] +[%- for k, arg in func.out.items() %] + [[arg.type.c_type]] [[func.name | to_snake_case]]_out_[[arg.name]]_; // for internal use by Fibre +[%- endfor %] +[%- endfor %] +}; +[%- endmacro %] + +[% for intf in toplevel_interfaces %] +[[render_interface(intf)]] +[% endfor %] + +[%- for _, enum in value_types.items() %] +[%- if enum.is_flags %] +// this is technically not thread-safe but practically it might be +inline [[enum.c_type]] operator | ([[enum.c_type]] a, [[enum.c_type]] b) { return static_cast<[[enum.c_type]]>(static_cast>(a) | static_cast>(b)); } +inline [[enum.c_type]] operator & ([[enum.c_type]] a, [[enum.c_type]] b) { return static_cast<[[enum.c_type]]>(static_cast>(a) & static_cast>(b)); } +inline [[enum.c_type]] operator ^ ([[enum.c_type]] a, [[enum.c_type]] b) { return static_cast<[[enum.c_type]]>(static_cast>(a) ^ static_cast>(b)); } +inline [[enum.c_type]]& operator |= ([[enum.c_type]] &a, [[enum.c_type]] b) { return reinterpret_cast<[[enum.c_type]]&>(reinterpret_cast&>(a) |= static_cast>(b)); } +inline [[enum.c_type]]& operator &= ([[enum.c_type]] &a, [[enum.c_type]] b) { return reinterpret_cast<[[enum.c_type]]&>(reinterpret_cast&>(a) &= static_cast>(b)); } +inline [[enum.c_type]]& operator ^= ([[enum.c_type]] &a, [[enum.c_type]] b) { return reinterpret_cast<[[enum.c_type]]&>(reinterpret_cast&>(a) ^= static_cast>(b)); } +inline [[enum.c_type]] operator ~ ([[enum.c_type]] a) { return static_cast<[[enum.c_type]]>(~static_cast>(a)); } +[%- endif %] +[%- endfor %] + + diff --git a/Firmware/fibre/cpp/protocol.cpp b/Firmware/fibre/cpp/protocol.cpp index d5af8a0b..e8285c87 100644 --- a/Firmware/fibre/cpp/protocol.cpp +++ b/Firmware/fibre/cpp/protocol.cpp @@ -13,19 +13,11 @@ /* Global constant data ------------------------------------------------------*/ /* Global variables ----------------------------------------------------------*/ -Endpoint** endpoint_list_ = nullptr; // initialized by calling fibre_publish -size_t n_endpoints_ = 0; // initialized by calling fibre_publish -uint16_t json_crc_; // initialized by calling fibre_publish -uint32_t json_version_id_; // initialized by calling fibre_publish -JSONDescriptorEndpoint json_file_endpoint_ = JSONDescriptorEndpoint(); -EndpointProvider* application_endpoints_; - /* Private constant data -----------------------------------------------------*/ /* Private variables ---------------------------------------------------------*/ /* Private function prototypes -----------------------------------------------*/ static void hexdump(const uint8_t* buf, size_t len); -static inline int write_string(const char* str, StreamSink* output); /* Function implementations --------------------------------------------------*/ @@ -116,45 +108,26 @@ int StreamBasedPacketSink::process_packet(const uint8_t *buffer, size_t length) } - -void JSONDescriptorEndpoint::write_json(size_t id, StreamSink* output) { - write_string("{\"name\":\"\",", output); - - // write endpoint ID - write_string("\"id\":", output); - char id_buf[10]; - snprintf(id_buf, sizeof(id_buf), "%u", (unsigned)id); // TODO: get rid of printf - write_string(id_buf, output); - - write_string(",\"type\":\"json\",\"access\":\"r\"}", output); -} - -void JSONDescriptorEndpoint::register_endpoints(Endpoint** list, size_t id, size_t length) { - if (id < length) - list[id] = this; -} - // Returns part of the JSON interface definition. -void JSONDescriptorEndpoint::handle(const uint8_t* input, size_t input_length, StreamSink* output) { +bool fibre::endpoint0_handler(fibre::cbufptr_t* input_buffer, fibre::bufptr_t* output_buffer) { // The request must contain a 32 bit integer to specify an offset - if (input_length < 4) - return; - uint32_t offset = 0; - read_le(&offset, input); - - // If the offset is special value 0xFFFFFFFF, send back the JSON version ID instead - if (offset == 0xffffffff) { - default_readwrite_endpoint_handler(&json_version_id_, nullptr, 0, output); + std::optional offset = read_le(input_buffer); + + if (!offset.has_value()) { + // Didn't receive any offset + return false; + } else if (offset.value() == 0xffffffff) { + // If the offset is special value 0xFFFFFFFF, send back the JSON version ID instead + return write_le(json_version_id_, output_buffer); + } else if (offset.value() >= embedded_json_length) { + // Attempt to read beyond the buffer end - return empty response + return true; } else { - NullStreamSink output_with_offset = NullStreamSink(offset, *output); - - size_t id = 0; - write_string("[", &output_with_offset); - json_file_endpoint_.write_json(id, &output_with_offset); - id += decltype(json_file_endpoint_)::endpoint_count; - write_string(",", &output_with_offset); - application_endpoints_->write_json(id, &output_with_offset); - write_string("]", &output_with_offset); + // Return part of the json file + size_t n_copy = std::min(output_buffer->size(), embedded_json_length - (size_t)offset.value()); + memcpy(output_buffer->begin(), embedded_json + offset.value(), n_copy); + *output_buffer = output_buffer->skip(n_copy); + return true; } } @@ -176,19 +149,10 @@ int BidirectionalPacketBasedChannel::process_packet(const uint8_t* buffer, size_ bool expect_response = endpoint_id & 0x8000; endpoint_id &= 0x7fff; - if (endpoint_id >= n_endpoints_) - return -1; - - Endpoint* endpoint = endpoint_list_[endpoint_id]; - if (!endpoint) { - LOG_FIBRE("critical: no endpoint at %d", endpoint_id); - return -1; - } - // Verify packet trailer. The expected trailer value depends on the selected endpoint. // For endpoint 0 this is just the protocol version, for all other endpoints it's a // CRC over the entire JSON descriptor tree (this may change in future versions). - uint16_t expected_trailer = endpoint_id ? json_crc_ : PROTOCOL_VERSION; + uint16_t expected_trailer = endpoint_id ? fibre::json_crc_ : PROTOCOL_VERSION; uint16_t actual_trailer = buffer[length - 2] | (buffer[length - 1] << 8); if (expected_trailer != actual_trailer) { LOG_FIBRE("trailer mismatch for endpoint %d: expected %04x, got %04x\r\n", endpoint_id, expected_trailer, actual_trailer); @@ -204,12 +168,13 @@ int BidirectionalPacketBasedChannel::process_packet(const uint8_t* buffer, size_ if (expected_response_length > sizeof(tx_buf_) - 2) expected_response_length = sizeof(tx_buf_) - 2; - MemoryStreamSink output(tx_buf_ + 2, expected_response_length); - endpoint->handle(buffer, length - 2, &output); + fibre::cbufptr_t input_buffer{buffer, length - 2}; + fibre::bufptr_t output_buffer{tx_buf_ + 2, expected_response_length}; + fibre::endpoint_handler(endpoint_id, &input_buffer, &output_buffer); // Send response if (expect_response) { - size_t actual_response_length = expected_response_length - output.get_free_space() + 2; + size_t actual_response_length = expected_response_length - output_buffer.size() + 2; write_le(seq_no | 0x8000, tx_buf_); LOG_FIBRE("send packet:\r\n"); @@ -220,15 +185,3 @@ int BidirectionalPacketBasedChannel::process_packet(const uint8_t* buffer, size_ return 0; } - -bool is_endpoint_ref_valid(endpoint_ref_t endpoint_ref) { - return (endpoint_ref.json_crc == json_crc_) - && (endpoint_ref.endpoint_id < n_endpoints_); -} - -Endpoint* get_endpoint(endpoint_ref_t endpoint_ref) { - if (is_endpoint_ref_valid(endpoint_ref)) - return endpoint_list_[endpoint_ref.endpoint_id]; - else - return nullptr; -} diff --git a/Firmware/interface_generator.py b/Firmware/interface_generator.py new file mode 100644 index 00000000..38468036 --- /dev/null +++ b/Firmware/interface_generator.py @@ -0,0 +1,589 @@ +#!/bin/python3 + +import yaml +import json +import jinja2 +import jsonschema +import re +import argparse +import sys + +# This schema describes what we expect interface definition files to look like +validator = jsonschema.Draft7Validator(yaml.safe_load(""" +definitions: + interface: + type: object + properties: + c_is_class: {type: boolean} + c_name: {type: string} + functions: + type: object + additionalProperties: {"$ref": "#/definitions/function"} + attributes: + type: object + additionalProperties: {"$ref": "#/definitions/attribute"} + __line__: {type: object} + __column__: {type: object} + additionalProperties: false + + valuetype: + type: object + properties: + mode: {type: string} # this shouldn't be here + c_name: {type: string} + values: {type: object} + flags: {type: object} + nullflag: {type: string} + __line__: {type: object} + __column__: {type: object} + additionalProperties: false + + intf_or_val_type: + anyOf: + - {"$ref": "#/definitions/interface"} + - {"$ref": "#/definitions/valuetype"} + - {"type": "string"} + + attribute: + anyOf: # this is probably not being used correctly + - {"$ref": "#/definitions/intf_or_val_type"} + - type: object + - type: object + properties: + type: {"$ref": "#/definitions/intf_or_val_type"} + c_name: {"type": string} + unit: {"type": string} + doc: {"type": string} + additionalProperties: false + + function: + anyOf: + - type: 'null' + - type: object + properties: + in: {type: object} + out: {type: object} + doc: {type: string} + __line__: {type: object} + __column__: {type: object} + additionalProperties: false + + +type: object +properties: + ns: {type: string} + version: {type: string} + summary: {type: string} + interfaces: + type: object + additionalProperties: { "$ref": "#/definitions/interface" } + valuetypes: + type: object + additionalProperties: { "$ref": "#/definitions/valuetype" } + __line__: {type: object} + __column__: {type: object} +additionalProperties: false +""")) + +# Source: https://stackoverflow.com/a/53647080/3621512 +class SafeLineLoader(yaml.SafeLoader): + pass +# def compose_node(self, parent, index): +# # the line number where the previous token has ended (plus empty lines) +# line = self.line +# node = super(SafeLineLoader, self).compose_node(parent, index) +# node.__line__ = line + 1 +# return node +# +# def construct_mapping(self, node, deep=False): +# mapping = super(SafeLineLoader, self).construct_mapping(node, deep=deep) +# mapping['__line__'] = node.__line__ +# #mapping['__column__'] = node.start_mark.column + 1 +# return mapping + + + +def get_words(string): + """ + Splits a string in PascalCase into a list of lower case words + """ + return [w.lower() for w in re.findall('[a-z0-9]+|[A-Z][a-z0-9]*', string)] + +def join_name(*names, delimiter: str = '.'): + """ + Joins two name components. + e.g. 'io.helloworld' + 'sayhello' => 'io.helloworld.sayhello' + """ + return delimiter.join(y for x in names for y in x.split(delimiter) if y != '') + +def split_name(name, delimiter: str = '.'): + def replace_delimiter_in_parentheses(): + parenthesis_depth = 0 + for c in name: + parenthesis_depth += 1 if c == '<' else -1 if c == '>' else 0 + yield c if (parenthesis_depth == 0) or (c != delimiter) else ':' + return [part.replace(':', '.') for part in ''.join(replace_delimiter_in_parentheses()).split('.')] + +def to_pascal_case(s): return ''.join([w.title() for w in get_words(s)]) +def to_camel_case(s): return ''.join([(c.lower() if i == 0 else c) for i, c in enumerate(''.join([w.title() for w in get_words(s)]))]) +def to_macro_case(s): return '_'.join(get_words(s)).upper() +def to_snake_case(s): return '_'.join(get_words(s)).lower() +def to_kebab_case(s): return '-'.join(get_words(s)).lower() + +value_types = { + 'bool': {'builtin': True, 'fullname': 'bool', 'name': 'bool', 'c_type': 'bool'}, + 'float32': {'builtin': True, 'fullname': 'float32', 'name': 'float32', 'c_type': 'float'}, + 'uint8': {'builtin': True, 'fullname': 'uint8', 'name': 'uint8', 'c_type': 'uint8_t'}, + 'uint16': {'builtin': True, 'fullname': 'uint16', 'name': 'uint16', 'c_type': 'uint16_t'}, + 'uint32': {'builtin': True, 'fullname': 'uint32', 'name': 'uint32', 'c_type': 'uint32_t'}, + 'uint64': {'builtin': True, 'fullname': 'uint64', 'name': 'uint64', 'c_type': 'uint64_t'}, + 'int8': {'builtin': True, 'fullname': 'int8', 'name': 'int8', 'c_type': 'int8_t'}, + 'int16': {'builtin': True, 'fullname': 'int16', 'name': 'int16', 'c_type': 'int16_t'}, + 'int32': {'builtin': True, 'fullname': 'int32', 'name': 'int32', 'c_type': 'int32_t'}, + 'int64': {'builtin': True, 'fullname': 'int64', 'name': 'int64', 'c_type': 'int64_t'}, +} + +enums = {} + +interfaces = {} + +def make_property_type(typeargs): + value_type = resolve_valuetype('', typeargs['fibre.Property.type']) + mode = typeargs.get('fibre.Property.mode', 'readwrite') + name = 'Property<' + value_type['fullname'] + ', ' + mode + '>' + fullname = join_name('fibre', name) + if fullname in interfaces: + return interfaces[fullname] + + c_type = 'Property<' + ('const ' if mode == 'readonly' else '') + value_type['c_type'] + '>' + prop_type = { + 'name': name, + 'fullname': fullname, + 'c_type': c_type, + 'value_type': value_type, # TODO: should be a metaarg + 'mode': mode, # TODO: should be a metaarg + 'attributes': {}, + 'functions': {} + } + if mode != 'readonly': + prop_type['functions']['exchange'] = { + 'name': 'exchange', + 'fullname': join_name(fullname, 'exchange'), + 'in': {'obj': {'name': 'obj', 'type': {'c_type': c_type}}, 'value': {'name': 'value', 'type': value_type, 'optional': True}}, + 'out': {'value': {'name': 'value', 'type': value_type}}, + #'implementation': 'fibre_property_exchange<' + value_type['c_type'] + '>' + } + else: + prop_type['functions']['read'] = { + 'name': 'read', + 'fullname': join_name(fullname, 'read'), + 'in': {'obj': {'name': 'obj', 'type': {'c_type': c_type}}}, + 'out': {'value': {'name': 'value', 'type': value_type}}, + #'implementation': 'fibre_property_read<' + value_type['c_type'] + '>' + } + + interfaces[fullname] = prop_type + return prop_type + +generics = { + 'fibre.Property': make_property_type # TODO: improve generic support +} + + +def make_ref_type(interface): + name = 'Ref<' + interface['fullname'] + '>' + fullname = join_name('fibre', name) + if fullname in interfaces: + return interfaces[fullname] + + ref_type = { + 'builtin': True, + 'name': name, + 'fullname': fullname, + 'c_type': interface['fullname'].replace('.', 'Intf::') + 'Intf*' + } + value_types[fullname] = ref_type + + return ref_type + +def get_dict(elem, key): + return elem.get(key, None) or {} + +def regularize_arg(path, name, elem): + if elem is None: + elem = {} + elif isinstance(elem, str): + elem = {'type': elem} + elem['name'] = name + elem['fullname'] = path = join_name(path, name) + elem['type'] = regularize_valuetype(path, name, elem['type']) + return elem + +def regularize_func(path, name, elem, prepend_args): + if elem is None: + elem = {} + elem['name'] = name + elem['fullname'] = path = join_name(path, name) + elem['in'] = {n: regularize_arg(path, n, arg) + for n, arg in {**prepend_args, **get_dict(elem, 'in')}.items()} + elem['out'] = {n: regularize_arg(path, n, arg) + for n, arg in get_dict(elem, 'out').items()} + return elem + +def regularize_attribute(path, name, elem, c_is_class): + if elem is None: + elem = {} + if isinstance(elem, str): + elem = {'type': elem} + elif not 'type' in elem: + elem['type'] = {} + if 'attributes' in elem: elem['type']['attributes'] = elem.pop('attributes') + if 'functions' in elem: elem['type']['functions'] = elem.pop('functions') + if 'values' in elem: elem['type']['values'] = elem.pop('values') + if 'flags' in elem: elem['type']['flags'] = elem.pop('flags') + if 'nullflag' in elem: elem['type']['nullflag'] = elem.pop('nullflag') + + elem['name'] = name + elem['fullname'] = join_name(path, name) + elem['typeargs'] = elem.get('typeargs', {}) + elem['c_name'] = elem.get('c_name', None) or (elem['name'] + ('_' if c_is_class else '')) + + if isinstance(elem['type'], str) and elem['type'].startswith('readonly '): + elem['typeargs']['fibre.Property.mode'] = 'readonly' + elem['typeargs']['fibre.Property.type'] = elem['type'][len('readonly '):] + elem['type'] = 'fibre.Property' + elif ('flags' in elem['type']) or ('values' in elem['type']): + elem['typeargs']['fibre.Property.mode'] = elem['typeargs'].get('fibre.Property.mode', None) or 'readwrite' + elem['typeargs']['fibre.Property.type'] = regularize_valuetype(path, to_pascal_case(name), elem['type']) + elem['type'] = 'fibre.Property' + else: + elem['type'] = regularize_interface(path, to_pascal_case(name), elem['type']) + return elem + + +def regularize_interface(path, name, elem): + if elem is None: + elem = {} + if isinstance(elem, str): + return elem # will be resolved during type resolution + #if path is None: + # max_anonymous_type = max([int((re.findall('^' + join_name(path, 'AnonymousType') + '([1-9]+)$', x) + ['0'])[0]) for x in interfaces.keys()]) + # path = 'AnonymousType' + str(max_anonymous_type + 1) + elem['name'] = split_name(name)[-1] + elem['fullname'] = path = join_name(path, name) + elem['c_type'] = elem.get('c_type', elem['fullname'].replace('.', 'Intf::')) + 'Intf' + interfaces[path] = elem + elem['functions'] = {name: regularize_func(path, name, func, {'obj': {'type': make_ref_type(elem)}}) + for name, func in get_dict(elem, 'functions').items()} + treat_as_class = elem.get('c_is_class', None) or (len(elem['functions']) > 0) + elem['attributes'] = {name: regularize_attribute(path, name, prop, treat_as_class) + for name, prop in get_dict(elem, 'attributes').items()} + elem['interfaces'] = [] + elem['enums'] = [] + return elem + +def regularize_valuetype(path, name, elem): + if elem is None: + elem = {} + if isinstance(elem, str): + return elem # will be resolved during type resolution + elem['name'] = split_name(name)[-1] + elem['fullname'] = path = join_name(path, name) + elem['c_type'] = elem.get('c_type', elem['fullname'].replace('.', 'Intf::')) + value_types[path] = elem + + if 'flags' in elem: # treat as flags + bit = 0 + for k, v in elem['flags'].items(): + elem['flags'][k] = elem['flags'][k] or {} + current_bit = elem['flags'][k].get('bit', bit) + elem['flags'][k]['bit'] = current_bit + elem['flags'][k]['value'] = 0 if current_bit is None else (1 << current_bit) + bit = bit if current_bit is None else current_bit + 1 + if 'nullflag' in elem: + elem['flags'] = {elem['nullflag']: {'value': 0, 'bit': None}, **elem['flags']} + elem['values'] = elem['flags'] + elem['is_flags'] = True + elem['is_enum'] = True + enums[path] = elem + + elif 'values' in elem: # treat as enum + val = 0 + for k, v in elem['values'].items(): + elem['values'][k] = elem['values'][k] or {} + val = elem['values'][k].get('value', val) + elem['values'][k]['value'] = val + val += 1 + enums[path] = elem + elem['is_enum'] = True + + return elem + +def resolve_interface(scope, name, typeargs): + """ + Resolves a type name (i.e. interface name or value type name) given as a + string to an interface object. The innermost scope is searched first. + At every scope level, if no matching interface is found, it is checked if a + matching value type exists. If so, the interface type fibre.Property + is returned. + """ + if not isinstance(name, str): + return name + + if 'fibre.Property.type' in typeargs: + typeargs['fibre.Property.type'] = resolve_valuetype(scope, typeargs['fibre.Property.type']) + + scope = scope.split('.') + for probe_scope in [join_name(*scope[:(len(scope)-i)]) for i in range(len(scope)+1)]: + probe_name = join_name(probe_scope, name) + #print('probing ' + probe_name) + if probe_name in interfaces: + return interfaces[probe_name] + elif probe_name in value_types: + typeargs['fibre.Property.type'] = value_types[probe_name] + return make_property_type(typeargs) + elif probe_name in generics: + return generics[probe_name](typeargs) + + raise Exception(f'could not resolve type {name} in {join_name(*scope)}. Known interfaces are: {list(interfaces.keys())}. Known value types are: {list(value_types.keys())}') + +def resolve_valuetype(scope, name): + """ + Resolves a type name given as a string to the type object. + The innermost scope is searched first. + """ + if not isinstance(name, str): + return name + + scope = scope.split('.') + for probe_scope in [join_name(*scope[:(len(scope)-i)]) for i in range(len(scope)+1)]: + probe_name = join_name(probe_scope, name) + if probe_name in value_types: + return value_types[probe_name] + + raise Exception(f'could not resolve type {name} in {join_name(*scope)}. Known value types are: {list(value_types.keys())}') + + +def map_to_fibre01_type(t): + if t.get('is_enum', False): + return 'int32' + elif t['fullname'] == 'float32': + return 'float' + return t['fullname'] + +def generate_endpoint_for_property(prop, bindto, idx): + c_value_type = prop['type']['value_type']['c_type'] + if prop.get('c_setter', None) is None: + c_setter = '[](void* ctx, ' + c_value_type + ' val){ ((decltype(&' + bindto + '))ctx)->' + prop['c_name'] + ' = val; }' + else: + c_setter = '[](void* ctx, ' + c_value_type + ' val){ ((decltype(&' + bindto + '))ctx)->' + prop['c_setter'] + '(val); }' + c_getter = '[](void* ctx) { return (const ' + c_value_type + '&)((decltype(&' + bindto + '))ctx)->' + prop['c_name'] + '; }' + + prop_intf = interfaces[prop['type']['fullname']] + if prop['type']['mode'] == 'readonly': + attr_bindto = prop_intf['c_type'] + '{(void*)&' + bindto + ', ' + c_getter + '}' + else: + attr_bindto = prop_intf['c_type'] + '{(void*)&' + bindto + ', ' + c_getter + ', ' + c_setter + '}' + + endpoint = { + 'id': idx, + 'function': prop_intf['functions']['read' if prop['type']['mode'] == 'readonly' else 'exchange'], + 'in_bindings': {'obj': attr_bindto}, + 'out_bindings': [] + } + endpoint_definition = { + 'name': prop['name'], + 'id': idx, + 'type': map_to_fibre01_type(prop['type']['value_type']), + 'access': 'r' if prop['type']['mode'] == 'readonly' else 'rw', + } + return endpoint, endpoint_definition + +def generate_endpoint_table(intf, bindto, idx): + """ + Generates a Fibre v0.1 endpoint table for a given interface. + This will probably be deprecated in the future. + The object must have no circular property types (i.e. A.b has type B and B.a has type A). + """ + endpoints = [] + endpoint_definitions = [] + cnt = 0 + + for k, prop in intf['attributes'].items(): + property_value_type = re.findall('^fibre\.Property<([^>]*), (readonly|readwrite)>$', prop['type']['fullname']) + #attr_bindto = join_name(bindto, bindings_map.get(join_name(intf['fullname'], k), k + ('_' if len(intf['functions']) or (intf['fullname'] in treat_as_classes) else ''))) + if len(property_value_type): + # Special handling for Property<...> attributes: they resolve to one single endpoint + endpoint, endpoint_definition = generate_endpoint_for_property(prop, bindto, idx + cnt) + endpoints.append(endpoint) + endpoint_definitions.append(endpoint_definition) + cnt += 1 + else: + attr_bindto = join_name(bindto, prop['c_name']) + inner_endpoints, inner_endpoint_definitions, inner_cnt = generate_endpoint_table(prop['type'], attr_bindto, idx + cnt) + endpoints += inner_endpoints + endpoint_definitions.append({ + 'name': k, + 'type': 'object', + 'members': inner_endpoint_definitions + }) + cnt += inner_cnt + + for k, func in intf['functions'].items(): + endpoints.append({ + 'id': idx + cnt, + 'function': func, + 'in_bindings': {**{'obj': '&' + bindto}, **{k_arg: bindto + '.' + func['name'] + '_in_' + k_arg + '_' for k_arg in list(func['in'].keys())[1:]}}, + 'out_bindings': {k_arg: '&' + bindto + '.' + func['name'] + '_out_' + k_arg + '_' for k_arg in func['out'].keys()}, + }) + in_def = [] + out_def = [] + for i, (k_arg, arg) in enumerate(list(func['in'].items())[1:]): + endpoint, endpoint_definition = generate_endpoint_for_property({ + 'name': arg['name'], + 'c_name': func['name'] + '_in_' + k_arg + '_', + 'type': make_property_type({'fibre.Property.type': arg['type'], 'fibre.Property.mode': 'readwrite'}) + }, bindto, idx + cnt + 1 + i) + endpoints.append(endpoint) + in_def.append(endpoint_definition) + for i, (k_arg, arg) in enumerate(func['out'].items()): + endpoint, endpoint_definition = generate_endpoint_for_property({ + 'name': arg['name'], + 'c_name': func['name'] + '_out_' + k_arg + '_', + 'type': make_property_type({'fibre.Property.type': arg['type'], 'fibre.Property.mode': 'readwrite'}) + }, bindto, idx + cnt + len(func['in']) + i) + endpoints.append(endpoint) + out_def.append(endpoint_definition) + + endpoint_definitions.append({ + 'name': k, + 'id': idx + cnt, + 'type': 'function', + 'inputs': in_def, + 'outputs': out_def + }) + cnt += len(func['in']) + len(func['out']) + + return endpoints, endpoint_definitions, cnt + + +# Parse arguments + +parser = argparse.ArgumentParser(description="Gernerate code from YAML interface definitions") +parser.add_argument("--version", action="store_true", + help="print version information") +parser.add_argument("-v", "--verbose", action="store_true", + help="print debug information (on stderr)") +parser.add_argument("-d", "--definitions", type=argparse.FileType('r'), nargs='+', + help="the YAML interface definition file(s) used to generate the code") +parser.add_argument("-t", "--template", type=argparse.FileType('r'), + help="the code template") +parser.add_argument("-o", "--output", type=argparse.FileType('w'), default='-', + help="path of the generated output") +args = parser.parse_args() + +if args.version: + print("0.0.1") + sys.exit(0) + + +definition_files = args.definitions +template_file = args.template +output_file = args.output + + +# Load definition files + +for definition_file in definition_files: + try: + file_content = yaml.load(definition_file, Loader=SafeLineLoader) + except yaml.scanner.ScannerError as ex: + print("YAML parsing error: " + str(ex), file=sys.stderr) + sys.exit(1) + for err in validator.iter_errors(file_content): + if '__line__' in err.absolute_path: + continue + if '__column__' in err.absolute_path: + continue + #instance = err.instance.get(re.findall("([^']*)' (?:was|were) unexpected\)", err.message)[0], err.instance) + # TODO: print line number + raise Exception(err.message + '\nat ' + str(list(err.absolute_path))) + interfaces = {**interfaces, **get_dict(file_content, 'interfaces')} + value_types = {**value_types, **get_dict(file_content, 'valuetypes')} + + +# Preprocess definitions + +# Regularize everything into a wellknown form +for k, item in list(interfaces.items()): + regularize_interface('', k, item) +for k, item in list(value_types.items()): + regularize_valuetype('', k, item) + +if args.verbose: + print('Known interfaces: ' + ''.join([('\n ' + k) for k in interfaces.keys()])) + print('Known value types: ' + ''.join([('\n ' + k) for k in value_types.keys()])) + +# Resolve all types into references +for _, item in list(interfaces.items()): + for _, prop in item['attributes'].items(): + prop['type'] = resolve_interface(item['fullname'], prop['type'], prop['typeargs']) + for _, func in item['functions'].items(): + for _, arg in func['in'].items(): + arg['type'] = resolve_valuetype(item['fullname'], arg['type']) + for _, arg in func['out'].items(): + arg['type'] = resolve_valuetype(item['fullname'], arg['type']) + +# Attach interfaces to their parents +toplevel_interfaces = [] +for k, item in list(interfaces.items()): + k = split_name(k) + if len(k) == 1: + toplevel_interfaces.append(item) + else: + if k[:-1] != ['fibre']: # TODO: remove special handling + interfaces[join_name(*k[:-1])]['interfaces'].append(item) +toplevel_enums = [] +for k, item in list(enums.items()): + k = split_name(k) + if len(k) == 1: + toplevel_enums.append(item) + else: + if k[:-1] != ['fibre']: # TODO: remove special handling + interfaces[join_name(*k[:-1])]['enums'].append(item) + + + +endpoints, embedded_endpoint_definitions, _ = generate_endpoint_table(interfaces['Odrive'], 'odrv', 1) # TODO: make user-configurable +embedded_endpoint_definitions = [{'name': '', 'id': 0, 'type': 'json', 'access': 'r'}] + embedded_endpoint_definitions +endpoints = [{'id': 0, 'function': {'fullname': 'endpoint0_handler', 'in': {}, 'out': {}}, 'bindings': {}}] + endpoints + + +# Render template + +env = jinja2.Environment( + comment_start_string='[#', comment_end_string='#]', + block_start_string='[%', block_end_string='%]', + variable_start_string='[[', variable_end_string=']]' +) + +env.filters['to_pascal_case'] = to_pascal_case +env.filters['to_camel_case'] = to_camel_case +env.filters['to_macro_case'] = to_macro_case +env.filters['to_snake_case'] = to_snake_case +env.filters['to_kebab_case'] = to_kebab_case +env.filters['first'] = lambda x: next(iter(x)) +env.filters['skip_first'] = lambda x: list(x)[1:] +env.filters['to_c_string'] = lambda x: '\n'.join(('"' + line.replace('"', '\\"') + '"') for line in json.dumps(x, separators=(',', ':')).replace('{"name"', '\n{"name"').split('\n')) + +template = env.from_string(template_file.read()) + +output = template.render( + interfaces = interfaces, + value_types = value_types, + toplevel_interfaces = toplevel_interfaces, + endpoints = endpoints, + embedded_endpoint_definitions = embedded_endpoint_definitions +) + +output_file.write(output) diff --git a/Firmware/odrive-interface.yaml b/Firmware/odrive-interface.yaml new file mode 100644 index 00000000..ebf50db9 --- /dev/null +++ b/Firmware/odrive-interface.yaml @@ -0,0 +1,662 @@ +--- +version: 0.0.1 +ns: com.odriverobotics +summary: ODrive Interface Definitions + +interfaces: + Odrive: + attributes: + vbus_voltage: readonly float32 + ibus: readonly float32 + serial_number: readonly uint64 + hw_version_major: readonly uint8 + hw_version_minor: readonly uint8 + hw_version_variant: readonly uint8 + fw_version_major: readonly uint8 + fw_version_minor: readonly uint8 + fw_version_revision: readonly uint8 + fw_version_unreleased: + type: readonly uint8 + doc: 0 for official releases, 1 otherwise + brake_resistor_armed: readonly bool + brake_resistor_saturated: bool + system_stats: + attributes: + uptime: readonly uint32 + min_heap_space: readonly uint32 + min_stack_space_axis0: readonly uint32 + min_stack_space_axis1: readonly uint32 + min_stack_space_comms: readonly uint32 + min_stack_space_usb: readonly uint32 + min_stack_space_uart: readonly uint32 + min_stack_space_can: readonly uint32 + min_stack_space_usb_irq: readonly uint32 + min_stack_space_startup: readonly uint32 + stack_usage_axis0: readonly uint32 + stack_usage_axis1: readonly uint32 + stack_usage_comms: readonly uint32 + stack_usage_usb: readonly uint32 + stack_usage_uart: readonly uint32 + stack_usage_usb_irq: readonly uint32 + stack_usage_startup: readonly uint32 + stack_usage_can: readonly uint32 + usb: + attributes: + rx_cnt: readonly uint32 + tx_cnt: readonly uint32 + tx_overrun_cnt: readonly uint32 + i2c: + attributes: + addr: readonly uint8 + addr_match_cnt: readonly uint32 + rx_cnt: readonly uint32 + error_cnt: readonly uint32 + config: + attributes: + enable_uart: + type: bool + doc: 'TODO: changing this currently requires a reboot - fix this' + uart_baudrate: + type: uint32 + doc: "Defines the baudrate used on the UART interface. + Some baudrates will have a small timing error due to hardware limitations. + + Here's an (incomplete) list of baudrates for ODrive v3.x: + + Configured | Actual | Error [%] + -------------|---------------|----------- + 1.2 KBps | 1.2 KBps | 0 + 2.4 KBps | 2.4 KBps | 0 + 9.6 KBps | 9.6 KBps | 0 + 19.2 KBps | 19.195 KBps | 0.02 + 38.4 KBps | 38.391 KBps | 0.02 + 57.6 KBps | 57.613 KBps | 0.02 + 115.2 KBps | 115.068 KBps | 0.11 + 230.4 KBps | 230.769 KBps | 0.16 + 460.8 KBps | 461.538 KBps | 0.16 + 921.6 KBps | 913.043 KBps | 0.93 + 1.792 MBps | 1.826 MBps | 1.9 + 1.8432 MBps | 1.826 MBps | 0.93 + + For more information refer to Section 30.3.4 and Table 142 (the column with f_PCLK = 42 MHz) in the STM datasheet: + https://www.st.com/content/ccc/resource/technical/document/reference_manual/3d/6d/5a/66/b4/99/40/d4/DM00031020.pdf/files/DM00031020.pdf/jcr:content/translations/en.DM00031020.pdf" + enable_i2c_instead_of_can: + type: bool + doc: 'Changing this requires a reboot' + enable_ascii_protocol_on_usb: bool + max_regen_current: float32 + brake_resistance: + type: float32 + unit: Ohm + doc: Value of the brake resistor connected to the ODrive. Set to 0 to disable. + + dc_bus_undervoltage_trip_level: + type: float32 + unit: V + doc: Minimum voltage below which the motor stops operating. + dc_bus_overvoltage_trip_level: + type: float32 + unit: V + doc: Maximum voltage above which the motor stops operating. + This protects against cases in which the power supply fails to dissipate + the brake power if the brake resistor is disabled. + The default is 26V for the 24V board version and 52V for the 48V board version. + + enable_dc_bus_overvoltage_ramp: + type: bool + doc: 'If enabled, if the measured DC voltage exceeds `dc_bus_overvoltage_ramp_start`, + the ODrive will sink more power than usual into the the brake resistor + in an attempt to bring the voltage down again. + + The brake duty cycle is increased by the following amount: + vbus_voltage == dc_bus_overvoltage_ramp_start => brake_duty_cycle += 0% + vbus_voltage == dc_bus_overvoltage_ramp_end => brake_duty_cycle += 100% + + Remarks: + - This feature is active even when all motors are disarmed. + - This feature is disabled if `brake_resistance` is non-positive.' + dc_bus_overvoltage_ramp_start: + type: float32 + doc: See `enable_dc_bus_overvoltage_ramp`. + Do not set this lower than your usual vbus_voltage, + unless you like fried brake resistors. + dc_bus_overvoltage_ramp_end: + type: float32 + doc: See `enable_dc_bus_overvoltage_ramp`. + Must be larger than `dc_bus_overvoltage_ramp_start`, + otherwise the ramp feature is disabled. + + dc_max_positive_current: + type: float32 + unit: A + doc: Max current the power supply can source. + dc_max_negative_current: + type: float32 + unit: A + doc: Max current the power supply can sink. You most likely want a non-positive value here. Set to -INFINITY to disable. + + #gpio1_pwm_mapping: Endpoint # TODO: disable for ODrive v3.2 and older + #gpio2_pwm_mapping: Endpoint # TODO: disable for ODrive v3.2 and older + #gpio3_pwm_mapping: Endpoint # TODO: disable for ODrive v3.2 and older + #gpio4_pwm_mapping: Endpoint + #gpio3_analog_mapping: Endpoint + #gpio4_analog_mapping: Endpoint + user_config_loaded: readonly bool + + axis0: {type: Axis, c_name: get_axis(0)} + axis1: {type: Axis, c_name: get_axis(1)} + can: {type: Can, c_name: get_can()} + test_property: uint32 + + functions: + test_function: {in: {delta: int32}, out: {cnt: int32}} + get_oscilloscope_val: {in: {index: uint32}, out: {val: float32}} + get_adc_voltage: {in: {gpio: uint32}, out: {voltage: float32}} + save_configuration: + erase_configuration: + reboot: + enter_dfu_mode: + + Odrive.Can: + attributes: + error: + nullflag: None + flags: {DuplicateCanIds: } + config: + attributes: + baud_rate: readonly uint32 + protocol: + values: {Simple: } + functions: + set_baud_rate: {in: {baudRate: uint32}} + + Axis: + attributes: + error: + typeargs: {fibre.Property.mode: readonly} + nullflag: 'None' + flags: + InvalidState: + doc: An invalid state was requested. + DcBusUnderVoltage: + DcBusOverVoltage: + CurrentMeasurementTimeout: + BrakeResistorDisarmed: + doc: The brake resistor was unexpectedly disarmed. + MotorDisarmed: + doc: The motor was unexpectedly disarmed. + MotorFailed: + doc: Check `motor.error` for more information. + SensorlessEstimatorFailed: + EncoderFailed: + doc: Check `encoder.error` for more information. + ControllerFailed: + PosCtrlDuringSensorless: + doc: DEPRECATED + WatchdogTimerExpired: + MinEndstopPressed: + MaxEndstopPressed: + EstopRequested: + HomingWithoutEndstop: + doc: the min endstop was not enabled during homing + step_dir_active: readonly bool + current_state: readonly AxisState + requested_state: AxisState + loop_counter: readonly uint32 + lockin_state: + typeargs: {fibre.Property.mode: readonly} + values: + Inactive: + Ramp: + Accelerate: + ConstVel: + is_homed: {type: bool, c_name: homing_.is_homed} + config: + attributes: + startup_motor_calibration: + type: bool + doc: run motor calibration at startup, skip otherwise + startup_encoder_index_search: + type: bool + doc: run encoder index search after startup, skip otherwise this only has an effect if encoder.config.use_index is also true + startup_encoder_offset_calibration: + type: bool + doc: run encoder offset calibration after startup, skip otherwise + startup_closed_loop_control: + type: bool + doc: enable closed loop control after calibration/startup + startup_sensorless_control: + type: bool + doc: enable sensorless control after calibration/startup + startup_homing: + type: bool + doc: enable homing after calibration/startup + enable_step_dir: + type: bool + doc: Enable step/dir input after calibration. + For M0 this has no effect if `enable_uart` is true. + step_dir_always_on: + type: bool + doc: Keep step/dir enabled while the motor is disabled. + This is ignored if enable_step_dir is false. + This setting only takes effect on a state transition + into idle or out of closed loop control. + counts_per_step: float32 + watchdog_timeout: + type: float32 + unit: s + doc: 0 disables watchdog + enable_watchdog: bool + step_gpio_pin: {type: uint16, c_setter: 'set_step_gpio_pin'} + dir_gpio_pin: {type: uint16, c_setter: 'set_dir_gpio_pin'} + calibration_lockin: # TODO: this is a subset of lockin state + attributes: + current: float32 + ramp_time: float32 + ramp_distance: float32 + accel: float32 + vel: float32 + sensorless_ramp: LockinState + general_lockin: LockinState + can_node_id: + type: uint8 + doc: Both axes will have the same id to start + can_heartbeat_rate_ms: uint32 + motor: Motor + controller: Controller + encoder: Encoder + sensorless_estimator: SensorlessEstimator + trap_traj: TrapezoidalTrajectory + min_endstop: Endstop + max_endstop: Endstop + functions: + watchdog_feed: + doc: Feed the watchdog to prevent watchdog timeouts. + clear_errors: + doc: Check the watchdog timer for expiration. Also sets the watchdog error bit if expired. + + Axis.LockinState: + attributes: + current: + type: float32 + unit: A + ramp_time: + type: float32 + unit: s + ramp_distance: + type: float32 + unit: rad + accel: + type: float32 + unit: rad/s^2 + vel: + type: float32 + unit: rad/s + finish_distance: + type: float32 + unit: rad + finish_on_vel: bool + finish_on_distance: bool + finish_on_enc_idx: bool + + + Motor: + c_is_class: True + attributes: + error: + nullflag: None + flags: + PhaseResistanceOutOfRange: + PhaseInductanceOutOfRange: + AdcFailed: + DrvFault: + ControlDeadlineMissed: + NotImplementedMotorType: + BrakeCurrentOutOfRange: + ModulationMagnitude: + BrakeDeadtimeViolation: + UnexpectedTimerCallback: + CurrentSenseSaturation: + InverterOverTemp: + CurrentLimitViolation: + BrakeDutyCycleNan: + DcBusOverRegenCurrent: {doc: too much current pushed into the power supply} + DcBusOverCurrent: {doc: too much current pulled out of the power supply} + armed_state: + typeargs: {fibre.Property.mode: readonly} + values: + Disarmed: + WaitingForTimings: + WaitingForUpdate: + Armed: + is_calibrated: readonly bool + current_meas_phB: {type: readonly float32, c_name: current_meas_.phB} + current_meas_phC: {type: readonly float32, c_name: current_meas_.phC} + DC_calib_phB: {type: float32, c_name: DC_calib_.phB} + DC_calib_phC: {type: float32, c_name: DC_calib_.phC} + phase_current_rev_gain: float32 + thermal_current_lim: readonly float32 + inverter_temp: readonly float32 + current_control: + attributes: + p_gain: float32 + i_gain: float32 + v_current_control_integral_d: float32 + v_current_control_integral_q: float32 + Ibus: float32 + final_v_alpha: float32 + final_v_beta: float32 + Id_setpoint: float32 + Iq_setpoint: readonly float32 + Iq_measured: float32 + Id_measured: float32 + I_measured_report_filter_k: float32 + max_allowed_current: readonly float32 + overcurrent_trip_level: readonly float32 + acim_rotor_flux: float32 + async_phase_vel: readonly float32 + async_phase_offset: float32 + gate_driver: + c_name: gate_driver_exported_ + attributes: + drv_fault: + typeargs: {fibre.Property.mode: readonly} + nullflag: NoFault + flags: + FetLowCOvercurrent: {bit: 0, doc: FET Low side, Phase C Over Current fault} + FetHighCOvercurrent: {bit: 1, doc: FET High side, Phase C Over Current fault} + FetLowBOvercurrent: {bit: 2, doc: FET Low side, Phase B Over Current fault} + FetHighBOvercurrent: {bit: 3, doc: FET High side, Phase B Over Current fault} + FetLowAOvercurrent: {bit: 4, doc: FET Low side, Phase A Over Current fault} + FetHighAOvercurrent: {bit: 5, doc: FET High side, Phase A Over Current fault} + OvertemperatureWarning: {bit: 6, doc: Over Temperature Warning fault} + OvertemperatureShutdown: {bit: 7, doc: Over Temperature Shut Down fault} + PVddUndervoltage: {bit: 8, doc: Power supply Vdd Under Voltage fault} + GVddUndervoltage: {bit: 9, doc: DRV8301 Vdd Under Voltage fault} + GVddOvervoltage: {bit: 10, doc: DRV8301 Vdd Over Voltage fault} + # status_reg_1: readonly uint32 + # status_reg_2: readonly uint32 + # ctrl_reg_1: readonly uint32 + # ctrl_reg_2: readonly uint32 + timing_log: + attributes: + general: {type: readonly uint16, c_name: 'get(Motor::TIMING_LOG_GENERAL)'} + adc_cb_i: {type: readonly uint16, c_name: 'get(Motor::TIMING_LOG_ADC_CB_I)'} + adc_cb_dc: {type: readonly uint16, c_name: 'get(Motor::TIMING_LOG_ADC_CB_DC)'} + meas_r: {type: readonly uint16, c_name: 'get(Motor::TIMING_LOG_MEAS_R)'} + meas_l: {type: readonly uint16, c_name: 'get(Motor::TIMING_LOG_MEAS_L)'} + enc_calib: {type: readonly uint16, c_name: 'get(Motor::TIMING_LOG_ENC_CALIB)'} + idx_search: {type: readonly uint16, c_name: 'get(Motor::TIMING_LOG_IDX_SEARCH)'} + foc_voltage: {type: readonly uint16, c_name: 'get(Motor::TIMING_LOG_FOC_VOLTAGE)'} + foc_current: {type: readonly uint16, c_name: 'get(Motor::TIMING_LOG_FOC_CURRENT)'} + spi_start: {type: readonly uint16, c_name: 'get(Motor::TIMING_LOG_SPI_START)'} + sample_now: {type: readonly uint16, c_name: 'get(Motor::TIMING_LOG_SAMPLE_NOW)'} + spi_end: {type: readonly uint16, c_name: 'get(Motor::TIMING_LOG_SPI_END)'} + config: + attributes: + pre_calibrated: {type: bool, c_setter: set_pre_calibrated} + pole_pairs: int32 + calibration_current: float32 + resistance_calib_max_voltage: float32 + phase_inductance: {type: float32, c_setter: set_phase_inductance} + phase_resistance: {type: float32, c_setter: set_phase_resistance} + direction: int32 + motor_type: MotorType + current_lim: float32 + current_lim_margin: float32 + inverter_temp_limit_lower: float32 + inverter_temp_limit_upper: float32 + requested_current_range: float32 + current_control_bandwidth: {type: float32, c_setter: set_current_control_bandwidth} + acim_slip_velocity: float32 + acim_gain_min_flux: float32 + acim_autoflux_min_Id: float32 + acim_autoflux_enable: bool + acim_autoflux_attack_gain: float32 + acim_autoflux_decay_gain: float32 + + + Controller: + attributes: + error: + nullflag: None + flags: + Overspeed: + InvalidInputMode: + UnstableGain: + InvalidMirrorAxis: + InvalidLoadEncoder: + InvalidEstimate: + input_pos: {type: float32, c_setter: set_input_pos} + input_vel: float32 + input_current: float32 + pos_setpoint: readonly float32 + vel_setpoint: readonly float32 + current_setpoint: readonly float32 + trajectory_done: readonly bool + vel_integrator_current: float32 + anticogging_valid: bool + config: + attributes: + gain_scheduling_width: float32 + enable_vel_limit: bool + enable_current_mode_vel_limit: + type: bool + doc: Enable velocity limit in current control mode (requires a valid velocity estimator). + enable_gain_scheduling: bool + enable_overspeed_error: bool + control_mode: ControlMode + input_mode: InputMode + pos_gain: + type: float32 + unit: (counts/s) / counts + vel_gain: + type: float32 + unit: 'A/(counts/s) (or A/(rad/s) in sensorless mode' + vel_integrator_gain: + type: float32 + unit: A/(counts/s * s) + vel_limit: + type: float32 + unit: counts/s + doc: Infinity to disable. + vel_limit_tolerance: + type: float32 + doc: Ratio to `vel_limit`. Infinity to disable. + vel_ramp_rate: float32 + current_ramp_rate: + type: float32 + unit: A / sec + homing_speed: + type: float32 + unit: counts/s + inertia: + type: float32 + unit: A/(count/s^2) + axis_to_mirror: uint8 + mirror_ratio: float32 + load_encoder_axis: + type: uint8 + # TODO: this is meaningless for a user. Should there be a separate developer note? + doc: Default depends on Axis number and is set in load_configuration() + input_filter_bandwidth: + type: float32 + unit: 1/s + c_setter: set_input_filter_bandwidth + anticogging: + attributes: + index: readonly uint32 + pre_calibrated: bool + calib_anticogging: readonly bool + calib_pos_threshold: float32 + calib_vel_threshold: float32 + cogging_ratio: readonly float32 + anticogging_enabled: bool + functions: + move_incremental: {in: {displacement: float32, from_input_pos: bool}} + start_anticogging_calibration: + + + Encoder: + attributes: + error: + nullflag: None + flags: + UnstableGain: + CprPolepairsMismatch: + NoResponse: + UnsupportedEncoderMode: + IllegalHallState: + IndexNotFoundYet: + AbsSpiTimeout: + AbsSpiComFail: + AbsSpiNotReady: + is_ready: readonly bool + index_found: readonly bool + shadow_count: readonly int32 + count_in_cpr: readonly int32 + interpolation: readonly float32 + phase: readonly float32 + pos_estimate: readonly float32 + pos_cpr: readonly float32 + hall_state: readonly uint8 + vel_estimate: readonly float32 + calib_scan_response: readonly float32 + pos_abs: int32 + spi_error_rate: readonly float32 + config: + attributes: + mode: Mode + use_index: {type: bool, c_setter: set_use_index} + find_idx_on_lockin_only: {type: bool, c_setter: set_find_idx_on_lockin_only} + abs_spi_cs_gpio_pin: {type: uint16, c_setter: set_abs_spi_cs_gpio_pin} + zero_count_on_find_idx: bool + cpr: int32 + offset: int32 + pre_calibrated: {type: bool, c_setter: set_pre_calibrated} + offset_float: float32 + enable_phase_interpolation: bool + bandwidth: {type: float32, c_setter: set_bandwidth} + calib_range: float32 + calib_scan_distance: float32 + calib_scan_omega: float32 + idx_search_unidirectional: bool + ignore_illegal_hall_state: bool + sincos_gpio_pin_sin: uint16 + sincos_gpio_pin_cos: uint16 + functions: + set_linear_count: {in: {count: int32}} + + + SensorlessEstimator: + c_is_class: True + attributes: + error: + nullflag: None + flags: + UnstableGain: + phase: float32 + pll_pos: float32 + vel_estimate: float32 + # pll_kp: float32 + # pll_ki: float32 + config: + attributes: + observer_gain: float32 + pll_bandwidth: float32 + pm_flux_linkage: float32 + + + TrapezoidalTrajectory: + c_is_class: True + attributes: + config: + attributes: + vel_limit: float32 + accel_limit: float32 + decel_limit: float32 + + + Endstop: + c_is_class: True + attributes: + endstop_state: readonly bool + config: + attributes: + gpio_num: {type: uint16, c_setter: set_gpio_num} + enabled: {type: bool, c_setter: set_enabled} + offset: float32 + is_active_high: bool + pullup: bool + debounce_ms: {type: uint32, c_setter: set_debounce_ms} + + +valuetypes: + Axis.AxisState: # TODO: remove redundant "Axis" in name + values: + Undefined: + doc: will fall through to idle + Idle: + doc: disable PWM and do nothing + StartupSequence: + doc: the actual sequence is defined by the config.startup... flags + FullCalibrationSequence: + doc: run all calibration procedures, then idle + MotorCalibration: + doc: run motor calibration + SensorlessControl: + doc: run sensorless control + EncoderIndexSearch: + doc: run encoder index search + EncoderOffsetCalibration: + doc: run encoder offset calibration + ClosedLoopControl: + doc: run closed loop control + LockinSpin: + doc: run lockin spin + EncoderDirFind: + Homing: + doc: run axis homing function + + Encoder.Mode: + values: + Incremental: + Hall: + Sincos: + SpiAbsCui: + value: 0x100 + doc: compatible with CUI AMT23xx + SpiAbsAms: + value: 0x101 + doc: compatible with AMS AS5047P, AS5048A/AS5048B (no daisy chain support) + SpiAbsAeat: + value: 0x102 + doc: not yet implemented + + Controller.ControlMode: + values: + # Note: these should be sorted from lowest level of control to + # highest level of control, to allow "<" style comparisons. + VoltageControl: + CurrentControl: + VelocityControl: + PositionControl: + + Controller.InputMode: + values: + Inactive: + Passthrough: + VelRamp: + PosFilter: + MixChannels: + TrapTraj: + CurrentRamp: + Mirror: + + + Motor.MotorType: + values: + HighCurrent: + LowCurrent: # TODO: hide this in code + Gimbal: + Acim: \ No newline at end of file diff --git a/docs/interface-definition-file.md b/docs/interface-definition-file.md new file mode 100644 index 00000000..9e4628e1 --- /dev/null +++ b/docs/interface-definition-file.md @@ -0,0 +1,133 @@ +# Interface Definition File + +This document describes the rules on which the ODrive Interface Definition file is built. It is intended for ODrive contributors who wish to modify it or ODrive users who want to autogenerate their own code from this file to interface with the ODrive. + +## Terms and Concepts + +*Value types* are a way of saying how values of this type are serialized/deserialized to/from raw bytes. +Value types can be: + - one of the well-known types `bool`, `int8`, `uint8`, `int16`, `uint16`, `int32`, `uint32`, `int32`, `uint32`, `int64`, `uint64`, `float32`, `float64`, `fibre.Ref` + - An enumeration (that is, a mapping between serialized numbers and well known value names) + - A set of flags (in many programming languages this is the same as normal enums) + +An *interface* is a collection of features (attributes and functions) that can be implemented by an object or used by a client as a filter for object discovery. + +A *function* is something that takes zero or more inputs from the client, does something, and then returns zero or more outputs to the client. Since these input and output arguments are transmitted as raw bytes, they each have a value type. + +An *attribute* is a reference to a subobject which again implements some interface. + +Many languages don't make clear distinctions between interfaces and value types so let's be clear on this: attributes _always_ have an interface type and function input/output arguments _always_ have a value type. If you see something that looks like an attribute with a value type (let's say `uint32`), it's actually an attribute with the interface type `fibre.Property`. If you see a function argument that looks like an interface type (let's say `MyIntf`) it's actually of the value type `fibre.Ref`. + + +## File Structure + +The top level contains a dictionary of interfaces and a dictionary of value types. +Interfaces as well as value types can be subordinate to other interfaces. Nested names are specified using dots in between the subnames. + +Example: + +```yaml +interfaces: + MyFirstInterface: ... + MyFirstInterface.SubInterface: ... + +valuetypes: + MyFirstEnum: ... + MyFirstInterface.SubEnum: ... +``` + +## Interfaces + +Interfaces consist of an `attributes` dictionary and a `functions` dictionary. + +**Attributes** have a type which is either given by name as a string or directly in place. +Even though attributes conceptually and internally are always resolved to an interface type, for your convenience you can also give a value type which is then implicitly resolved to `fibre.Property`. + +If the type is given as a string, it is resolved based on the scope in which it occurs. The search precedence is as follows: The innermost scope is searched first for an interface with that name and then for a value type with that name. If both names don't exist, the next outer scope is checked. Note that the order in which types are defined does not matter. The whole file is read before any type resolution occurs. + +**Functions** have an `in` and `out` dictionary specifying one or more argument names with their corresponding value types. Like with attributes, the types can be specified in place or as a name. Type resolution also works the same except that only value types are checked for. + +Example: +```yaml +interfaces: + Car: + attributes: + velocity: float + door_front_left: Door + door_front_right: Door + steering_wheel: + attributes: + angle: float + functions: + turn: {in: {delta_angle: float32}, out: {final_angle: float32}} + Car.Door: + attributes: + is_open: bool + part_of: Car + functions: + open: + close: +``` + +Let's see how the type resolution of the attibute `Car.Door.part_of: Car` would work here: + + 1. Interface `Car.Door.Car` => not found, proceed + 2. Value type `Car.Door.Car` => not found, proceed + 3. Interface `Car.Car` => not found, proceed + 4. Value type `Car.Car` => not found, proceed + 5. Interface `Car` => found. Link to this interface type. + + +## Enums + +Enums are values which are associated with a name. They are serialized as 32-bit numbers. + +Enumerators without an explicitly stated numerical value are guaranteed to have an underlying value one larger than that of the preceding enumerator. + +Each enumerator must have a unique value. + +Example: + +```yaml +valuetypes: + ModeOfTransport: + values: + Walking: + Bicycle: + Car: {value: 5} + Train: +``` + +This would be serialized as: + - Walking <=> `0x00000000` <=> `0x00 0x00 0x00 0x00` + - Bicycle <=> `0x00000001` <=> `0x01 0x00 0x00 0x00` + - Car <=> `0x00000005` <=> `0x05 0x00 0x00 0x00` + - Train <=> `0x00000006` <=> `0x06 0x00 0x00 0x00` + +## Flagfields + +Flagfields are serialized as 32-bit low endian values where each bit has a named meaning. + +A flag without an explicit bit number is guaranteed to have the bit number of the preceding flag plus one or bit 0 it it's the first in the list. + +Each flag must have a unique bit number. + +Example: + +```yaml +valuetypes: + Anchor: + nullflag: Nowhere + flags: + Top: + Left: + Bottom: {bit: 8} + Right: +``` + +This would be serialized as: + - Nowhere <=> `0x00000000` <=> `0x00 0x00 0x00 0x00` + - Top <=> `0x00000001` <=> `0x01 0x00 0x00 0x00` + - Top and Left <=> `0x00000003` <=> `0x03 0x00 0x00 0x00` + - Bottom <=> `0x00000100` <=> `0x00 0x01 0x00 0x00` + - Top and Bottom and Right <=> `0x00000301` <=> `0x01 0x03 0x00 0x00` From a5fd9dbeff8c595d184efed1534c4b13ffa5740d Mon Sep 17 00:00:00 2001 From: Samuel Sadok Date: Fri, 15 May 2020 16:32:27 +0200 Subject: [PATCH 03/28] make explicit c_is_class a requirement remove inheritance from config --- Firmware/MotorControl/controller.hpp | 2 +- Firmware/MotorControl/encoder.hpp | 2 +- Firmware/MotorControl/endstop.hpp | 2 +- Firmware/MotorControl/motor.hpp | 2 +- Firmware/communication/interface_can.cpp | 4 +-- Firmware/communication/interface_can.hpp | 2 +- Firmware/interface_generator.py | 6 ++++- Firmware/odrive-interface.yaml | 33 +++++++++++++++++++++--- 8 files changed, 41 insertions(+), 12 deletions(-) diff --git a/Firmware/MotorControl/controller.hpp b/Firmware/MotorControl/controller.hpp index f1529d96..ce3cf53f 100644 --- a/Firmware/MotorControl/controller.hpp +++ b/Firmware/MotorControl/controller.hpp @@ -18,7 +18,7 @@ public: bool anticogging_enabled = true; } Anticogging_t; - struct Config_t : ConfigIntf { + struct Config_t { ControlMode control_mode = CONTROL_MODE_POSITION_CONTROL; //see: ControlMode InputMode input_mode = INPUT_MODE_PASSTHROUGH; //see: InputMode float pos_gain = 20.0f; // [(counts/s) / counts] diff --git a/Firmware/MotorControl/encoder.hpp b/Firmware/MotorControl/encoder.hpp index 04471e62..77f40068 100644 --- a/Firmware/MotorControl/encoder.hpp +++ b/Firmware/MotorControl/encoder.hpp @@ -9,7 +9,7 @@ class Encoder : public EncoderIntf { public: const uint32_t MODE_FLAG_ABS = 0x100; - struct Config_t : EncoderIntf::ConfigIntf { + struct Config_t { Mode mode = MODE_INCREMENTAL; bool use_index = false; bool pre_calibrated = false; // If true, this means the offset stored in diff --git a/Firmware/MotorControl/endstop.hpp b/Firmware/MotorControl/endstop.hpp index e9412f89..a87af4f6 100644 --- a/Firmware/MotorControl/endstop.hpp +++ b/Firmware/MotorControl/endstop.hpp @@ -13,7 +13,7 @@ class Endstop { bool pullup = true; // custom setters - Endstop* parent = nullptr + Endstop* parent = nullptr; void set_gpio_num(uint16_t value) { gpio_num = value; parent->update_config(); } void set_enabled(uint32_t value) { enabled = value; parent->update_config(); } void set_debounce_ms(uint32_t value) { debounce_ms = value; parent->update_config(); } diff --git a/Firmware/MotorControl/motor.hpp b/Firmware/MotorControl/motor.hpp index 88a735e3..6043d7be 100644 --- a/Firmware/MotorControl/motor.hpp +++ b/Firmware/MotorControl/motor.hpp @@ -38,7 +38,7 @@ public: // NOTE: for gimbal motors, all units of A are instead V. // example: vel_gain is [V/(count/s)] instead of [A/(count/s)] // example: current_lim and calibration_current will instead determine the maximum voltage applied to the motor. - struct Config_t : public ConfigIntf { + struct Config_t { bool pre_calibrated = false; // can be set to true to indicate that all values here are valid int32_t pole_pairs = 7; float calibration_current = 10.0f; // [A] diff --git a/Firmware/communication/interface_can.cpp b/Firmware/communication/interface_can.cpp index 659958c7..20b8ea6a 100644 --- a/Firmware/communication/interface_can.cpp +++ b/Firmware/communication/interface_can.cpp @@ -32,7 +32,7 @@ void ODriveCAN::can_server_thread() { while (available()) { read(rxmsg); switch (config_.protocol) { - case Config_t::PROTOCOL_SIMPLE: + case PROTOCOL_SIMPLE: CANSimple::handle_can_message(rxmsg); break; } @@ -183,7 +183,7 @@ void ODriveCAN::send_heartbeat(Axis *axis) { uint32_t now = osKernelSysTick(); if ((now - axis->last_heartbeat_) >= axis->config_.can_heartbeat_rate_ms) { switch (config_.protocol) { - case Config_t::PROTOCOL_SIMPLE: + case PROTOCOL_SIMPLE: CANSimple::send_heartbeat(axis); break; } diff --git a/Firmware/communication/interface_can.hpp b/Firmware/communication/interface_can.hpp index 44791a33..b4864611 100644 --- a/Firmware/communication/interface_can.hpp +++ b/Firmware/communication/interface_can.hpp @@ -21,7 +21,7 @@ enum { class ODriveCAN : public OdriveIntf::CanIntf { public: - struct Config_t : ConfigIntf { + struct Config_t { uint32_t baud_rate = CAN_BAUD_250K; Protocol protocol = PROTOCOL_SIMPLE; }; diff --git a/Firmware/interface_generator.py b/Firmware/interface_generator.py index 38468036..a5c939f7 100644 --- a/Firmware/interface_generator.py +++ b/Firmware/interface_generator.py @@ -24,6 +24,7 @@ definitions: additionalProperties: {"$ref": "#/definitions/attribute"} __line__: {type: object} __column__: {type: object} + required: [c_is_class] additionalProperties: false valuetype: @@ -239,6 +240,7 @@ def regularize_attribute(path, name, elem, c_is_class): elem['type'] = {} if 'attributes' in elem: elem['type']['attributes'] = elem.pop('attributes') if 'functions' in elem: elem['type']['functions'] = elem.pop('functions') + if 'c_is_class' in elem: elem['type']['c_is_class'] = elem.pop('c_is_class') if 'values' in elem: elem['type']['values'] = elem.pop('values') if 'flags' in elem: elem['type']['flags'] = elem.pop('flags') if 'nullflag' in elem: elem['type']['nullflag'] = elem.pop('nullflag') @@ -275,7 +277,9 @@ def regularize_interface(path, name, elem): interfaces[path] = elem elem['functions'] = {name: regularize_func(path, name, func, {'obj': {'type': make_ref_type(elem)}}) for name, func in get_dict(elem, 'functions').items()} - treat_as_class = elem.get('c_is_class', None) or (len(elem['functions']) > 0) + if not 'c_is_class' in elem: + raise Exception(elem) + treat_as_class = elem['c_is_class'] # TODO: add command line arg to make this selectively optional elem['attributes'] = {name: regularize_attribute(path, name, prop, treat_as_class) for name, prop in get_dict(elem, 'attributes').items()} elem['interfaces'] = [] diff --git a/Firmware/odrive-interface.yaml b/Firmware/odrive-interface.yaml index ebf50db9..174d0822 100644 --- a/Firmware/odrive-interface.yaml +++ b/Firmware/odrive-interface.yaml @@ -5,6 +5,7 @@ summary: ODrive Interface Definitions interfaces: Odrive: + c_is_class: True attributes: vbus_voltage: readonly float32 ibus: readonly float32 @@ -21,6 +22,7 @@ interfaces: brake_resistor_armed: readonly bool brake_resistor_saturated: bool system_stats: + c_is_class: False attributes: uptime: readonly uint32 min_heap_space: readonly uint32 @@ -41,17 +43,20 @@ interfaces: stack_usage_startup: readonly uint32 stack_usage_can: readonly uint32 usb: + c_is_class: False attributes: rx_cnt: readonly uint32 tx_cnt: readonly uint32 tx_overrun_cnt: readonly uint32 i2c: + c_is_class: False attributes: addr: readonly uint8 addr_match_cnt: readonly uint32 rx_cnt: readonly uint32 error_cnt: readonly uint32 config: + c_is_class: False attributes: enable_uart: type: bool @@ -158,19 +163,21 @@ interfaces: enter_dfu_mode: Odrive.Can: + c_is_class: True attributes: error: nullflag: None flags: {DuplicateCanIds: } config: + c_is_class: False attributes: baud_rate: readonly uint32 - protocol: - values: {Simple: } + protocol: Protocol functions: set_baud_rate: {in: {baudRate: uint32}} Axis: + c_is_class: True attributes: error: typeargs: {fibre.Property.mode: readonly} @@ -212,6 +219,7 @@ interfaces: ConstVel: is_homed: {type: bool, c_name: homing_.is_homed} config: + c_is_class: False attributes: startup_motor_calibration: type: bool @@ -250,6 +258,7 @@ interfaces: step_gpio_pin: {type: uint16, c_setter: 'set_step_gpio_pin'} dir_gpio_pin: {type: uint16, c_setter: 'set_dir_gpio_pin'} calibration_lockin: # TODO: this is a subset of lockin state + c_is_class: False attributes: current: float32 ramp_time: float32 @@ -276,6 +285,7 @@ interfaces: doc: Check the watchdog timer for expiration. Also sets the watchdog error bit if expired. Axis.LockinState: + c_is_class: False attributes: current: type: float32 @@ -338,6 +348,7 @@ interfaces: thermal_current_lim: readonly float32 inverter_temp: readonly float32 current_control: + c_is_class: False attributes: p_gain: float32 i_gain: float32 @@ -358,6 +369,7 @@ interfaces: async_phase_offset: float32 gate_driver: c_name: gate_driver_exported_ + c_is_class: False attributes: drv_fault: typeargs: {fibre.Property.mode: readonly} @@ -379,6 +391,7 @@ interfaces: # ctrl_reg_1: readonly uint32 # ctrl_reg_2: readonly uint32 timing_log: + c_is_class: False attributes: general: {type: readonly uint16, c_name: 'get(Motor::TIMING_LOG_GENERAL)'} adc_cb_i: {type: readonly uint16, c_name: 'get(Motor::TIMING_LOG_ADC_CB_I)'} @@ -393,6 +406,7 @@ interfaces: sample_now: {type: readonly uint16, c_name: 'get(Motor::TIMING_LOG_SAMPLE_NOW)'} spi_end: {type: readonly uint16, c_name: 'get(Motor::TIMING_LOG_SPI_END)'} config: + c_is_class: False attributes: pre_calibrated: {type: bool, c_setter: set_pre_calibrated} pole_pairs: int32 @@ -417,6 +431,7 @@ interfaces: Controller: + c_is_class: True attributes: error: nullflag: None @@ -437,6 +452,7 @@ interfaces: vel_integrator_current: float32 anticogging_valid: bool config: + c_is_class: False attributes: gain_scheduling_width: float32 enable_vel_limit: bool @@ -484,6 +500,7 @@ interfaces: unit: 1/s c_setter: set_input_filter_bandwidth anticogging: + c_is_class: False attributes: index: readonly uint32 pre_calibrated: bool @@ -498,6 +515,7 @@ interfaces: Encoder: + c_is_class: True attributes: error: nullflag: None @@ -525,6 +543,7 @@ interfaces: pos_abs: int32 spi_error_rate: readonly float32 config: + c_is_class: False attributes: mode: Mode use_index: {type: bool, c_setter: set_use_index} @@ -561,6 +580,7 @@ interfaces: # pll_kp: float32 # pll_ki: float32 config: + c_is_class: False attributes: observer_gain: float32 pll_bandwidth: float32 @@ -571,6 +591,7 @@ interfaces: c_is_class: True attributes: config: + c_is_class: False attributes: vel_limit: float32 accel_limit: float32 @@ -582,6 +603,7 @@ interfaces: attributes: endstop_state: readonly bool config: + c_is_class: False attributes: gpio_num: {type: uint16, c_setter: set_gpio_num} enabled: {type: bool, c_setter: set_enabled} @@ -592,6 +614,9 @@ interfaces: valuetypes: + Odrive.Can.Protocol: + values: {Simple: } + Axis.AxisState: # TODO: remove redundant "Axis" in name values: Undefined: @@ -657,6 +682,6 @@ valuetypes: Motor.MotorType: values: HighCurrent: - LowCurrent: # TODO: hide this in code - Gimbal: + #LowCurrent: # not implemented + Gimbal: {value: 2} Acim: \ No newline at end of file From 149dcf737786a1dae555cccf7bb0a8b4167c7a38 Mon Sep 17 00:00:00 2001 From: Samuel Sadok Date: Mon, 18 May 2020 14:44:37 +0200 Subject: [PATCH 04/28] reenable ascii protocol --- Firmware/MotorControl/encoder.cpp | 12 +- Firmware/MotorControl/low_level.cpp | 4 +- Firmware/MotorControl/motor.hpp | 16 -- Firmware/MotorControl/odrive_main.h | 18 ++ Firmware/Tupfile.lua | 2 +- Firmware/ascii_type_info_template.j2 | 90 -------- Firmware/build.lua | 2 +- Firmware/communication/ascii_protocol.cpp | 21 +- .../fibre/cpp/include/fibre/introspection.hpp | 202 ++++++++++++++++++ Firmware/fibre/cpp/include/fibre/protocol.hpp | 24 ++- Firmware/fibre/cpp/interfaces_template.j2 | 15 ++ Firmware/fibre/cpp/type_info_template.j2 | 34 +++ Firmware/interface_generator.py | 37 ++-- Firmware/odrive-interface.yaml | 25 ++- 14 files changed, 332 insertions(+), 170 deletions(-) delete mode 100644 Firmware/ascii_type_info_template.j2 create mode 100644 Firmware/fibre/cpp/include/fibre/introspection.hpp create mode 100644 Firmware/fibre/cpp/type_info_template.j2 diff --git a/Firmware/MotorControl/encoder.cpp b/Firmware/MotorControl/encoder.cpp index c21683e7..743ae591 100644 --- a/Firmware/MotorControl/encoder.cpp +++ b/Firmware/MotorControl/encoder.cpp @@ -207,7 +207,7 @@ bool Encoder::run_offset_calibration() { axis_->run_control_loop([&](){ if (!axis_->motor_.enqueue_voltage_timings(voltage_magnitude, 0.0f)) return false; // error set inside enqueue_voltage_timings - axis_->motor_.log_timing(Motor::TIMING_LOG_ENC_CALIB); + axis_->motor_.log_timing(TIMING_LOG_ENC_CALIB); return ++i < start_lock_duration * current_meas_hz; }); if (axis_->error_ != Axis::ERROR_NONE) @@ -224,7 +224,7 @@ bool Encoder::run_offset_calibration() { 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(Motor::TIMING_LOG_ENC_CALIB); + axis_->motor_.log_timing(TIMING_LOG_ENC_CALIB); encvaluesum += shadow_count_; @@ -264,7 +264,7 @@ bool Encoder::run_offset_calibration() { 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(Motor::TIMING_LOG_ENC_CALIB); + axis_->motor_.log_timing(TIMING_LOG_ENC_CALIB); encvaluesum += shadow_count_; @@ -312,7 +312,7 @@ void Encoder::sample_now() { case MODE_SPI_ABS_CUI: case MODE_SPI_ABS_AEAT: { - axis_->motor_.log_timing(Motor::TIMING_LOG_SAMPLE_NOW); + axis_->motor_.log_timing(TIMING_LOG_SAMPLE_NOW); // Do nothing } break; @@ -348,7 +348,7 @@ bool Encoder::abs_spi_init(){ bool Encoder::abs_spi_start_transaction(){ if (mode_ & MODE_FLAG_ABS){ - axis_->motor_.log_timing(Motor::TIMING_LOG_SPI_START); + axis_->motor_.log_timing(TIMING_LOG_SPI_START); if(hw_config_.spi->State != HAL_SPI_STATE_READY){ set_error(ERROR_ABS_SPI_NOT_READY); return false; @@ -377,7 +377,7 @@ uint8_t cui_parity(uint16_t v) { void Encoder::abs_spi_cb(){ HAL_GPIO_WritePin(abs_spi_cs_port_, abs_spi_cs_pin_, GPIO_PIN_SET); - axis_->motor_.log_timing(Motor::TIMING_LOG_SPI_END); + axis_->motor_.log_timing(TIMING_LOG_SPI_END); uint16_t pos; diff --git a/Firmware/MotorControl/low_level.cpp b/Firmware/MotorControl/low_level.cpp index 5c2886cf..ccfe6aa5 100644 --- a/Firmware/MotorControl/low_level.cpp +++ b/Firmware/MotorControl/low_level.cpp @@ -498,9 +498,9 @@ void pwm_trig_adc_cb(ADC_HandleTypeDef* hadc, bool injected) { // Check the timing of the sequencing if (current_meas_not_DC_CAL) - axis.motor_.log_timing(Motor::TIMING_LOG_ADC_CB_I); + axis.motor_.log_timing(TIMING_LOG_ADC_CB_I); else - axis.motor_.log_timing(Motor::TIMING_LOG_ADC_CB_DC); + axis.motor_.log_timing(TIMING_LOG_ADC_CB_DC); bool update_timings = false; if (hadc == &hadc2) { diff --git a/Firmware/MotorControl/motor.hpp b/Firmware/MotorControl/motor.hpp index 6043d7be..9b6632cb 100644 --- a/Firmware/MotorControl/motor.hpp +++ b/Firmware/MotorControl/motor.hpp @@ -74,22 +74,6 @@ public: void set_current_control_bandwidth(float value) { current_control_bandwidth = value; parent->update_current_controller_gains(); } }; - enum TimingLog_t { - TIMING_LOG_GENERAL, - TIMING_LOG_ADC_CB_I, - TIMING_LOG_ADC_CB_DC, - TIMING_LOG_MEAS_R, - TIMING_LOG_MEAS_L, - TIMING_LOG_ENC_CALIB, - TIMING_LOG_IDX_SEARCH, - TIMING_LOG_FOC_VOLTAGE, - TIMING_LOG_FOC_CURRENT, - TIMING_LOG_SPI_START, - TIMING_LOG_SAMPLE_NOW, - TIMING_LOG_SPI_END, - TIMING_LOG_NUM_SLOTS - }; - Motor(const MotorHardwareConfig_t& hw_config, const GateDriverHardwareConfig_t& gate_driver_config, Config_t& config); diff --git a/Firmware/MotorControl/odrive_main.h b/Firmware/MotorControl/odrive_main.h index a3eb4447..20b1124a 100644 --- a/Firmware/MotorControl/odrive_main.h +++ b/Firmware/MotorControl/odrive_main.h @@ -177,6 +177,24 @@ inline ENUMTYPE &operator &= (ENUMTYPE &a, ENUMTYPE b) { return reinterpret_cast inline ENUMTYPE &operator ^= (ENUMTYPE &a, ENUMTYPE b) { return reinterpret_cast(reinterpret_cast&>(a) ^= static_cast>(b)); } \ inline ENUMTYPE operator ~ (ENUMTYPE a) { return static_cast(~static_cast>(a)); } + +enum TimingLog_t { + TIMING_LOG_GENERAL, + TIMING_LOG_ADC_CB_I, + TIMING_LOG_ADC_CB_DC, + TIMING_LOG_MEAS_R, + TIMING_LOG_MEAS_L, + TIMING_LOG_ENC_CALIB, + TIMING_LOG_IDX_SEARCH, + TIMING_LOG_FOC_VOLTAGE, + TIMING_LOG_FOC_CURRENT, + TIMING_LOG_SPI_START, + TIMING_LOG_SAMPLE_NOW, + TIMING_LOG_SPI_END, + TIMING_LOG_NUM_SLOTS +}; + + #include "autogen/interfaces.hpp" // ODrive specific includes diff --git a/Firmware/Tupfile.lua b/Firmware/Tupfile.lua index dec3547d..94e5ab53 100644 --- a/Firmware/Tupfile.lua +++ b/Firmware/Tupfile.lua @@ -4,7 +4,7 @@ tup.include('build.lua') tup.frule{inputs={'fibre/cpp/interfaces_template.j2'}, command='python3 interface_generator.py --definitions odrive-interface.yaml --template %f --output %o', outputs='autogen/interfaces.hpp'} tup.frule{inputs={'fibre/cpp/function_stubs_template.j2'}, command='python3 interface_generator.py --definitions odrive-interface.yaml --template %f --output %o', outputs='autogen/function_stubs.hpp'} tup.frule{inputs={'fibre/cpp/endpoints_template.j2'}, command='python3 interface_generator.py --definitions odrive-interface.yaml --template %f --output %o', outputs='autogen/endpoints.hpp'} -tup.frule{inputs={'ascii_type_info_template.j2'}, command='python3 interface_generator.py --definitions odrive-interface.yaml --template %f --output %o', outputs='autogen/ascii_type_info.hpp'} +tup.frule{inputs={'fibre/cpp/type_info_template.j2'}, command='python3 interface_generator.py --definitions odrive-interface.yaml --template %f --output %o', outputs='autogen/type_info.hpp'} tup.frule{ command='python ../tools/odrive/version.py --output %o', diff --git a/Firmware/ascii_type_info_template.j2 b/Firmware/ascii_type_info_template.j2 deleted file mode 100644 index 200eb1ba..00000000 --- a/Firmware/ascii_type_info_template.j2 +++ /dev/null @@ -1,90 +0,0 @@ -/*[# This is the original template, thus the warning below does not apply to this file #] - * ============================ WARNING ============================ - * ==== This is an autogenerated file. ==== - * ==== Any changes to this file will be lost when recompiling. ==== - * ================================================================= - * - * This file contains support functions for the ODrive ASCII protocol. - * - * TODO: might generalize this as an approach to runtime introspection. - */ - - -class TypeInfo; - -struct PropertyInfo { - const char * name; - void*(*getter)(void*); - TypeInfo* type_info; -}; - -class TypeInfo { -public: - TypeInfo(const PropertyInfo* property_table, size_t property_table_length) - : property_table_(property_table), property_table_length_(property_table_length) {} - - //virtual bool read_string(void* ctx) { return false; }; - //virtual bool write_string(void* ctx) { return false; }; - - const PropertyInfo* get_property_info(const char * name, size_t length) { - for (const PropertyInfo* prop = property_table_; prop < (property_table_ + property_table_length_); ++prop) { - if (!strncmp(name, prop->name, length)) { - return prop; - } - } - return nullptr; - } - -private: - const PropertyInfo* property_table_; - size_t property_table_length_; -}; - - -class Introspectable { -public: - Introspectable(void* obj, TypeInfo* type_info) : obj_(obj), type_info_(type_info) {} - - Introspectable get_child(const char * path, size_t length) { - Introspectable current = *this; - - const char * begin = path; - const char * end = path + length; - - while ((begin < end) && current.obj_ && current.type_info_) { - const char * end_of_token = std::find(begin, end, '.'); - const PropertyInfo* prop_info = current.type_info_->get_property_info(begin, end_of_token - begin); - if (prop_info) { - current = Introspectable{(*prop_info->getter)(obj_), prop_info->type_info}; - } else { - current = Introspectable{nullptr, nullptr}; - } - begin = std::min(end, end_of_token + 1); - } - - return current; - }; - -private: - void* obj_; - TypeInfo* type_info_; -}; - -[% for intf in interfaces.values() %] - -template -struct [[intf.fullname | to_pascal_case]]TypeInfo : TypeInfo { - static const PropertyInfo property_table[]; - static const TypeInfo singleton; -}; - -template -const PropertyInfo [[intf.name | to_pascal_case]]TypeInfo::property_table[] = { -[%- for property in intf.attributes.values() %] - {"[[property.name]]", [](void* obj){ return (void*)static_cast<[[property.type.c_type]]*>(&((T*)obj)->[[property.name | to_snake_case]]); }, [[property.type.fullname | to_pascal_case]]TypeInfo().[[property.name | to_snake_case]])>::singleton}, -[%- endfor %] -}; -template -const TypeInfo [[intf.fullname | to_pascal_case]]TypeInfo::singleton{[[intf.fullname | to_pascal_case]]TypeInfo::property_table, sizeof([[intf.fullname | to_pascal_case]]TypeInfo::property_table) / sizeof([[intf.fullname | to_pascal_case]]TypeInfo::property_table[0])}; - -[% endfor %] diff --git a/Firmware/build.lua b/Firmware/build.lua index 0f91697f..1204b5fe 100644 --- a/Firmware/build.lua +++ b/Firmware/build.lua @@ -72,7 +72,7 @@ function GCCToolchain(prefix, builddir, compiler_flags, linker_flags) else extra_outputs = {} end - extra_inputs = {'autogen/version.h', 'autogen/interfaces.hpp', 'autogen/function_stubs.hpp', 'autogen/endpoints.hpp'} -- TODO: fix hack + extra_inputs = {'autogen/version.h', 'autogen/interfaces.hpp', 'autogen/function_stubs.hpp', 'autogen/endpoints.hpp', 'autogen/type_info.hpp'} -- TODO: fix hack tup.frule{ inputs= { src, extra_inputs=extra_inputs }, command=compiler..' -c %f '.. diff --git a/Firmware/communication/ascii_protocol.cpp b/Firmware/communication/ascii_protocol.cpp index 4eaaf611..1f8b1acf 100644 --- a/Firmware/communication/ascii_protocol.cpp +++ b/Firmware/communication/ascii_protocol.cpp @@ -14,8 +14,8 @@ #include #include -//#include "autogen/interfaces.hpp" -//#include "autogen/ascii_type_info.hpp" +#include "autogen/type_info.hpp" +#include "communication/interface_can.hpp" /* Private macros ------------------------------------------------------------*/ /* Private typedef -----------------------------------------------------------*/ @@ -28,6 +28,9 @@ #define TO_STR(s) TO_STR_INNER(s) /* Private variables ---------------------------------------------------------*/ + +static Introspectable root_obj = OdriveTypeInfo::make_introspectable(odrv); + /* Private function prototypes -----------------------------------------------*/ /* Function implementations --------------------------------------------------*/ @@ -224,19 +227,18 @@ void ASCII_protocol_process_line(const uint8_t* buffer, size_t len, StreamSink& odrv.reboot(); } -#if 0 } else if (cmd[0] == 'r') { // read property char name[MAX_LINE_LENGTH]; int numscan = sscanf(cmd, "r %255s", name); if (numscan < 1) { respond(response_channel, use_checksum, "invalid command format"); } else { - Endpoint* endpoint = application_endpoints_->get_by_name(name, sizeof(name)); - if (!endpoint) { + Introspectable property = root_obj.get_child(name, sizeof(name)); + if (!property.is_valid()) { respond(response_channel, use_checksum, "invalid property"); } else { char response[10]; - bool success = endpoint->get_string(response, sizeof(response)); + bool success = property.get_string(response, sizeof(response)); if (!success) respond(response_channel, use_checksum, "not implemented"); else @@ -251,16 +253,15 @@ void ASCII_protocol_process_line(const uint8_t* buffer, size_t len, StreamSink& if (numscan < 1) { respond(response_channel, use_checksum, "invalid command format"); } else { - Endpoint* endpoint = application_endpoints_->get_by_name(name, sizeof(name)); - if (!endpoint) { + Introspectable property = root_obj.get_child(name, sizeof(name)); + if (!property.is_valid()) { respond(response_channel, use_checksum, "invalid property"); } else { - bool success = endpoint->set_string(value, sizeof(value)); + bool success = property.set_string(value, sizeof(value)); if (!success) respond(response_channel, use_checksum, "not implemented"); } } -#endif } else if (cmd[0] == 'u') { // Update axis watchdog. unsigned motor_number; diff --git a/Firmware/fibre/cpp/include/fibre/introspection.hpp b/Firmware/fibre/cpp/include/fibre/introspection.hpp new file mode 100644 index 00000000..ac5885f2 --- /dev/null +++ b/Firmware/fibre/cpp/include/fibre/introspection.hpp @@ -0,0 +1,202 @@ +#ifndef __FIBRE_INTROSPECTION_HPP +#define __FIBRE_INTROSPECTION_HPP + +#include +#include +#include + +class TypeInfo; +class Introspectable; + +struct PropertyInfo { + const char * name; + void(*getter)(Introspectable&); + const TypeInfo* type_info; +}; + +/** + * @brief Contains runtime accessible type information. + * + * Specifically, this information consists of a list of PropertyInfo items which + * enable accessing attributes of an object by a runtime string. + * + * Typically, for each combination of C++ type and Fibre interface implemented + * by this type, one (static constant) TypeInfo object will exist. + */ +class TypeInfo { + friend class Introspectable; +public: + TypeInfo(const PropertyInfo* property_table, size_t property_table_length) + : property_table_(property_table), property_table_length_(property_table_length) {} + + const PropertyInfo* get_property_info(const char * name, size_t length) const { + for (const PropertyInfo* prop = property_table_; prop < (property_table_ + property_table_length_); ++prop) { + if (!strncmp(name, prop->name, length)) { + return prop; + } + } + return nullptr; + } + +protected: + template static T& as(Introspectable& obj); + template static const T& as(const Introspectable& obj); + template static Introspectable make_introspectable(T obj, const TypeInfo* type_info); + +private: + virtual bool get_string(const Introspectable& obj, char* buffer, size_t length) const { return false; } + virtual bool set_string(const Introspectable& obj, char* buffer, size_t length) const { return false; } + + const PropertyInfo* property_table_; + size_t property_table_length_; +}; + +/** + * @brief Wraps a reference to an application object by attaching runtime + * accessible type information. + * + * The reference that is wrapped is typically a pointer but can also be a small + * temporary, on-demand constructed object such as a fibre::Property<...> which + * contains multiple pointers. + */ +class Introspectable { + friend class TypeInfo; +public: + /** + * @brief Returns an Introspectable object for the attribute referenced by + * the specified attribute name. + * + * The name can consist of multiple parts separated by dots. + * + * If the attribute does not exist, an invalid Introspectable is returned. + * + * @param path: The name or path of the attribute. + * @param length: The maximum length of the name. + */ + Introspectable get_child(const char * path, size_t length) { + Introspectable current = *this; + + const char * begin = path; + const char * end = std::find(begin, path + length, '\0'); + + while ((begin < end) && current.type_info_) { + const char * end_of_token = std::find(begin, end, '.'); + const PropertyInfo* prop_info = current.type_info_->get_property_info(begin, end_of_token - begin); + if (prop_info) { + (*prop_info->getter)(current); + current.type_info_ = prop_info->type_info; + } else { + current.type_info_ = nullptr; + } + begin = std::min(end, end_of_token + 1); + } + + return current; + }; + + bool is_valid() { + return type_info_; + } + + /** + * @brief Returns the underlying value as a string. This will only succeed + * if this Introspectable contains a Property<...> object. + */ + bool get_string(char* buffer, size_t length) { + return type_info_ && type_info_->get_string(*this, buffer, length); + } + + /** + * @brief Sets the underlying value from a string. This will only succeed + * if this Introspectable contains a Property<...> object. + */ + bool set_string(char* buffer, size_t length) { + return type_info_ && type_info_->set_string(*this, buffer, length); + } + +private: + Introspectable() {} + + // We use this storage to hold generic small objects. Usually that's a pointer + // but sometimes it's an on-demand constructed Property<...>. + // Caution: only put objects in here which are trivially copyable, movable + // and destructible as any custom operation wouldn't be called. + unsigned char storage_[12]; + const TypeInfo* type_info_ = nullptr; +}; + + + +template T& TypeInfo::as(Introspectable& obj) { + static_assert(sizeof(T) <= sizeof(obj.storage_)); + return *(T*)obj.storage_; +} +template const T& TypeInfo::as(const Introspectable& obj) { + static_assert(sizeof(T) <= sizeof(obj.storage_)); + return *(const T*)obj.storage_; +} +template Introspectable TypeInfo::make_introspectable(T obj, const TypeInfo* type_info) { + Introspectable introspectable; + as(introspectable) = obj; + introspectable.type_info_ = type_info; + return introspectable; +} + + +// maybe_underlying_type_t resolves to the underlying type of T if T is an enum type or otherwise to T itself. +template::value> struct maybe_underlying_type; +template struct maybe_underlying_type { typedef std::underlying_type_t type; }; +template struct maybe_underlying_type { typedef T type; }; +template using maybe_underlying_type_t = typename maybe_underlying_type::type; + + + +/* Built-in type infos ********************************************************/ + +template +struct FibrePropertyTypeInfo; + +// readonly property +template +struct FibrePropertyTypeInfo> : TypeInfo { + using TypeInfo::TypeInfo; + static const PropertyInfo property_table[]; + static const FibrePropertyTypeInfo> singleton; + + bool get_string(const Introspectable& obj, char* buffer, size_t length) const override { + return to_string(static_cast>(as>(obj).read()), buffer, length, 0); + } +}; + +template +const PropertyInfo FibrePropertyTypeInfo>::property_table[] = {}; +template +const FibrePropertyTypeInfo> FibrePropertyTypeInfo>::singleton{FibrePropertyTypeInfo>::property_table, sizeof(FibrePropertyTypeInfo>::property_table) / sizeof(FibrePropertyTypeInfo>::property_table[0])}; + +// readwrite property +template +struct FibrePropertyTypeInfo> : TypeInfo { + using TypeInfo::TypeInfo; + static const PropertyInfo property_table[]; + static const FibrePropertyTypeInfo> singleton; + + bool get_string(const Introspectable& obj, char* buffer, size_t length) const override { + return to_string(static_cast>(as>(obj).read()), buffer, length, 0); + } + + bool set_string(const Introspectable& obj, char* buffer, size_t length) const override { + maybe_underlying_type_t value; + if (!from_string(buffer, length, &value, 0)) { + return false; + } + as>(obj).exchange(static_cast(value)); + return true; + } +}; + +template +const PropertyInfo FibrePropertyTypeInfo>::property_table[] = {}; +template +const FibrePropertyTypeInfo> FibrePropertyTypeInfo>::singleton{FibrePropertyTypeInfo>::property_table, sizeof(FibrePropertyTypeInfo>::property_table) / sizeof(FibrePropertyTypeInfo>::property_table[0])}; + +#endif // __FIBRE_INTROSPECTION_HPP \ No newline at end of file diff --git a/Firmware/fibre/cpp/include/fibre/protocol.hpp b/Firmware/fibre/cpp/include/fibre/protocol.hpp index 30a0b8a8..62cb384f 100644 --- a/Firmware/fibre/cpp/include/fibre/protocol.hpp +++ b/Firmware/fibre/cpp/include/fibre/protocol.hpp @@ -555,33 +555,39 @@ template struct Property { Property(void* ctx, T(*getter)(void*), void(*setter)(void*, T)) : ctx_(ctx), getter_(getter), setter_(setter) {} + Property& operator*() { return *this; } Property* operator->() { return this; } - - void* ctx_; - T(*getter_)(void*); - void(*setter_)(void*, T); - T exchange(std::optional value) { + T read() const { + return (*getter_)(ctx_); + } + + T exchange(std::optional value) const { T old_value = (*getter_)(ctx_); if (value.has_value()) { (*setter_)(ctx_, value.value()); } return old_value; } + + void* ctx_; + T(*getter_)(void*); + void(*setter_)(void*, T); }; template struct Property { Property(void* ctx, T(*getter)(void*)) : ctx_(ctx), getter_(getter) {} + Property& operator*() { return *this; } Property* operator->() { return this; } - - void* ctx_; - T(*getter_)(void*); - T read() { + T read() const { return (*getter_)(ctx_); } + + void* ctx_; + T(*getter_)(void*); }; diff --git a/Firmware/fibre/cpp/interfaces_template.j2 b/Firmware/fibre/cpp/interfaces_template.j2 index 76e0c90d..44a8293d 100644 --- a/Firmware/fibre/cpp/interfaces_template.j2 +++ b/Firmware/fibre/cpp/interfaces_template.j2 @@ -33,15 +33,30 @@ public: [%- endfor %] }; [%- endfor %] + +[%- for property in intf.attributes.values() %] +[%- if property.type.fullname.startswith("fibre.Property") %] +[%- if not property.c_setter %] + template static auto get_[[property.name]](T* obj) { return [[property.type.c_type]]{obj, [](void* ctx){ return ([[property.type.value_type.c_type]])((T*)ctx)->[[property.c_getter]]; }}; } +[%- else %] + template static auto get_[[property.name]](T* obj) { return [[property.type.c_type]]{obj, [](void* ctx){ return ([[property.type.value_type.c_type]])((T*)ctx)->[[property.c_getter]]; }, [](void* ctx, [[property.type.value_type.c_type]] value){ ((T*)ctx)->[[property.c_setter]](value); }}; } +[%- endif %] +[%- else %] + template static auto get_[[property.name]](T* obj) { return &obj->[[property.c_name]]; } +[%- endif %] +[%- endfor %] + [%- for func in intf.functions.values() %] virtual [[rettype(func)]] [[func.name | to_snake_case]]([% for in in func.in.values() %][% if loop.index0 %][[in.type.c_type]] [[in.name]][[', ' if not loop.last]][% endif %][% endfor %]) = 0; [%- endfor %] [%- for func in intf.functions.values() %] [%- for k, arg in func.in.items() | skip_first %] [[arg.type.c_type]] [[func.name | to_snake_case]]_in_[[arg.name]]_; // for internal use by Fibre + template static auto get_[[func.name | to_snake_case]]_in_[[arg.name]]_(T* obj) { return Property<[[arg.type.c_type]]>{obj, [](void* ctx){ return ([[arg.type.c_type]])((T*)ctx)->[[func.name | to_snake_case]]_in_[[arg.name]]_; }, [](void* ctx, [[arg.type.c_type]] value){ ((T*)ctx)->[[func.name | to_snake_case]]_in_[[arg.name]]_ = value; }}; } [%- endfor %] [%- for k, arg in func.out.items() %] [[arg.type.c_type]] [[func.name | to_snake_case]]_out_[[arg.name]]_; // for internal use by Fibre + template static auto get_[[func.name | to_snake_case]]_out_[[arg.name]]_(T* obj) { return Property{obj, [](void* ctx){ return ([[arg.type.c_type]])((T*)ctx)->[[func.name | to_snake_case]]_out_[[arg.name]]_; }}; } [%- endfor %] [%- endfor %] }; diff --git a/Firmware/fibre/cpp/type_info_template.j2 b/Firmware/fibre/cpp/type_info_template.j2 new file mode 100644 index 00000000..eb2a0b2c --- /dev/null +++ b/Firmware/fibre/cpp/type_info_template.j2 @@ -0,0 +1,34 @@ +/*[# This is the original template, thus the warning below does not apply to this file #] + * ============================ WARNING ============================ + * ==== This is an autogenerated file. ==== + * ==== Any changes to this file will be lost when recompiling. ==== + * ================================================================= + * + * This file contains support functions for the ODrive ASCII protocol. + * + * TODO: might generalize this as an approach to runtime introspection. + */ + +#include + +[% for intf in interfaces.values() %][% if not intf.builtin %] +template +struct [[intf.fullname | to_pascal_case]]TypeInfo : TypeInfo { + using TypeInfo::TypeInfo; + static const PropertyInfo property_table[]; + static const [[intf.fullname | to_pascal_case]]TypeInfo singleton; + static Introspectable make_introspectable(T& obj) { return TypeInfo::make_introspectable(&obj, &singleton); } +}; +[% endif %][% endfor %] + +[% for intf in interfaces.values() %][% if not intf.builtin %] +template +const PropertyInfo [[intf.fullname | to_pascal_case]]TypeInfo::property_table[] = { +[%- for property in intf.attributes.values() %] + {"[[property.name]]", [](Introspectable& obj){ as()))>>(obj) = [[intf.c_type]]::get_[[property.name]](as(obj)); }, &[[(property.type.purename or property.type.fullname) | to_pascal_case]]TypeInfo()))>>::singleton}, +[%- endfor %] +}; +template +const [[intf.fullname | to_pascal_case]]TypeInfo [[intf.fullname | to_pascal_case]]TypeInfo::singleton{[[intf.fullname | to_pascal_case]]TypeInfo::property_table, sizeof([[intf.fullname | to_pascal_case]]TypeInfo::property_table) / sizeof([[intf.fullname | to_pascal_case]]TypeInfo::property_table[0])}; + +[% endif %][% endfor %] diff --git a/Firmware/interface_generator.py b/Firmware/interface_generator.py index a5c939f7..c0b2148e 100644 --- a/Firmware/interface_generator.py +++ b/Firmware/interface_generator.py @@ -160,9 +160,11 @@ def make_property_type(typeargs): prop_type = { 'name': name, 'fullname': fullname, + 'purename': 'fibre.Property', 'c_type': c_type, 'value_type': value_type, # TODO: should be a metaarg 'mode': mode, # TODO: should be a metaarg + 'builtin': True, 'attributes': {}, 'functions': {} } @@ -249,15 +251,19 @@ def regularize_attribute(path, name, elem, c_is_class): elem['fullname'] = join_name(path, name) elem['typeargs'] = elem.get('typeargs', {}) elem['c_name'] = elem.get('c_name', None) or (elem['name'] + ('_' if c_is_class else '')) + elem['c_getter'] = elem.get('c_getter', elem['c_name']) + elem['c_setter'] = elem.get('c_setter', elem['c_name'] + ' = ') if isinstance(elem['type'], str) and elem['type'].startswith('readonly '): elem['typeargs']['fibre.Property.mode'] = 'readonly' elem['typeargs']['fibre.Property.type'] = elem['type'][len('readonly '):] elem['type'] = 'fibre.Property' + if elem['typeargs']['fibre.Property.mode'] == 'readonly': elem.pop('c_setter') elif ('flags' in elem['type']) or ('values' in elem['type']): elem['typeargs']['fibre.Property.mode'] = elem['typeargs'].get('fibre.Property.mode', None) or 'readwrite' elem['typeargs']['fibre.Property.type'] = regularize_valuetype(path, to_pascal_case(name), elem['type']) elem['type'] = 'fibre.Property' + if elem['typeargs']['fibre.Property.mode'] == 'readonly': elem.pop('c_setter') else: elem['type'] = regularize_interface(path, to_pascal_case(name), elem['type']) return elem @@ -375,19 +381,8 @@ def map_to_fibre01_type(t): return 'float' return t['fullname'] -def generate_endpoint_for_property(prop, bindto, idx): - c_value_type = prop['type']['value_type']['c_type'] - if prop.get('c_setter', None) is None: - c_setter = '[](void* ctx, ' + c_value_type + ' val){ ((decltype(&' + bindto + '))ctx)->' + prop['c_name'] + ' = val; }' - else: - c_setter = '[](void* ctx, ' + c_value_type + ' val){ ((decltype(&' + bindto + '))ctx)->' + prop['c_setter'] + '(val); }' - c_getter = '[](void* ctx) { return (const ' + c_value_type + '&)((decltype(&' + bindto + '))ctx)->' + prop['c_name'] + '; }' - +def generate_endpoint_for_property(prop, attr_bindto, idx): prop_intf = interfaces[prop['type']['fullname']] - if prop['type']['mode'] == 'readonly': - attr_bindto = prop_intf['c_type'] + '{(void*)&' + bindto + ', ' + c_getter + '}' - else: - attr_bindto = prop_intf['c_type'] + '{(void*)&' + bindto + ', ' + c_getter + ', ' + c_setter + '}' endpoint = { 'id': idx, @@ -416,14 +411,14 @@ def generate_endpoint_table(intf, bindto, idx): for k, prop in intf['attributes'].items(): property_value_type = re.findall('^fibre\.Property<([^>]*), (readonly|readwrite)>$', prop['type']['fullname']) #attr_bindto = join_name(bindto, bindings_map.get(join_name(intf['fullname'], k), k + ('_' if len(intf['functions']) or (intf['fullname'] in treat_as_classes) else ''))) + attr_bindto = intf['c_type'] + '::get_' + prop['name'] + '(' + bindto + ')' if len(property_value_type): # Special handling for Property<...> attributes: they resolve to one single endpoint - endpoint, endpoint_definition = generate_endpoint_for_property(prop, bindto, idx + cnt) + endpoint, endpoint_definition = generate_endpoint_for_property(prop, attr_bindto, idx + cnt) endpoints.append(endpoint) endpoint_definitions.append(endpoint_definition) cnt += 1 else: - attr_bindto = join_name(bindto, prop['c_name']) inner_endpoints, inner_endpoint_definitions, inner_cnt = generate_endpoint_table(prop['type'], attr_bindto, idx + cnt) endpoints += inner_endpoints endpoint_definitions.append({ @@ -437,25 +432,23 @@ def generate_endpoint_table(intf, bindto, idx): endpoints.append({ 'id': idx + cnt, 'function': func, - 'in_bindings': {**{'obj': '&' + bindto}, **{k_arg: bindto + '.' + func['name'] + '_in_' + k_arg + '_' for k_arg in list(func['in'].keys())[1:]}}, - 'out_bindings': {k_arg: '&' + bindto + '.' + func['name'] + '_out_' + k_arg + '_' for k_arg in func['out'].keys()}, + 'in_bindings': {**{'obj': bindto}, **{k_arg: '(' + bindto + ')->' + func['name'] + '_in_' + k_arg + '_' for k_arg in list(func['in'].keys())[1:]}}, + 'out_bindings': {k_arg: '&(' + bindto + ')->' + func['name'] + '_out_' + k_arg + '_' for k_arg in func['out'].keys()}, }) in_def = [] out_def = [] for i, (k_arg, arg) in enumerate(list(func['in'].items())[1:]): endpoint, endpoint_definition = generate_endpoint_for_property({ 'name': arg['name'], - 'c_name': func['name'] + '_in_' + k_arg + '_', 'type': make_property_type({'fibre.Property.type': arg['type'], 'fibre.Property.mode': 'readwrite'}) - }, bindto, idx + cnt + 1 + i) + }, intf['c_type'] + '::get_' + func['name'] + '_in_' + k_arg + '_' + '(' + bindto + ')', idx + cnt + 1 + i) endpoints.append(endpoint) in_def.append(endpoint_definition) for i, (k_arg, arg) in enumerate(func['out'].items()): endpoint, endpoint_definition = generate_endpoint_for_property({ 'name': arg['name'], - 'c_name': func['name'] + '_out_' + k_arg + '_', - 'type': make_property_type({'fibre.Property.type': arg['type'], 'fibre.Property.mode': 'readwrite'}) - }, bindto, idx + cnt + len(func['in']) + i) + 'type': make_property_type({'fibre.Property.type': arg['type'], 'fibre.Property.mode': 'readonly'}) + }, intf['c_type'] + '::get_' + func['name'] + '_out_' + k_arg + '_' + '(' + bindto + ')', idx + cnt + len(func['in']) + i) endpoints.append(endpoint) out_def.append(endpoint_definition) @@ -558,7 +551,7 @@ for k, item in list(enums.items()): -endpoints, embedded_endpoint_definitions, _ = generate_endpoint_table(interfaces['Odrive'], 'odrv', 1) # TODO: make user-configurable +endpoints, embedded_endpoint_definitions, _ = generate_endpoint_table(interfaces['Odrive'], '&odrv', 1) # TODO: make user-configurable embedded_endpoint_definitions = [{'name': '', 'id': 0, 'type': 'json', 'access': 'r'}] + embedded_endpoint_definitions endpoints = [{'id': 0, 'function': {'fullname': 'endpoint0_handler', 'in': {}, 'out': {}}, 'bindings': {}}] + endpoints diff --git a/Firmware/odrive-interface.yaml b/Firmware/odrive-interface.yaml index 174d0822..3123e886 100644 --- a/Firmware/odrive-interface.yaml +++ b/Firmware/odrive-interface.yaml @@ -180,7 +180,6 @@ interfaces: c_is_class: True attributes: error: - typeargs: {fibre.Property.mode: readonly} nullflag: 'None' flags: InvalidState: @@ -393,18 +392,18 @@ interfaces: timing_log: c_is_class: False attributes: - general: {type: readonly uint16, c_name: 'get(Motor::TIMING_LOG_GENERAL)'} - adc_cb_i: {type: readonly uint16, c_name: 'get(Motor::TIMING_LOG_ADC_CB_I)'} - adc_cb_dc: {type: readonly uint16, c_name: 'get(Motor::TIMING_LOG_ADC_CB_DC)'} - meas_r: {type: readonly uint16, c_name: 'get(Motor::TIMING_LOG_MEAS_R)'} - meas_l: {type: readonly uint16, c_name: 'get(Motor::TIMING_LOG_MEAS_L)'} - enc_calib: {type: readonly uint16, c_name: 'get(Motor::TIMING_LOG_ENC_CALIB)'} - idx_search: {type: readonly uint16, c_name: 'get(Motor::TIMING_LOG_IDX_SEARCH)'} - foc_voltage: {type: readonly uint16, c_name: 'get(Motor::TIMING_LOG_FOC_VOLTAGE)'} - foc_current: {type: readonly uint16, c_name: 'get(Motor::TIMING_LOG_FOC_CURRENT)'} - spi_start: {type: readonly uint16, c_name: 'get(Motor::TIMING_LOG_SPI_START)'} - sample_now: {type: readonly uint16, c_name: 'get(Motor::TIMING_LOG_SAMPLE_NOW)'} - spi_end: {type: readonly uint16, c_name: 'get(Motor::TIMING_LOG_SPI_END)'} + general: {type: readonly uint16, c_name: 'get(TIMING_LOG_GENERAL)'} + adc_cb_i: {type: readonly uint16, c_name: 'get(TIMING_LOG_ADC_CB_I)'} + adc_cb_dc: {type: readonly uint16, c_name: 'get(TIMING_LOG_ADC_CB_DC)'} + meas_r: {type: readonly uint16, c_name: 'get(TIMING_LOG_MEAS_R)'} + meas_l: {type: readonly uint16, c_name: 'get(TIMING_LOG_MEAS_L)'} + enc_calib: {type: readonly uint16, c_name: 'get(TIMING_LOG_ENC_CALIB)'} + idx_search: {type: readonly uint16, c_name: 'get(TIMING_LOG_IDX_SEARCH)'} + foc_voltage: {type: readonly uint16, c_name: 'get(TIMING_LOG_FOC_VOLTAGE)'} + foc_current: {type: readonly uint16, c_name: 'get(TIMING_LOG_FOC_CURRENT)'} + spi_start: {type: readonly uint16, c_name: 'get(TIMING_LOG_SPI_START)'} + sample_now: {type: readonly uint16, c_name: 'get(TIMING_LOG_SAMPLE_NOW)'} + spi_end: {type: readonly uint16, c_name: 'get(TIMING_LOG_SPI_END)'} config: c_is_class: False attributes: From 15b8f72f77dff77e28d777e0dae1e7698898cb54 Mon Sep 17 00:00:00 2001 From: Samuel Sadok Date: Mon, 18 May 2020 16:23:42 +0200 Subject: [PATCH 05/28] autogen enums --- Firmware/odrive-interface.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/Firmware/odrive-interface.yaml b/Firmware/odrive-interface.yaml index 3123e886..59f901b0 100644 --- a/Firmware/odrive-interface.yaml +++ b/Firmware/odrive-interface.yaml @@ -204,6 +204,7 @@ interfaces: MaxEndstopPressed: EstopRequested: HomingWithoutEndstop: + bit: 17 doc: the min endstop was not enabled during homing step_dir_active: readonly bool current_state: readonly AxisState From 43a167f9fba81489e2dadc432facd10cdb686eb2 Mon Sep 17 00:00:00 2001 From: Samuel Sadok Date: Tue, 19 May 2020 15:50:43 +0200 Subject: [PATCH 06/28] make PWM/analog input work --- Firmware/fibre/cpp/endpoints_template.j2 | 27 +++++++++++-------- .../fibre/cpp/include/fibre/introspection.hpp | 24 ++++++++++++++--- Firmware/fibre/cpp/include/fibre/protocol.hpp | 11 ++++++++ Firmware/interface_generator.py | 1 + Firmware/odrive-interface.yaml | 19 ++++++++----- 5 files changed, 62 insertions(+), 20 deletions(-) diff --git a/Firmware/fibre/cpp/endpoints_template.j2 b/Firmware/fibre/cpp/endpoints_template.j2 index 98c5eb1d..8e2bc9e1 100644 --- a/Firmware/fibre/cpp/endpoints_template.j2 +++ b/Firmware/fibre/cpp/endpoints_template.j2 @@ -13,6 +13,8 @@ #ifndef __FIBRE_INTERFACES_HPP #define __FIBRE_INTERFACES_HPP +#include + namespace fibre { const unsigned char embedded_json[] = [[embedded_endpoint_definitions | to_c_string]]; @@ -48,22 +50,25 @@ bool is_endpoint_ref_valid(endpoint_ref_t endpoint_ref) { } } +Introspectable get_property(size_t idx) { + switch (idx) { +[%- for endpoint in endpoints %] +[%- if endpoint.function.name == 'exchange' and endpoint.in_bindings | list == ['obj'] %][# //case [[endpoint.id]]: *(decltype([[endpoint.in_bindings['obj']]])*)buf = [[endpoint.in_bindings['obj']]]; break;#] + case [[endpoint.id]]: return FibrePropertyTypeInfo<[[endpoint.function.in['obj'].type.c_type]]>::make_introspectable([[endpoint.in_bindings['obj']]]); +[%- endif %] +[%- endfor %] + default: return {}; + } +} + bool set_endpoint_from_float(endpoint_ref_t endpoint_ref, float value) { if (endpoint_ref.json_crc != json_crc_) { return false; } - return false; - // TODO: implement - /*cbufptr_t input_buffer{}; - bufptr_t output_buffer{}; - - switch (idx) { -[%- for endpoint in endpoints %] - case [[endpoint.id]]: return [[endpoint.function.fullname | to_snake_case]]([% for k, arg in endpoint.function.in.items() %][% if k in endpoint.in_bindings %]static_cast<[[arg.type.c_type]]>([[endpoint.in_bindings[k]]])[% else %]std::nullopt[% endif %], [% endfor %]&input_buffer, &output_buffer); -[%- endfor %] - default: return false; - }*/ + Introspectable property = get_property(endpoint_ref.endpoint_id); + const FloatSettableTypeInfo* type_info = dynamic_cast(property.get_type_info()); + return type_info && type_info->set_float(property, value); } } diff --git a/Firmware/fibre/cpp/include/fibre/introspection.hpp b/Firmware/fibre/cpp/include/fibre/introspection.hpp index ac5885f2..6792d9be 100644 --- a/Firmware/fibre/cpp/include/fibre/introspection.hpp +++ b/Firmware/fibre/cpp/include/fibre/introspection.hpp @@ -62,6 +62,8 @@ private: class Introspectable { friend class TypeInfo; public: + Introspectable() {} + /** * @brief Returns an Introspectable object for the attribute referenced by * the specified attribute name. @@ -114,9 +116,11 @@ public: return type_info_ && type_info_->set_string(*this, buffer, length); } -private: - Introspectable() {} + const TypeInfo* get_type_info() { + return type_info_; + } +private: // We use this storage to hold generic small objects. Usually that's a pointer // but sometimes it's an on-demand constructed Property<...>. // Caution: only put objects in here which are trivially copyable, movable @@ -151,6 +155,10 @@ template using maybe_underlying_type_t = typename maybe_underlying_t +struct FloatSettableTypeInfo { + virtual bool set_float(const Introspectable& obj, float val) const { return false; } +}; + /* Built-in type infos ********************************************************/ template @@ -175,10 +183,11 @@ const FibrePropertyTypeInfo> FibrePropertyTypeInfo -struct FibrePropertyTypeInfo> : TypeInfo { +struct FibrePropertyTypeInfo> : FloatSettableTypeInfo, TypeInfo { using TypeInfo::TypeInfo; static const PropertyInfo property_table[]; static const FibrePropertyTypeInfo> singleton; + static const Introspectable make_introspectable(Property obj) { return TypeInfo::make_introspectable(obj, &singleton); } bool get_string(const Introspectable& obj, char* buffer, size_t length) const override { return to_string(static_cast>(as>(obj).read()), buffer, length, 0); @@ -192,6 +201,15 @@ struct FibrePropertyTypeInfo> : TypeInfo { as>(obj).exchange(static_cast(value)); return true; } + + bool set_float(const Introspectable& obj, float val) const override { + maybe_underlying_type_t value; + if (!conversion::set_from_float(val, &value)) { + return false; + } + as>(obj).exchange(static_cast(value)); + return true; + } }; template diff --git a/Firmware/fibre/cpp/include/fibre/protocol.hpp b/Firmware/fibre/cpp/include/fibre/protocol.hpp index 62cb384f..7f9733aa 100644 --- a/Firmware/fibre/cpp/include/fibre/protocol.hpp +++ b/Firmware/fibre/cpp/include/fibre/protocol.hpp @@ -397,6 +397,17 @@ struct Codec::value>> { } static bool encode(T value, bufptr_t* buffer) { return SimpleSerializer::write(value, &(buffer->begin()), buffer->end()); } }; +template<> struct Codec { + static std::optional decode(cbufptr_t* buffer) { + std::optional val0 = SimpleSerializer::read(&(buffer->begin()), buffer->end()); + std::optional val1 = SimpleSerializer::read(&(buffer->begin()), buffer->end()); + return (val0.has_value() && val1.has_value()) ? std::make_optional(endpoint_ref_t{val1.value(), val0.value()}) : std::nullopt; + } + static bool encode(endpoint_ref_t value, bufptr_t* buffer) { + return SimpleSerializer::write(value.endpoint_id, &(buffer->begin()), buffer->end()) + && SimpleSerializer::write(value.json_crc, &(buffer->begin()), buffer->end()); + } +}; } diff --git a/Firmware/interface_generator.py b/Firmware/interface_generator.py index c0b2148e..7e84afff 100644 --- a/Firmware/interface_generator.py +++ b/Firmware/interface_generator.py @@ -142,6 +142,7 @@ value_types = { 'int16': {'builtin': True, 'fullname': 'int16', 'name': 'int16', 'c_type': 'int16_t'}, 'int32': {'builtin': True, 'fullname': 'int32', 'name': 'int32', 'c_type': 'int32_t'}, 'int64': {'builtin': True, 'fullname': 'int64', 'name': 'int64', 'c_type': 'int64_t'}, + 'endpoint_ref': {'builtin': True, 'fullname': 'endpoint_ref', 'name': 'endpoint_ref', 'c_type': 'endpoint_ref_t'}, } enums = {} diff --git a/Firmware/odrive-interface.yaml b/Firmware/odrive-interface.yaml index 59f901b0..854f9588 100644 --- a/Firmware/odrive-interface.yaml +++ b/Firmware/odrive-interface.yaml @@ -140,12 +140,12 @@ interfaces: unit: A doc: Max current the power supply can sink. You most likely want a non-positive value here. Set to -INFINITY to disable. - #gpio1_pwm_mapping: Endpoint # TODO: disable for ODrive v3.2 and older - #gpio2_pwm_mapping: Endpoint # TODO: disable for ODrive v3.2 and older - #gpio3_pwm_mapping: Endpoint # TODO: disable for ODrive v3.2 and older - #gpio4_pwm_mapping: Endpoint - #gpio3_analog_mapping: Endpoint - #gpio4_analog_mapping: Endpoint + gpio1_pwm_mapping: {type: Endpoint, c_name: 'pwm_mappings[0]'} # TODO: disable for ODrive v3.2 and older + gpio2_pwm_mapping: {type: Endpoint, c_name: 'pwm_mappings[1]'} # TODO: disable for ODrive v3.2 and older + gpio3_pwm_mapping: {type: Endpoint, c_name: 'pwm_mappings[2]'} # TODO: disable for ODrive v3.2 and older + gpio4_pwm_mapping: {type: Endpoint, c_name: 'pwm_mappings[3]'} + gpio3_analog_mapping: {type: Endpoint, c_name: 'analog_mappings[0]'} + gpio4_analog_mapping: {type: Endpoint, c_name: 'analog_mappings[1]'} user_config_loaded: readonly bool axis0: {type: Axis, c_name: get_axis(0)} @@ -176,6 +176,13 @@ interfaces: functions: set_baud_rate: {in: {baudRate: uint32}} + Endpoint: + c_is_class: False + attributes: + endpoint: endpoint_ref + min: float32 + max: float32 + Axis: c_is_class: True attributes: From fbf5b296066cd1e14de87ec9d4d528dc7e1020df Mon Sep 17 00:00:00 2001 From: Samuel Sadok Date: Tue, 19 May 2020 20:15:27 +0200 Subject: [PATCH 07/28] attempt at optimizing (535'000 -> 522'268 B) --- Firmware/communication/ascii_protocol.cpp | 10 ++- Firmware/fibre/cpp/endpoints_template.j2 | 38 ++++++--- .../fibre/cpp/include/fibre/introspection.hpp | 82 +++++++++---------- Firmware/fibre/cpp/include/fibre/protocol.hpp | 4 + Firmware/fibre/cpp/interfaces_template.j2 | 15 +++- Firmware/fibre/cpp/type_info_template.j2 | 18 +++- Firmware/interface_generator.py | 9 +- 7 files changed, 109 insertions(+), 67 deletions(-) diff --git a/Firmware/communication/ascii_protocol.cpp b/Firmware/communication/ascii_protocol.cpp index 1f8b1acf..632ceea3 100644 --- a/Firmware/communication/ascii_protocol.cpp +++ b/Firmware/communication/ascii_protocol.cpp @@ -234,11 +234,12 @@ void ASCII_protocol_process_line(const uint8_t* buffer, size_t len, StreamSink& respond(response_channel, use_checksum, "invalid command format"); } else { Introspectable property = root_obj.get_child(name, sizeof(name)); - if (!property.is_valid()) { + const StringConvertibleTypeInfo* type_info = dynamic_cast(property.get_type_info()); + if (!type_info) { respond(response_channel, use_checksum, "invalid property"); } else { char response[10]; - bool success = property.get_string(response, sizeof(response)); + bool success = type_info->get_string(property, response, sizeof(response)); if (!success) respond(response_channel, use_checksum, "not implemented"); else @@ -254,10 +255,11 @@ void ASCII_protocol_process_line(const uint8_t* buffer, size_t len, StreamSink& respond(response_channel, use_checksum, "invalid command format"); } else { Introspectable property = root_obj.get_child(name, sizeof(name)); - if (!property.is_valid()) { + const StringConvertibleTypeInfo* type_info = dynamic_cast(property.get_type_info()); + if (!type_info) { respond(response_channel, use_checksum, "invalid property"); } else { - bool success = property.set_string(value, sizeof(value)); + bool success = type_info->set_string(property, value, sizeof(value)); if (!success) respond(response_channel, use_checksum, "not implemented"); } diff --git a/Firmware/fibre/cpp/endpoints_template.j2 b/Firmware/fibre/cpp/endpoints_template.j2 index 8e2bc9e1..0d880997 100644 --- a/Firmware/fibre/cpp/endpoints_template.j2 +++ b/Firmware/fibre/cpp/endpoints_template.j2 @@ -15,6 +15,9 @@ #include +#pragma GCC push_options +#pragma GCC optimize ("s") + namespace fibre { const unsigned char embedded_json[] = [[embedded_endpoint_definitions | to_c_string]]; @@ -22,16 +25,36 @@ const size_t embedded_json_length = sizeof(embedded_json) - 1; const uint16_t json_crc_ = calc_crc16(PROTOCOL_VERSION, embedded_json, embedded_json_length); const uint32_t json_version_id_ = (json_crc_ << 16) | calc_crc16(json_crc_, embedded_json, embedded_json_length); +Introspectable get_property(size_t idx) { + switch (idx) { +[%- for endpoint in endpoints %] +[%- if endpoint.function.name == 'exchange' and endpoint.in_bindings | list == ['obj'] %] + case [[endpoint.id]]: return FibrePropertyTypeInfo<[[endpoint.function.in['obj'].type.c_type]]>::make_introspectable([[endpoint.in_bindings['obj']]]); +[%- endif %] +[%- endfor %] + default: return {}; + } +} + + // Note: with -Og this function reserves a huge amount of stack space because it // reserves separate space for the stack frame of each of the inlined functions. // The minimum known set of flags to prevent this is `-O1 -fipa-sra`. // `-O2` is a superset of this so that's what we use here. -bool endpoint_handler(int idx, cbufptr_t* input_buffer, bufptr_t* output_buffer) __attribute__ ((optimize(2))); +//bool endpoint_handler(int idx, cbufptr_t* input_buffer, bufptr_t* output_buffer) __attribute__ ((optimize(2))); bool endpoint_handler(int idx, cbufptr_t* input_buffer, bufptr_t* output_buffer) { + //Introspectable property = get_property(idx); + //if property.is_valid() + switch (idx) { [%- for endpoint in endpoints %] +[%- if (endpoint.function.name == 'exchange' or endpoint.function.name == 'read') and endpoint.in_bindings | list == ['obj'] %] + //case [[endpoint.id]]: return FibrePropertyTypeInfo<[[endpoint.function.in['obj'].type.c_type]]>::make_introspectable([[endpoint.in_bindings['obj']]]); case [[endpoint.id]]: { return [[endpoint.function.fullname | to_snake_case]]([% for k, arg in endpoint.function.in.items() %][% if k in endpoint.in_bindings %]static_cast<[[arg.type.c_type]]>([[endpoint.in_bindings[k]]])[% else %]std::nullopt[% endif %], [% endfor %][% for k, arg in endpoint.function.out.items() %][% if k in endpoint.out_bindings %]static_cast<[[arg.type.c_type]]*>([[endpoint.out_bindings[k]]])[% else %]nullptr[% endif %], [% endfor %]input_buffer, output_buffer); } break; +[%- else %] + case [[endpoint.id]]: { return [[endpoint.function.fullname | to_snake_case]]([% for k, arg in endpoint.function.in.items() %][% if k in endpoint.in_bindings %]static_cast<[[arg.type.c_type]]>([[endpoint.in_bindings[k]]])[% else %]std::nullopt[% endif %], [% endfor %][% for k, arg in endpoint.function.out.items() %][% if k in endpoint.out_bindings %]static_cast<[[arg.type.c_type]]*>([[endpoint.out_bindings[k]]])[% else %]nullptr[% endif %], [% endfor %]input_buffer, output_buffer); } break; +[%- endif %] [%- endfor %] default: return false; } @@ -50,17 +73,6 @@ bool is_endpoint_ref_valid(endpoint_ref_t endpoint_ref) { } } -Introspectable get_property(size_t idx) { - switch (idx) { -[%- for endpoint in endpoints %] -[%- if endpoint.function.name == 'exchange' and endpoint.in_bindings | list == ['obj'] %][# //case [[endpoint.id]]: *(decltype([[endpoint.in_bindings['obj']]])*)buf = [[endpoint.in_bindings['obj']]]; break;#] - case [[endpoint.id]]: return FibrePropertyTypeInfo<[[endpoint.function.in['obj'].type.c_type]]>::make_introspectable([[endpoint.in_bindings['obj']]]); -[%- endif %] -[%- endfor %] - default: return {}; - } -} - bool set_endpoint_from_float(endpoint_ref_t endpoint_ref, float value) { if (endpoint_ref.json_crc != json_crc_) { return false; @@ -73,4 +85,6 @@ bool set_endpoint_from_float(endpoint_ref_t endpoint_ref, float value) { } +#pragma GCC pop_options + #endif // __FIBRE_INTERFACES_HPP \ No newline at end of file diff --git a/Firmware/fibre/cpp/include/fibre/introspection.hpp b/Firmware/fibre/cpp/include/fibre/introspection.hpp index 6792d9be..d490ff71 100644 --- a/Firmware/fibre/cpp/include/fibre/introspection.hpp +++ b/Firmware/fibre/cpp/include/fibre/introspection.hpp @@ -5,12 +5,15 @@ #include #include +#pragma GCC push_options +#pragma GCC optimize ("s") + class TypeInfo; class Introspectable; +using introspectable_storage_t = std::aligned_storage<16, 4>::type; struct PropertyInfo { const char * name; - void(*getter)(Introspectable&); const TypeInfo* type_info; }; @@ -29,24 +32,16 @@ public: TypeInfo(const PropertyInfo* property_table, size_t property_table_length) : property_table_(property_table), property_table_length_(property_table_length) {} - const PropertyInfo* get_property_info(const char * name, size_t length) const { - for (const PropertyInfo* prop = property_table_; prop < (property_table_ + property_table_length_); ++prop) { - if (!strncmp(name, prop->name, length)) { - return prop; - } - } - return nullptr; - } + virtual introspectable_storage_t get_child(introspectable_storage_t obj, size_t idx) const = 0; + Introspectable get_child(const Introspectable& obj, const char * name, size_t length) const; protected: + template static T& as(Introspectable& obj); template static const T& as(const Introspectable& obj); template static Introspectable make_introspectable(T obj, const TypeInfo* type_info); private: - virtual bool get_string(const Introspectable& obj, char* buffer, size_t length) const { return false; } - virtual bool set_string(const Introspectable& obj, char* buffer, size_t length) const { return false; } - const PropertyInfo* property_table_; size_t property_table_length_; }; @@ -83,13 +78,7 @@ public: while ((begin < end) && current.type_info_) { const char * end_of_token = std::find(begin, end, '.'); - const PropertyInfo* prop_info = current.type_info_->get_property_info(begin, end_of_token - begin); - if (prop_info) { - (*prop_info->getter)(current); - current.type_info_ = prop_info->type_info; - } else { - current.type_info_ = nullptr; - } + current = current.get_direct_child(begin, end_of_token - begin); begin = std::min(end, end_of_token + 1); } @@ -100,44 +89,38 @@ public: return type_info_; } - /** - * @brief Returns the underlying value as a string. This will only succeed - * if this Introspectable contains a Property<...> object. - */ - bool get_string(char* buffer, size_t length) { - return type_info_ && type_info_->get_string(*this, buffer, length); - } - - /** - * @brief Sets the underlying value from a string. This will only succeed - * if this Introspectable contains a Property<...> object. - */ - bool set_string(char* buffer, size_t length) { - return type_info_ && type_info_->set_string(*this, buffer, length); - } - const TypeInfo* get_type_info() { return type_info_; } private: + Introspectable get_direct_child(const char * name, size_t length) const { + for (size_t i = 0; i < type_info_->property_table_length_; ++i) { + if (!strncmp(name, type_info_->property_table_[i].name, length)) { + Introspectable result; + result.storage_ = type_info_->get_child(storage_, i); + result.type_info_ = type_info_->property_table_[i].type_info; + return result; + } + } + return {}; + } + // We use this storage to hold generic small objects. Usually that's a pointer // but sometimes it's an on-demand constructed Property<...>. // Caution: only put objects in here which are trivially copyable, movable // and destructible as any custom operation wouldn't be called. - unsigned char storage_[12]; + introspectable_storage_t storage_; const TypeInfo* type_info_ = nullptr; }; - - template T& TypeInfo::as(Introspectable& obj) { static_assert(sizeof(T) <= sizeof(obj.storage_)); - return *(T*)obj.storage_; + return *(T*)&obj.storage_; } template const T& TypeInfo::as(const Introspectable& obj) { static_assert(sizeof(T) <= sizeof(obj.storage_)); - return *(const T*)obj.storage_; + return *(const T*)&obj.storage_; } template Introspectable TypeInfo::make_introspectable(T obj, const TypeInfo* type_info) { Introspectable introspectable; @@ -154,8 +137,13 @@ template struct maybe_underlying_type { typedef T type; }; template using maybe_underlying_type_t = typename maybe_underlying_type::type; +struct StringConvertibleTypeInfo { + virtual bool get_string(const Introspectable& obj, char* buffer, size_t length) const { return false; } + virtual bool set_string(const Introspectable& obj, char* buffer, size_t length) const { return false; } +}; struct FloatSettableTypeInfo { + //virtual bool get_float(const Introspectable& obj, float* val) const { return false; } virtual bool set_float(const Introspectable& obj, float val) const { return false; } }; @@ -166,11 +154,15 @@ struct FibrePropertyTypeInfo; // readonly property template -struct FibrePropertyTypeInfo> : TypeInfo { +struct FibrePropertyTypeInfo> : StringConvertibleTypeInfo, TypeInfo { using TypeInfo::TypeInfo; static const PropertyInfo property_table[]; static const FibrePropertyTypeInfo> singleton; + introspectable_storage_t get_child(introspectable_storage_t obj, size_t idx) const override { + return {}; + } + bool get_string(const Introspectable& obj, char* buffer, size_t length) const override { return to_string(static_cast>(as>(obj).read()), buffer, length, 0); } @@ -183,12 +175,16 @@ const FibrePropertyTypeInfo> FibrePropertyTypeInfo -struct FibrePropertyTypeInfo> : FloatSettableTypeInfo, TypeInfo { +struct FibrePropertyTypeInfo> : FloatSettableTypeInfo, StringConvertibleTypeInfo, TypeInfo { using TypeInfo::TypeInfo; static const PropertyInfo property_table[]; static const FibrePropertyTypeInfo> singleton; static const Introspectable make_introspectable(Property obj) { return TypeInfo::make_introspectable(obj, &singleton); } + introspectable_storage_t get_child(introspectable_storage_t obj, size_t idx) const override { + return {}; + } + bool get_string(const Introspectable& obj, char* buffer, size_t length) const override { return to_string(static_cast>(as>(obj).read()), buffer, length, 0); } @@ -217,4 +213,6 @@ const PropertyInfo FibrePropertyTypeInfo>::property_table[] = {}; template const FibrePropertyTypeInfo> FibrePropertyTypeInfo>::singleton{FibrePropertyTypeInfo>::property_table, sizeof(FibrePropertyTypeInfo>::property_table) / sizeof(FibrePropertyTypeInfo>::property_table[0])}; +#pragma GCC pop_options + #endif // __FIBRE_INTROSPECTION_HPP \ No newline at end of file diff --git a/Firmware/fibre/cpp/include/fibre/protocol.hpp b/Firmware/fibre/cpp/include/fibre/protocol.hpp index 7f9733aa..a03a596e 100644 --- a/Firmware/fibre/cpp/include/fibre/protocol.hpp +++ b/Firmware/fibre/cpp/include/fibre/protocol.hpp @@ -566,6 +566,8 @@ template struct Property { Property(void* ctx, T(*getter)(void*), void(*setter)(void*, T)) : ctx_(ctx), getter_(getter), setter_(setter) {} + Property(T* ctx) + : ctx_(ctx), getter_([](void* ctx){ return *(T*)ctx; }), setter_([](void* ctx, T val){ *(T*)ctx = val; }) {} Property& operator*() { return *this; } Property* operator->() { return this; } @@ -590,6 +592,8 @@ template struct Property { Property(void* ctx, T(*getter)(void*)) : ctx_(ctx), getter_(getter) {} + Property(const T* ctx) + : ctx_(const_cast(ctx)), getter_([](void* ctx){ return *(const T*)ctx; }) {} Property& operator*() { return *this; } Property* operator->() { return this; } diff --git a/Firmware/fibre/cpp/interfaces_template.j2 b/Firmware/fibre/cpp/interfaces_template.j2 index 44a8293d..b8feeafa 100644 --- a/Firmware/fibre/cpp/interfaces_template.j2 +++ b/Firmware/fibre/cpp/interfaces_template.j2 @@ -10,6 +10,9 @@ * */ +#pragma GCC push_options +#pragma GCC optimize ("s") + [%- macro rettype(func) %] [%- if not func.out -%] void @@ -36,13 +39,15 @@ public: [%- for property in intf.attributes.values() %] [%- if property.type.fullname.startswith("fibre.Property") %] -[%- if not property.c_setter %] - template static auto get_[[property.name]](T* obj) { return [[property.type.c_type]]{obj, [](void* ctx){ return ([[property.type.value_type.c_type]])((T*)ctx)->[[property.c_getter]]; }}; } +[%- if not property.c_getter and not property.c_setter %] + template static inline auto get_[[property.name]](T* obj) { return [[property.type.c_type]]{&obj->[[property.c_name]]}; } +[%- elif not property.c_setter %] + template static inline auto get_[[property.name]](T* obj) { return [[property.type.c_type]]{obj, [](void* ctx){ return ([[property.type.value_type.c_type]])((T*)ctx)->[[property.c_getter]]; }}; } [%- else %] - template static auto get_[[property.name]](T* obj) { return [[property.type.c_type]]{obj, [](void* ctx){ return ([[property.type.value_type.c_type]])((T*)ctx)->[[property.c_getter]]; }, [](void* ctx, [[property.type.value_type.c_type]] value){ ((T*)ctx)->[[property.c_setter]](value); }}; } + template static inline auto get_[[property.name]](T* obj) { return [[property.type.c_type]]{obj, [](void* ctx){ return ([[property.type.value_type.c_type]])((T*)ctx)->[[property.c_getter]]; }, [](void* ctx, [[property.type.value_type.c_type]] value){ ((T*)ctx)->[[property.c_setter]](value); }}; } [%- endif %] [%- else %] - template static auto get_[[property.name]](T* obj) { return &obj->[[property.c_name]]; } + template static inline auto get_[[property.name]](T* obj) { return &obj->[[property.c_name]]; } [%- endif %] [%- endfor %] @@ -80,3 +85,5 @@ inline [[enum.c_type]] operator ~ ([[enum.c_type]] a) { return static_cast<[[enu [%- endfor %] + +#pragma GCC pop_options diff --git a/Firmware/fibre/cpp/type_info_template.j2 b/Firmware/fibre/cpp/type_info_template.j2 index eb2a0b2c..70cfae2a 100644 --- a/Firmware/fibre/cpp/type_info_template.j2 +++ b/Firmware/fibre/cpp/type_info_template.j2 @@ -11,6 +11,9 @@ #include +#pragma GCC push_options +#pragma GCC optimize ("s") + [% for intf in interfaces.values() %][% if not intf.builtin %] template struct [[intf.fullname | to_pascal_case]]TypeInfo : TypeInfo { @@ -18,6 +21,17 @@ struct [[intf.fullname | to_pascal_case]]TypeInfo : TypeInfo { static const PropertyInfo property_table[]; static const [[intf.fullname | to_pascal_case]]TypeInfo singleton; static Introspectable make_introspectable(T& obj) { return TypeInfo::make_introspectable(&obj, &singleton); } + + introspectable_storage_t get_child(introspectable_storage_t obj, size_t idx) const override { + T* ptr = *(T**)&obj; + introspectable_storage_t res; + switch (idx) { +[%- for property in intf.attributes.values() %] + case [[loop.index0]]: *(decltype([[intf.c_type]]::get_[[property.name]](std::declval()))*)(&res) = [[intf.c_type]]::get_[[property.name]](ptr); break; +[%- endfor %] + } + return res; + } }; [% endif %][% endfor %] @@ -25,10 +39,12 @@ struct [[intf.fullname | to_pascal_case]]TypeInfo : TypeInfo { template const PropertyInfo [[intf.fullname | to_pascal_case]]TypeInfo::property_table[] = { [%- for property in intf.attributes.values() %] - {"[[property.name]]", [](Introspectable& obj){ as()))>>(obj) = [[intf.c_type]]::get_[[property.name]](as(obj)); }, &[[(property.type.purename or property.type.fullname) | to_pascal_case]]TypeInfo()))>>::singleton}, + {"[[property.name]]", &[[(property.type.purename or property.type.fullname) | to_pascal_case]]TypeInfo()))>>::singleton}, [%- endfor %] }; template const [[intf.fullname | to_pascal_case]]TypeInfo [[intf.fullname | to_pascal_case]]TypeInfo::singleton{[[intf.fullname | to_pascal_case]]TypeInfo::property_table, sizeof([[intf.fullname | to_pascal_case]]TypeInfo::property_table) / sizeof([[intf.fullname | to_pascal_case]]TypeInfo::property_table[0])}; [% endif %][% endfor %] + +#pragma GCC pop_options diff --git a/Firmware/interface_generator.py b/Firmware/interface_generator.py index 7e84afff..369a4a1c 100644 --- a/Firmware/interface_generator.py +++ b/Firmware/interface_generator.py @@ -252,19 +252,20 @@ def regularize_attribute(path, name, elem, c_is_class): elem['fullname'] = join_name(path, name) elem['typeargs'] = elem.get('typeargs', {}) elem['c_name'] = elem.get('c_name', None) or (elem['name'] + ('_' if c_is_class else '')) - elem['c_getter'] = elem.get('c_getter', elem['c_name']) - elem['c_setter'] = elem.get('c_setter', elem['c_name'] + ' = ') + if ('c_getter' in elem) or ('c_setter' in elem): + elem['c_getter'] = elem.get('c_getter', elem['c_name']) + elem['c_setter'] = elem.get('c_setter', elem['c_name'] + ' = ') if isinstance(elem['type'], str) and elem['type'].startswith('readonly '): elem['typeargs']['fibre.Property.mode'] = 'readonly' elem['typeargs']['fibre.Property.type'] = elem['type'][len('readonly '):] elem['type'] = 'fibre.Property' - if elem['typeargs']['fibre.Property.mode'] == 'readonly': elem.pop('c_setter') + if elem['typeargs']['fibre.Property.mode'] == 'readonly' and 'c_setter' in elem: elem.pop('c_setter') elif ('flags' in elem['type']) or ('values' in elem['type']): elem['typeargs']['fibre.Property.mode'] = elem['typeargs'].get('fibre.Property.mode', None) or 'readwrite' elem['typeargs']['fibre.Property.type'] = regularize_valuetype(path, to_pascal_case(name), elem['type']) elem['type'] = 'fibre.Property' - if elem['typeargs']['fibre.Property.mode'] == 'readonly': elem.pop('c_setter') + if elem['typeargs']['fibre.Property.mode'] == 'readonly' and 'c_setter' in elem: elem.pop('c_setter') else: elem['type'] = regularize_interface(path, to_pascal_case(name), elem['type']) return elem From f3c883f86f2bd051ab8d65752e62610a2aca5dfa Mon Sep 17 00:00:00 2001 From: Samuel Sadok Date: Tue, 19 May 2020 21:55:41 +0200 Subject: [PATCH 08/28] strip iostream from binary (~520'000 B -> ~380'000 B) --- Firmware/fibre/cpp/endpoints_template.j2 | 1 - .../fibre/cpp/include/fibre/cpp_utils.hpp | 66 ------------------- Firmware/fibre/cpp/interfaces_template.j2 | 4 +- 3 files changed, 2 insertions(+), 69 deletions(-) diff --git a/Firmware/fibre/cpp/endpoints_template.j2 b/Firmware/fibre/cpp/endpoints_template.j2 index 0d880997..72b9afe0 100644 --- a/Firmware/fibre/cpp/endpoints_template.j2 +++ b/Firmware/fibre/cpp/endpoints_template.j2 @@ -50,7 +50,6 @@ bool endpoint_handler(int idx, cbufptr_t* input_buffer, bufptr_t* output_buffer) switch (idx) { [%- for endpoint in endpoints %] [%- if (endpoint.function.name == 'exchange' or endpoint.function.name == 'read') and endpoint.in_bindings | list == ['obj'] %] - //case [[endpoint.id]]: return FibrePropertyTypeInfo<[[endpoint.function.in['obj'].type.c_type]]>::make_introspectable([[endpoint.in_bindings['obj']]]); case [[endpoint.id]]: { return [[endpoint.function.fullname | to_snake_case]]([% for k, arg in endpoint.function.in.items() %][% if k in endpoint.in_bindings %]static_cast<[[arg.type.c_type]]>([[endpoint.in_bindings[k]]])[% else %]std::nullopt[% endif %], [% endfor %][% for k, arg in endpoint.function.out.items() %][% if k in endpoint.out_bindings %]static_cast<[[arg.type.c_type]]*>([[endpoint.out_bindings[k]]])[% else %]nullptr[% endif %], [% endfor %]input_buffer, output_buffer); } break; [%- else %] case [[endpoint.id]]: { return [[endpoint.function.fullname | to_snake_case]]([% for k, arg in endpoint.function.in.items() %][% if k in endpoint.in_bindings %]static_cast<[[arg.type.c_type]]>([[endpoint.in_bindings[k]]])[% else %]std::nullopt[% endif %], [% endfor %][% for k, arg in endpoint.function.out.items() %][% if k in endpoint.out_bindings %]static_cast<[[arg.type.c_type]]*>([[endpoint.out_bindings[k]]])[% else %]nullptr[% endif %], [% endfor %]input_buffer, output_buffer); } break; diff --git a/Firmware/fibre/cpp/include/fibre/cpp_utils.hpp b/Firmware/fibre/cpp/include/fibre/cpp_utils.hpp index b2d84256..af28a07f 100644 --- a/Firmware/fibre/cpp/include/fibre/cpp_utils.hpp +++ b/Firmware/fibre/cpp/include/fibre/cpp_utils.hpp @@ -82,9 +82,6 @@ public: #include #include #include -//#include -#include -#include /* Backport features from C++14 and C++17 ------------------------------------*/ @@ -950,69 +947,6 @@ bool hex_string_to_int_arr(const char * str, TInt (&output)[ICount]) { return hex_string_to_int_arr(str, hex_digits() * ICount, output); } -namespace fibre { - -// TODO: move to print_utils.hpp -template -class HexPrinter { -public: - HexPrinter(T val, bool prefix) : val_(val) /*, prefix_(prefix)*/ { - const char digits[] = "0123456789abcdef"; - size_t prefix_length = prefix ? 2 : 0; - if (prefix) { - str[0] = '0'; - str[1] = 'x'; - } - str[prefix_length + hex_digits()] = '\0'; - - for (size_t i = 0; i < hex_digits(); ++i) { - str[prefix_length + hex_digits() - i - 1] = digits[val & 0xf]; - val >>= 4; - } - } - std::string to_string() const { return str; } - void to_string(char* buf) const { - for (size_t i = 0; (i < sizeof(str)) && str[i]; ++i) - buf[i] = str[i]; - } - - T val_; - //bool prefix_; - char str[hex_digits() + 3]; // 3 additional characters 0x and \0 -}; - -template -std::ostream& operator<<(std::ostream& stream, const HexPrinter& printer) { - // TODO: specialize for char - return stream << printer.to_string(); -} - -template -HexPrinter as_hex(T val, bool prefix = true) { return HexPrinter(val, prefix); } - -template -class HexArrayPrinter { -public: - HexArrayPrinter(T* ptr, size_t length) : ptr_(ptr), length_(length) {} - T* ptr_; - size_t length_; -}; - -template -std::ostream& operator<<(std::ostream& stream, const HexArrayPrinter& printer) { - for (size_t pos = 0; pos < printer.length_; ++pos) { - stream << " " << as_hex(printer.ptr_[pos]); - if (((pos + 1) % 16) == 0) - stream << std::endl; - } - return stream; -} - -template -HexArrayPrinter as_hex(T (&val)[ILength]) { return HexArrayPrinter(val, ILength); } - -} - template class simple_iterator : std::iterator { diff --git a/Firmware/fibre/cpp/interfaces_template.j2 b/Firmware/fibre/cpp/interfaces_template.j2 index b8feeafa..eb5167a7 100644 --- a/Firmware/fibre/cpp/interfaces_template.j2 +++ b/Firmware/fibre/cpp/interfaces_template.j2 @@ -57,11 +57,11 @@ public: [%- for func in intf.functions.values() %] [%- for k, arg in func.in.items() | skip_first %] [[arg.type.c_type]] [[func.name | to_snake_case]]_in_[[arg.name]]_; // for internal use by Fibre - template static auto get_[[func.name | to_snake_case]]_in_[[arg.name]]_(T* obj) { return Property<[[arg.type.c_type]]>{obj, [](void* ctx){ return ([[arg.type.c_type]])((T*)ctx)->[[func.name | to_snake_case]]_in_[[arg.name]]_; }, [](void* ctx, [[arg.type.c_type]] value){ ((T*)ctx)->[[func.name | to_snake_case]]_in_[[arg.name]]_ = value; }}; } + template static auto get_[[func.name | to_snake_case]]_in_[[arg.name]]_(T* obj) { return Property<[[arg.type.c_type]]>{&obj->[[func.name | to_snake_case]]_in_[[arg.name]]_}; } [%- endfor %] [%- for k, arg in func.out.items() %] [[arg.type.c_type]] [[func.name | to_snake_case]]_out_[[arg.name]]_; // for internal use by Fibre - template static auto get_[[func.name | to_snake_case]]_out_[[arg.name]]_(T* obj) { return Property{obj, [](void* ctx){ return ([[arg.type.c_type]])((T*)ctx)->[[func.name | to_snake_case]]_out_[[arg.name]]_; }}; } + template static auto get_[[func.name | to_snake_case]]_out_[[arg.name]]_(T* obj) { return Property{&obj->[[func.name | to_snake_case]]_out_[[arg.name]]_}; } [%- endfor %] [%- endfor %] }; From 20c5276126e11235e9408ba3d505c7e04b4c2ee2 Mon Sep 17 00:00:00 2001 From: Samuel Sadok Date: Mon, 25 May 2020 16:32:45 +0200 Subject: [PATCH 09/28] fix analog input regression --- Firmware/odrive-interface.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Firmware/odrive-interface.yaml b/Firmware/odrive-interface.yaml index d545f412..93158017 100644 --- a/Firmware/odrive-interface.yaml +++ b/Firmware/odrive-interface.yaml @@ -144,8 +144,8 @@ interfaces: gpio2_pwm_mapping: {type: Endpoint, c_name: 'pwm_mappings[1]'} # TODO: disable for ODrive v3.2 and older gpio3_pwm_mapping: {type: Endpoint, c_name: 'pwm_mappings[2]'} # TODO: disable for ODrive v3.2 and older gpio4_pwm_mapping: {type: Endpoint, c_name: 'pwm_mappings[3]'} - gpio3_analog_mapping: {type: Endpoint, c_name: 'analog_mappings[0]'} - gpio4_analog_mapping: {type: Endpoint, c_name: 'analog_mappings[1]'} + gpio3_analog_mapping: {type: Endpoint, c_name: 'analog_mappings[2]'} + gpio4_analog_mapping: {type: Endpoint, c_name: 'analog_mappings[3]'} user_config_loaded: readonly bool axis0: {type: Axis, c_name: get_axis(0)} From a255ffd121563cf0624b9d9936cd6708bbdf66e5 Mon Sep 17 00:00:00 2001 From: Samuel Sadok Date: Fri, 22 May 2020 17:18:48 +0200 Subject: [PATCH 10/28] autogenerate enums.py --- Firmware/Tupfile.lua | 1 + Firmware/enums_template.j2 | 13 ++ Firmware/interface_generator.py | 8 +- tools/odrive/enums.py | 218 ++++++++++++++---------- tools/odrive/tests/analog_input_test.py | 2 +- tools/odrive/tests/calibration_test.py | 4 +- tools/odrive/tests/can_test.py | 8 +- tools/odrive/tests/closed_loop_test.py | 4 +- tools/odrive/tests/encoder_test.py | 4 +- tools/odrive/tests/pwm_input_test.py | 2 +- tools/odrive/tests/test_runner.py | 2 +- tools/odrive/utils.py | 17 +- tools/setup_hall_as_index.py | 2 +- 13 files changed, 174 insertions(+), 111 deletions(-) create mode 100644 Firmware/enums_template.j2 diff --git a/Firmware/Tupfile.lua b/Firmware/Tupfile.lua index 94e5ab53..0e636fd2 100644 --- a/Firmware/Tupfile.lua +++ b/Firmware/Tupfile.lua @@ -5,6 +5,7 @@ tup.frule{inputs={'fibre/cpp/interfaces_template.j2'}, command='python3 interfac tup.frule{inputs={'fibre/cpp/function_stubs_template.j2'}, command='python3 interface_generator.py --definitions odrive-interface.yaml --template %f --output %o', outputs='autogen/function_stubs.hpp'} tup.frule{inputs={'fibre/cpp/endpoints_template.j2'}, command='python3 interface_generator.py --definitions odrive-interface.yaml --template %f --output %o', outputs='autogen/endpoints.hpp'} tup.frule{inputs={'fibre/cpp/type_info_template.j2'}, command='python3 interface_generator.py --definitions odrive-interface.yaml --template %f --output %o', outputs='autogen/type_info.hpp'} +tup.frule{command='python3 interface_generator.py --definitions odrive-interface.yaml --template enums_template.j2 --output ../tools/odrive/enums.py'} tup.frule{ command='python ../tools/odrive/version.py --output %o', diff --git a/Firmware/enums_template.j2 b/Firmware/enums_template.j2 new file mode 100644 index 00000000..28105c3c --- /dev/null +++ b/Firmware/enums_template.j2 @@ -0,0 +1,13 @@ + +# TODO: This file is dangerous because the enums could potentially change between API versions. Should transmit as part of the JSON. + +[%- for _, enum in value_types.items() %] +[%- if enum.is_enum %] + +# [[enum.fullname]] +[%- for k, value in enum['values'].items() %] +[[(((enum.parent.name if enum.name in ['Error', 'Mode'] else '') + enum.name + k) | to_macro_case).ljust(40)]] = [% if enum.is_flags %]0x[['%08x' | format(value.value)]][% else %][[value.value]][% endif %] +[%- endfor %] +[%- endif %] +[%- endfor %] + diff --git a/Firmware/interface_generator.py b/Firmware/interface_generator.py index 369a4a1c..f98cee1e 100644 --- a/Firmware/interface_generator.py +++ b/Firmware/interface_generator.py @@ -541,7 +541,9 @@ for k, item in list(interfaces.items()): toplevel_interfaces.append(item) else: if k[:-1] != ['fibre']: # TODO: remove special handling - interfaces[join_name(*k[:-1])]['interfaces'].append(item) + parent = interfaces[join_name(*k[:-1])] + parent['interfaces'].append(item) + item['parent'] = parent toplevel_enums = [] for k, item in list(enums.items()): k = split_name(k) @@ -549,7 +551,9 @@ for k, item in list(enums.items()): toplevel_enums.append(item) else: if k[:-1] != ['fibre']: # TODO: remove special handling - interfaces[join_name(*k[:-1])]['enums'].append(item) + parent = interfaces[join_name(*k[:-1])] + parent['enums'].append(item) + item['parent'] = parent diff --git a/tools/odrive/enums.py b/tools/odrive/enums.py index e3d3ebd0..55bc64e3 100644 --- a/tools/odrive/enums.py +++ b/tools/odrive/enums.py @@ -1,97 +1,141 @@ -# TODO: This is dangerous. Transmit as part of the JSON +# TODO: This file is dangerous because the enums could potentially change between API versions. Should transmit as part of the JSON. -AXIS_STATE_UNDEFINED = 0 -AXIS_STATE_IDLE = 1 -AXIS_STATE_STARTUP_SEQUENCE = 2 -AXIS_STATE_FULL_CALIBRATION_SEQUENCE = 3 -AXIS_STATE_MOTOR_CALIBRATION = 4 -AXIS_STATE_SENSORLESS_CONTROL = 5 -AXIS_STATE_ENCODER_INDEX_SEARCH = 6 -AXIS_STATE_ENCODER_OFFSET_CALIBRATION = 7 -AXIS_STATE_CLOSED_LOOP_CONTROL = 8 -AXIS_STATE_LOCKIN_SPIN = 9 -AXIS_STATE_ENCODER_DIR_FIND = 10 -AXIS_STATE_HOMING = 11 +# Odrive.Can.Protocol +PROTOCOL_SIMPLE = 0 -class errors: - class axis: - ERROR_NONE = 0x00 - ERROR_INVALID_STATE = 0x01 # Date: Fri, 22 May 2020 20:50:42 +0200 Subject: [PATCH 11/28] [interface generator] rename c_type to c_name --- Firmware/fibre/cpp/endpoints_template.j2 | 6 +-- Firmware/fibre/cpp/function_stubs_template.j2 | 10 ++-- Firmware/fibre/cpp/interfaces_template.j2 | 32 ++++++------- Firmware/fibre/cpp/type_info_template.j2 | 4 +- Firmware/interface_generator.py | 46 +++++++++---------- 5 files changed, 49 insertions(+), 49 deletions(-) diff --git a/Firmware/fibre/cpp/endpoints_template.j2 b/Firmware/fibre/cpp/endpoints_template.j2 index 72b9afe0..975d9422 100644 --- a/Firmware/fibre/cpp/endpoints_template.j2 +++ b/Firmware/fibre/cpp/endpoints_template.j2 @@ -29,7 +29,7 @@ Introspectable get_property(size_t idx) { switch (idx) { [%- for endpoint in endpoints %] [%- if endpoint.function.name == 'exchange' and endpoint.in_bindings | list == ['obj'] %] - case [[endpoint.id]]: return FibrePropertyTypeInfo<[[endpoint.function.in['obj'].type.c_type]]>::make_introspectable([[endpoint.in_bindings['obj']]]); + case [[endpoint.id]]: return FibrePropertyTypeInfo<[[endpoint.function.in['obj'].type.c_name]]>::make_introspectable([[endpoint.in_bindings['obj']]]); [%- endif %] [%- endfor %] default: return {}; @@ -50,9 +50,9 @@ bool endpoint_handler(int idx, cbufptr_t* input_buffer, bufptr_t* output_buffer) switch (idx) { [%- for endpoint in endpoints %] [%- if (endpoint.function.name == 'exchange' or endpoint.function.name == 'read') and endpoint.in_bindings | list == ['obj'] %] - case [[endpoint.id]]: { return [[endpoint.function.fullname | to_snake_case]]([% for k, arg in endpoint.function.in.items() %][% if k in endpoint.in_bindings %]static_cast<[[arg.type.c_type]]>([[endpoint.in_bindings[k]]])[% else %]std::nullopt[% endif %], [% endfor %][% for k, arg in endpoint.function.out.items() %][% if k in endpoint.out_bindings %]static_cast<[[arg.type.c_type]]*>([[endpoint.out_bindings[k]]])[% else %]nullptr[% endif %], [% endfor %]input_buffer, output_buffer); } break; + case [[endpoint.id]]: { return [[endpoint.function.fullname | to_snake_case]]([% for k, arg in endpoint.function.in.items() %][% if k in endpoint.in_bindings %]static_cast<[[arg.type.c_name]]>([[endpoint.in_bindings[k]]])[% else %]std::nullopt[% endif %], [% endfor %][% for k, arg in endpoint.function.out.items() %][% if k in endpoint.out_bindings %]static_cast<[[arg.type.c_name]]*>([[endpoint.out_bindings[k]]])[% else %]nullptr[% endif %], [% endfor %]input_buffer, output_buffer); } break; [%- else %] - case [[endpoint.id]]: { return [[endpoint.function.fullname | to_snake_case]]([% for k, arg in endpoint.function.in.items() %][% if k in endpoint.in_bindings %]static_cast<[[arg.type.c_type]]>([[endpoint.in_bindings[k]]])[% else %]std::nullopt[% endif %], [% endfor %][% for k, arg in endpoint.function.out.items() %][% if k in endpoint.out_bindings %]static_cast<[[arg.type.c_type]]*>([[endpoint.out_bindings[k]]])[% else %]nullptr[% endif %], [% endfor %]input_buffer, output_buffer); } break; + case [[endpoint.id]]: { return [[endpoint.function.fullname | to_snake_case]]([% for k, arg in endpoint.function.in.items() %][% if k in endpoint.in_bindings %]static_cast<[[arg.type.c_name]]>([[endpoint.in_bindings[k]]])[% else %]std::nullopt[% endif %], [% endfor %][% for k, arg in endpoint.function.out.items() %][% if k in endpoint.out_bindings %]static_cast<[[arg.type.c_name]]*>([[endpoint.out_bindings[k]]])[% else %]nullptr[% endif %], [% endfor %]input_buffer, output_buffer); } break; [%- endif %] [%- endfor %] default: return false; diff --git a/Firmware/fibre/cpp/function_stubs_template.j2 b/Firmware/fibre/cpp/function_stubs_template.j2 index 759b49eb..fb44fdaa 100644 --- a/Firmware/fibre/cpp/function_stubs_template.j2 +++ b/Firmware/fibre/cpp/function_stubs_template.j2 @@ -13,9 +13,9 @@ [% for intf in interfaces.values() %] [% for func in intf.functions.values() %] -static inline bool [[func.fullname | to_snake_case]]([% for arg in func.in.values() %]std::optional<[[arg.type.c_type]]> in_[[arg.name]], [% endfor %][% for arg in func.out.values() %][[arg.type.c_type]]* out_[[arg.name]], [% endfor %]fibre::cbufptr_t* input_buffer, fibre::bufptr_t* output_buffer) { +static inline bool [[func.fullname | to_snake_case]]([% for arg in func.in.values() %]std::optional<[[arg.type.c_name]]> in_[[arg.name]], [% endfor %][% for arg in func.out.values() %][[arg.type.c_name]]* out_[[arg.name]], [% endfor %]fibre::cbufptr_t* input_buffer, fibre::bufptr_t* output_buffer) { [%- if func.in %] - bool success = [% for arg in func.in.values() %](in_[[arg.name]].has_value() || (in_[[arg.name]] = fibre::Codec<[[arg.type.c_type]]>::decode(input_buffer)).has_value()[% if arg.optional %] || true[% endif %])[% if not loop.last %] + bool success = [% for arg in func.in.values() %](in_[[arg.name]].has_value() || (in_[[arg.name]] = fibre::Codec<[[arg.type.c_name]]>::decode(input_buffer)).has_value()[% if arg.optional %] || true[% endif %])[% if not loop.last %] && [% endif %][% endfor %]; [%- else %] bool success = true; @@ -24,12 +24,12 @@ static inline bool [[func.fullname | to_snake_case]]([% for arg in func.in.value return false; } [%- if func.implementation %] - [% if func.out %]std::tuple<[% for arg in func.out.values() %][[arg.type.c_type]][[', ' if not loop.last]][% endfor %]> ret = [% endif %][[func.implementation]]([% for arg in func.in.values() %]in_[[arg.name]][% if not arg.optional %].value()[% endif %][[', ' if not loop.last]][% endfor %]); + [% if func.out %]std::tuple<[% for arg in func.out.values() %][[arg.type.c_name]][[', ' if not loop.last]][% endfor %]> ret = [% endif %][[func.implementation]]([% for arg in func.in.values() %]in_[[arg.name]][% if not arg.optional %].value()[% endif %][[', ' if not loop.last]][% endfor %]); [%- else %] - [% if func.out %]std::tuple<[% for arg in func.out.values() %][[arg.type.c_type]][[', ' if not loop.last]][% endfor %]> ret = [% endif %]in_[[(func.in.values() | first).name]].value()->[[func.name]]([% for arg in func.in.values() | skip_first %]in_[[arg.name]][% if not arg.optional %].value()[% endif %][[', ' if not loop.last]][% endfor %]); + [% if func.out %]std::tuple<[% for arg in func.out.values() %][[arg.type.c_name]][[', ' if not loop.last]][% endfor %]> ret = [% endif %]in_[[(func.in.values() | first).name]].value()->[[func.name]]([% for arg in func.in.values() | skip_first %]in_[[arg.name]][% if not arg.optional %].value()[% endif %][[', ' if not loop.last]][% endfor %]); [%- endif %] [%- if func.out %] - return [% for arg in func.out.values() %]((out_[[arg.name]] && ((*out_[[arg.name]] = std::get<[[loop.index0]]>(ret)), true)) || fibre::Codec<[[arg.type.c_type]]>::encode(std::get<[[loop.index0]]>(ret), output_buffer))[% if not loop.last %] + return [% for arg in func.out.values() %]((out_[[arg.name]] && ((*out_[[arg.name]] = std::get<[[loop.index0]]>(ret)), true)) || fibre::Codec<[[arg.type.c_name]]>::encode(std::get<[[loop.index0]]>(ret), output_buffer))[% if not loop.last %] && [% endif %][% endfor %]; [%- else %] return true; diff --git a/Firmware/fibre/cpp/interfaces_template.j2 b/Firmware/fibre/cpp/interfaces_template.j2 index eb5167a7..60c92b35 100644 --- a/Firmware/fibre/cpp/interfaces_template.j2 +++ b/Firmware/fibre/cpp/interfaces_template.j2 @@ -17,7 +17,7 @@ [%- if not func.out -%] void [%- elif func.out | length == 1 -%] -[[(func.out.values() | first).type.c_type]] +[[(func.out.values() | first).type.c_name]] [%- else -%] [% for arg in func.out.values() %][[arg.type]][[', ' if not loop.last]][% endfor %] [%- endif -%] @@ -40,11 +40,11 @@ public: [%- for property in intf.attributes.values() %] [%- if property.type.fullname.startswith("fibre.Property") %] [%- if not property.c_getter and not property.c_setter %] - template static inline auto get_[[property.name]](T* obj) { return [[property.type.c_type]]{&obj->[[property.c_name]]}; } + template static inline auto get_[[property.name]](T* obj) { return [[property.type.c_name]]{&obj->[[property.c_name]]}; } [%- elif not property.c_setter %] - template static inline auto get_[[property.name]](T* obj) { return [[property.type.c_type]]{obj, [](void* ctx){ return ([[property.type.value_type.c_type]])((T*)ctx)->[[property.c_getter]]; }}; } + template static inline auto get_[[property.name]](T* obj) { return [[property.type.c_name]]{obj, [](void* ctx){ return ([[property.type.value_type.c_name]])((T*)ctx)->[[property.c_getter]]; }}; } [%- else %] - template static inline auto get_[[property.name]](T* obj) { return [[property.type.c_type]]{obj, [](void* ctx){ return ([[property.type.value_type.c_type]])((T*)ctx)->[[property.c_getter]]; }, [](void* ctx, [[property.type.value_type.c_type]] value){ ((T*)ctx)->[[property.c_setter]](value); }}; } + template static inline auto get_[[property.name]](T* obj) { return [[property.type.c_name]]{obj, [](void* ctx){ return ([[property.type.value_type.c_name]])((T*)ctx)->[[property.c_getter]]; }, [](void* ctx, [[property.type.value_type.c_name]] value){ ((T*)ctx)->[[property.c_setter]](value); }}; } [%- endif %] [%- else %] template static inline auto get_[[property.name]](T* obj) { return &obj->[[property.c_name]]; } @@ -52,16 +52,16 @@ public: [%- endfor %] [%- for func in intf.functions.values() %] - virtual [[rettype(func)]] [[func.name | to_snake_case]]([% for in in func.in.values() %][% if loop.index0 %][[in.type.c_type]] [[in.name]][[', ' if not loop.last]][% endif %][% endfor %]) = 0; + virtual [[rettype(func)]] [[func.name | to_snake_case]]([% for in in func.in.values() %][% if loop.index0 %][[in.type.c_name]] [[in.name]][[', ' if not loop.last]][% endif %][% endfor %]) = 0; [%- endfor %] [%- for func in intf.functions.values() %] [%- for k, arg in func.in.items() | skip_first %] - [[arg.type.c_type]] [[func.name | to_snake_case]]_in_[[arg.name]]_; // for internal use by Fibre - template static auto get_[[func.name | to_snake_case]]_in_[[arg.name]]_(T* obj) { return Property<[[arg.type.c_type]]>{&obj->[[func.name | to_snake_case]]_in_[[arg.name]]_}; } + [[arg.type.c_name]] [[func.name | to_snake_case]]_in_[[arg.name]]_; // for internal use by Fibre + template static auto get_[[func.name | to_snake_case]]_in_[[arg.name]]_(T* obj) { return Property<[[arg.type.c_name]]>{&obj->[[func.name | to_snake_case]]_in_[[arg.name]]_}; } [%- endfor %] [%- for k, arg in func.out.items() %] - [[arg.type.c_type]] [[func.name | to_snake_case]]_out_[[arg.name]]_; // for internal use by Fibre - template static auto get_[[func.name | to_snake_case]]_out_[[arg.name]]_(T* obj) { return Property{&obj->[[func.name | to_snake_case]]_out_[[arg.name]]_}; } + [[arg.type.c_name]] [[func.name | to_snake_case]]_out_[[arg.name]]_; // for internal use by Fibre + template static auto get_[[func.name | to_snake_case]]_out_[[arg.name]]_(T* obj) { return Property{&obj->[[func.name | to_snake_case]]_out_[[arg.name]]_}; } [%- endfor %] [%- endfor %] }; @@ -74,13 +74,13 @@ public: [%- for _, enum in value_types.items() %] [%- if enum.is_flags %] // this is technically not thread-safe but practically it might be -inline [[enum.c_type]] operator | ([[enum.c_type]] a, [[enum.c_type]] b) { return static_cast<[[enum.c_type]]>(static_cast>(a) | static_cast>(b)); } -inline [[enum.c_type]] operator & ([[enum.c_type]] a, [[enum.c_type]] b) { return static_cast<[[enum.c_type]]>(static_cast>(a) & static_cast>(b)); } -inline [[enum.c_type]] operator ^ ([[enum.c_type]] a, [[enum.c_type]] b) { return static_cast<[[enum.c_type]]>(static_cast>(a) ^ static_cast>(b)); } -inline [[enum.c_type]]& operator |= ([[enum.c_type]] &a, [[enum.c_type]] b) { return reinterpret_cast<[[enum.c_type]]&>(reinterpret_cast&>(a) |= static_cast>(b)); } -inline [[enum.c_type]]& operator &= ([[enum.c_type]] &a, [[enum.c_type]] b) { return reinterpret_cast<[[enum.c_type]]&>(reinterpret_cast&>(a) &= static_cast>(b)); } -inline [[enum.c_type]]& operator ^= ([[enum.c_type]] &a, [[enum.c_type]] b) { return reinterpret_cast<[[enum.c_type]]&>(reinterpret_cast&>(a) ^= static_cast>(b)); } -inline [[enum.c_type]] operator ~ ([[enum.c_type]] a) { return static_cast<[[enum.c_type]]>(~static_cast>(a)); } +inline [[enum.c_name]] operator | ([[enum.c_name]] a, [[enum.c_name]] b) { return static_cast<[[enum.c_name]]>(static_cast>(a) | static_cast>(b)); } +inline [[enum.c_name]] operator & ([[enum.c_name]] a, [[enum.c_name]] b) { return static_cast<[[enum.c_name]]>(static_cast>(a) & static_cast>(b)); } +inline [[enum.c_name]] operator ^ ([[enum.c_name]] a, [[enum.c_name]] b) { return static_cast<[[enum.c_name]]>(static_cast>(a) ^ static_cast>(b)); } +inline [[enum.c_name]]& operator |= ([[enum.c_name]] &a, [[enum.c_name]] b) { return reinterpret_cast<[[enum.c_name]]&>(reinterpret_cast&>(a) |= static_cast>(b)); } +inline [[enum.c_name]]& operator &= ([[enum.c_name]] &a, [[enum.c_name]] b) { return reinterpret_cast<[[enum.c_name]]&>(reinterpret_cast&>(a) &= static_cast>(b)); } +inline [[enum.c_name]]& operator ^= ([[enum.c_name]] &a, [[enum.c_name]] b) { return reinterpret_cast<[[enum.c_name]]&>(reinterpret_cast&>(a) ^= static_cast>(b)); } +inline [[enum.c_name]] operator ~ ([[enum.c_name]] a) { return static_cast<[[enum.c_name]]>(~static_cast>(a)); } [%- endif %] [%- endfor %] diff --git a/Firmware/fibre/cpp/type_info_template.j2 b/Firmware/fibre/cpp/type_info_template.j2 index 70cfae2a..7e6cda57 100644 --- a/Firmware/fibre/cpp/type_info_template.j2 +++ b/Firmware/fibre/cpp/type_info_template.j2 @@ -27,7 +27,7 @@ struct [[intf.fullname | to_pascal_case]]TypeInfo : TypeInfo { introspectable_storage_t res; switch (idx) { [%- for property in intf.attributes.values() %] - case [[loop.index0]]: *(decltype([[intf.c_type]]::get_[[property.name]](std::declval()))*)(&res) = [[intf.c_type]]::get_[[property.name]](ptr); break; + case [[loop.index0]]: *(decltype([[intf.c_name]]::get_[[property.name]](std::declval()))*)(&res) = [[intf.c_name]]::get_[[property.name]](ptr); break; [%- endfor %] } return res; @@ -39,7 +39,7 @@ struct [[intf.fullname | to_pascal_case]]TypeInfo : TypeInfo { template const PropertyInfo [[intf.fullname | to_pascal_case]]TypeInfo::property_table[] = { [%- for property in intf.attributes.values() %] - {"[[property.name]]", &[[(property.type.purename or property.type.fullname) | to_pascal_case]]TypeInfo()))>>::singleton}, + {"[[property.name]]", &[[(property.type.purename or property.type.fullname) | to_pascal_case]]TypeInfo()))>>::singleton}, [%- endfor %] }; template diff --git a/Firmware/interface_generator.py b/Firmware/interface_generator.py index f98cee1e..ad770328 100644 --- a/Firmware/interface_generator.py +++ b/Firmware/interface_generator.py @@ -132,17 +132,17 @@ def to_snake_case(s): return '_'.join(get_words(s)).lower() def to_kebab_case(s): return '-'.join(get_words(s)).lower() value_types = { - 'bool': {'builtin': True, 'fullname': 'bool', 'name': 'bool', 'c_type': 'bool'}, - 'float32': {'builtin': True, 'fullname': 'float32', 'name': 'float32', 'c_type': 'float'}, - 'uint8': {'builtin': True, 'fullname': 'uint8', 'name': 'uint8', 'c_type': 'uint8_t'}, - 'uint16': {'builtin': True, 'fullname': 'uint16', 'name': 'uint16', 'c_type': 'uint16_t'}, - 'uint32': {'builtin': True, 'fullname': 'uint32', 'name': 'uint32', 'c_type': 'uint32_t'}, - 'uint64': {'builtin': True, 'fullname': 'uint64', 'name': 'uint64', 'c_type': 'uint64_t'}, - 'int8': {'builtin': True, 'fullname': 'int8', 'name': 'int8', 'c_type': 'int8_t'}, - 'int16': {'builtin': True, 'fullname': 'int16', 'name': 'int16', 'c_type': 'int16_t'}, - 'int32': {'builtin': True, 'fullname': 'int32', 'name': 'int32', 'c_type': 'int32_t'}, - 'int64': {'builtin': True, 'fullname': 'int64', 'name': 'int64', 'c_type': 'int64_t'}, - 'endpoint_ref': {'builtin': True, 'fullname': 'endpoint_ref', 'name': 'endpoint_ref', 'c_type': 'endpoint_ref_t'}, + 'bool': {'builtin': True, 'fullname': 'bool', 'name': 'bool', 'c_name': 'bool'}, + 'float32': {'builtin': True, 'fullname': 'float32', 'name': 'float32', 'c_name': 'float'}, + 'uint8': {'builtin': True, 'fullname': 'uint8', 'name': 'uint8', 'c_name': 'uint8_t'}, + 'uint16': {'builtin': True, 'fullname': 'uint16', 'name': 'uint16', 'c_name': 'uint16_t'}, + 'uint32': {'builtin': True, 'fullname': 'uint32', 'name': 'uint32', 'c_name': 'uint32_t'}, + 'uint64': {'builtin': True, 'fullname': 'uint64', 'name': 'uint64', 'c_name': 'uint64_t'}, + 'int8': {'builtin': True, 'fullname': 'int8', 'name': 'int8', 'c_name': 'int8_t'}, + 'int16': {'builtin': True, 'fullname': 'int16', 'name': 'int16', 'c_name': 'int16_t'}, + 'int32': {'builtin': True, 'fullname': 'int32', 'name': 'int32', 'c_name': 'int32_t'}, + 'int64': {'builtin': True, 'fullname': 'int64', 'name': 'int64', 'c_name': 'int64_t'}, + 'endpoint_ref': {'builtin': True, 'fullname': 'endpoint_ref', 'name': 'endpoint_ref', 'c_name': 'endpoint_ref_t'}, } enums = {} @@ -157,12 +157,12 @@ def make_property_type(typeargs): if fullname in interfaces: return interfaces[fullname] - c_type = 'Property<' + ('const ' if mode == 'readonly' else '') + value_type['c_type'] + '>' + c_name = 'Property<' + ('const ' if mode == 'readonly' else '') + value_type['c_name'] + '>' prop_type = { 'name': name, 'fullname': fullname, 'purename': 'fibre.Property', - 'c_type': c_type, + 'c_name': c_name, 'value_type': value_type, # TODO: should be a metaarg 'mode': mode, # TODO: should be a metaarg 'builtin': True, @@ -173,17 +173,17 @@ def make_property_type(typeargs): prop_type['functions']['exchange'] = { 'name': 'exchange', 'fullname': join_name(fullname, 'exchange'), - 'in': {'obj': {'name': 'obj', 'type': {'c_type': c_type}}, 'value': {'name': 'value', 'type': value_type, 'optional': True}}, + 'in': {'obj': {'name': 'obj', 'type': {'c_name': c_name}}, 'value': {'name': 'value', 'type': value_type, 'optional': True}}, 'out': {'value': {'name': 'value', 'type': value_type}}, - #'implementation': 'fibre_property_exchange<' + value_type['c_type'] + '>' + #'implementation': 'fibre_property_exchange<' + value_type['c_name'] + '>' } else: prop_type['functions']['read'] = { 'name': 'read', 'fullname': join_name(fullname, 'read'), - 'in': {'obj': {'name': 'obj', 'type': {'c_type': c_type}}}, + 'in': {'obj': {'name': 'obj', 'type': {'c_name': c_name}}}, 'out': {'value': {'name': 'value', 'type': value_type}}, - #'implementation': 'fibre_property_read<' + value_type['c_type'] + '>' + #'implementation': 'fibre_property_read<' + value_type['c_name'] + '>' } interfaces[fullname] = prop_type @@ -204,7 +204,7 @@ def make_ref_type(interface): 'builtin': True, 'name': name, 'fullname': fullname, - 'c_type': interface['fullname'].replace('.', 'Intf::') + 'Intf*' + 'c_name': interface['fullname'].replace('.', 'Intf::') + 'Intf*' } value_types[fullname] = ref_type @@ -281,7 +281,7 @@ def regularize_interface(path, name, elem): # path = 'AnonymousType' + str(max_anonymous_type + 1) elem['name'] = split_name(name)[-1] elem['fullname'] = path = join_name(path, name) - elem['c_type'] = elem.get('c_type', elem['fullname'].replace('.', 'Intf::')) + 'Intf' + elem['c_name'] = elem.get('c_name', elem['fullname'].replace('.', 'Intf::')) + 'Intf' interfaces[path] = elem elem['functions'] = {name: regularize_func(path, name, func, {'obj': {'type': make_ref_type(elem)}}) for name, func in get_dict(elem, 'functions').items()} @@ -301,7 +301,7 @@ def regularize_valuetype(path, name, elem): return elem # will be resolved during type resolution elem['name'] = split_name(name)[-1] elem['fullname'] = path = join_name(path, name) - elem['c_type'] = elem.get('c_type', elem['fullname'].replace('.', 'Intf::')) + elem['c_name'] = elem.get('c_name', elem['fullname'].replace('.', 'Intf::')) value_types[path] = elem if 'flags' in elem: # treat as flags @@ -413,7 +413,7 @@ def generate_endpoint_table(intf, bindto, idx): for k, prop in intf['attributes'].items(): property_value_type = re.findall('^fibre\.Property<([^>]*), (readonly|readwrite)>$', prop['type']['fullname']) #attr_bindto = join_name(bindto, bindings_map.get(join_name(intf['fullname'], k), k + ('_' if len(intf['functions']) or (intf['fullname'] in treat_as_classes) else ''))) - attr_bindto = intf['c_type'] + '::get_' + prop['name'] + '(' + bindto + ')' + attr_bindto = intf['c_name'] + '::get_' + prop['name'] + '(' + bindto + ')' if len(property_value_type): # Special handling for Property<...> attributes: they resolve to one single endpoint endpoint, endpoint_definition = generate_endpoint_for_property(prop, attr_bindto, idx + cnt) @@ -443,14 +443,14 @@ def generate_endpoint_table(intf, bindto, idx): endpoint, endpoint_definition = generate_endpoint_for_property({ 'name': arg['name'], 'type': make_property_type({'fibre.Property.type': arg['type'], 'fibre.Property.mode': 'readwrite'}) - }, intf['c_type'] + '::get_' + func['name'] + '_in_' + k_arg + '_' + '(' + bindto + ')', idx + cnt + 1 + i) + }, intf['c_name'] + '::get_' + func['name'] + '_in_' + k_arg + '_' + '(' + bindto + ')', idx + cnt + 1 + i) endpoints.append(endpoint) in_def.append(endpoint_definition) for i, (k_arg, arg) in enumerate(func['out'].items()): endpoint, endpoint_definition = generate_endpoint_for_property({ 'name': arg['name'], 'type': make_property_type({'fibre.Property.type': arg['type'], 'fibre.Property.mode': 'readonly'}) - }, intf['c_type'] + '::get_' + func['name'] + '_out_' + k_arg + '_' + '(' + bindto + ')', idx + cnt + len(func['in']) + i) + }, intf['c_name'] + '::get_' + func['name'] + '_out_' + k_arg + '_' + '(' + bindto + ')', idx + cnt + len(func['in']) + i) endpoints.append(endpoint) out_def.append(endpoint_definition) From 28f48e8fab6368e3428470c18a44a0b0c0a5d02a Mon Sep 17 00:00:00 2001 From: Samuel Sadok Date: Fri, 22 May 2020 21:12:14 +0200 Subject: [PATCH 12/28] [interface autogen] remove hardcoded intf name --- Firmware/Tupfile.lua | 2 +- Firmware/communication/communication.cpp | 2 ++ Firmware/interface_generator.py | 13 +++++++++---- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/Firmware/Tupfile.lua b/Firmware/Tupfile.lua index 0e636fd2..2d4b7341 100644 --- a/Firmware/Tupfile.lua +++ b/Firmware/Tupfile.lua @@ -3,7 +3,7 @@ tup.include('build.lua') tup.frule{inputs={'fibre/cpp/interfaces_template.j2'}, command='python3 interface_generator.py --definitions odrive-interface.yaml --template %f --output %o', outputs='autogen/interfaces.hpp'} tup.frule{inputs={'fibre/cpp/function_stubs_template.j2'}, command='python3 interface_generator.py --definitions odrive-interface.yaml --template %f --output %o', outputs='autogen/function_stubs.hpp'} -tup.frule{inputs={'fibre/cpp/endpoints_template.j2'}, command='python3 interface_generator.py --definitions odrive-interface.yaml --template %f --output %o', outputs='autogen/endpoints.hpp'} +tup.frule{inputs={'fibre/cpp/endpoints_template.j2'}, command='python3 interface_generator.py --definitions odrive-interface.yaml --generate-endpoints Odrive --template %f --output %o', outputs='autogen/endpoints.hpp'} tup.frule{inputs={'fibre/cpp/type_info_template.j2'}, command='python3 interface_generator.py --definitions odrive-interface.yaml --template %f --output %o', outputs='autogen/type_info.hpp'} tup.frule{command='python3 interface_generator.py --definitions odrive-interface.yaml --template enums_template.j2 --output ../tools/odrive/enums.py'} diff --git a/Firmware/communication/communication.cpp b/Firmware/communication/communication.cpp index 4e732a9c..794debaf 100644 --- a/Firmware/communication/communication.cpp +++ b/Firmware/communication/communication.cpp @@ -95,4 +95,6 @@ int _write(int file, const char* data, int len) { #include "../autogen/function_stubs.hpp" + +ODrive& ep_root = odrv; #include "../autogen/endpoints.hpp" diff --git a/Firmware/interface_generator.py b/Firmware/interface_generator.py index ad770328..1acfc747 100644 --- a/Firmware/interface_generator.py +++ b/Firmware/interface_generator.py @@ -479,6 +479,8 @@ parser.add_argument("-t", "--template", type=argparse.FileType('r'), help="the code template") parser.add_argument("-o", "--output", type=argparse.FileType('w'), default='-', help="path of the generated output") +parser.add_argument("--generate-endpoints", type=str, nargs='?', + help="if specified, an endpoint table will be generated and passed to the template for the specified interface") args = parser.parse_args() if args.version: @@ -556,10 +558,13 @@ for k, item in list(enums.items()): item['parent'] = parent - -endpoints, embedded_endpoint_definitions, _ = generate_endpoint_table(interfaces['Odrive'], '&odrv', 1) # TODO: make user-configurable -embedded_endpoint_definitions = [{'name': '', 'id': 0, 'type': 'json', 'access': 'r'}] + embedded_endpoint_definitions -endpoints = [{'id': 0, 'function': {'fullname': 'endpoint0_handler', 'in': {}, 'out': {}}, 'bindings': {}}] + endpoints +if args.generate_endpoints: + endpoints, embedded_endpoint_definitions, _ = generate_endpoint_table(interfaces[args.generate_endpoints], '&ep_root', 1) # TODO: make user-configurable + embedded_endpoint_definitions = [{'name': '', 'id': 0, 'type': 'json', 'access': 'r'}] + embedded_endpoint_definitions + endpoints = [{'id': 0, 'function': {'fullname': 'endpoint0_handler', 'in': {}, 'out': {}}, 'bindings': {}}] + endpoints +else: + embedded_endpoint_definitions = None + endpoints = None # Render template From 49f789148c2c025efb2a3bec135517c5ecb49881 Mon Sep 17 00:00:00 2001 From: Samuel Sadok Date: Fri, 22 May 2020 23:51:17 +0200 Subject: [PATCH 13/28] [interface autogen] fix stack overflow --- Firmware/fibre/cpp/endpoints_template.j2 | 21 ++++++++++--------- .../fibre/cpp/include/fibre/introspection.hpp | 1 + Firmware/fibre/cpp/interfaces_template.j2 | 5 +++++ 3 files changed, 17 insertions(+), 10 deletions(-) diff --git a/Firmware/fibre/cpp/endpoints_template.j2 b/Firmware/fibre/cpp/endpoints_template.j2 index 975d9422..db9b8dbd 100644 --- a/Firmware/fibre/cpp/endpoints_template.j2 +++ b/Firmware/fibre/cpp/endpoints_template.j2 @@ -15,6 +15,12 @@ #include +// Note: with -Og the functions with large switch statements reserves a huge amount +// of stack space because they reserves separate space for the stack frame of each +// of the inlined functions. +// The minimum known set of flags to prevent this is `-O1 -fipa-sra`. +// `-O2`, `-O3` and `-Os` are supersets of this. + #pragma GCC push_options #pragma GCC optimize ("s") @@ -25,24 +31,18 @@ const size_t embedded_json_length = sizeof(embedded_json) - 1; const uint16_t json_crc_ = calc_crc16(PROTOCOL_VERSION, embedded_json, embedded_json_length); const uint32_t json_version_id_ = (json_crc_ << 16) | calc_crc16(json_crc_, embedded_json, embedded_json_length); -Introspectable get_property(size_t idx) { +static void get_property(Introspectable& result, size_t idx) { switch (idx) { [%- for endpoint in endpoints %] [%- if endpoint.function.name == 'exchange' and endpoint.in_bindings | list == ['obj'] %] - case [[endpoint.id]]: return FibrePropertyTypeInfo<[[endpoint.function.in['obj'].type.c_name]]>::make_introspectable([[endpoint.in_bindings['obj']]]); + case [[endpoint.id]]: { [[(endpoint.in_bindings['obj'] + '$') | replace(')$', ', &result.storage_)')]]; result.type_info_ = &FibrePropertyTypeInfo<[[endpoint.function.in['obj'].type.c_name]]>::singleton; } break; [%- endif %] [%- endfor %] - default: return {}; + default: break; } } -// Note: with -Og this function reserves a huge amount of stack space because it -// reserves separate space for the stack frame of each of the inlined functions. -// The minimum known set of flags to prevent this is `-O1 -fipa-sra`. -// `-O2` is a superset of this so that's what we use here. -//bool endpoint_handler(int idx, cbufptr_t* input_buffer, bufptr_t* output_buffer) __attribute__ ((optimize(2))); - bool endpoint_handler(int idx, cbufptr_t* input_buffer, bufptr_t* output_buffer) { //Introspectable property = get_property(idx); //if property.is_valid() @@ -77,7 +77,8 @@ bool set_endpoint_from_float(endpoint_ref_t endpoint_ref, float value) { return false; } - Introspectable property = get_property(endpoint_ref.endpoint_id); + Introspectable property{}; + get_property(property, endpoint_ref.endpoint_id); const FloatSettableTypeInfo* type_info = dynamic_cast(property.get_type_info()); return type_info && type_info->set_float(property, value); } diff --git a/Firmware/fibre/cpp/include/fibre/introspection.hpp b/Firmware/fibre/cpp/include/fibre/introspection.hpp index d490ff71..f52b73a8 100644 --- a/Firmware/fibre/cpp/include/fibre/introspection.hpp +++ b/Firmware/fibre/cpp/include/fibre/introspection.hpp @@ -106,6 +106,7 @@ private: return {}; } +public: // these should technically be protected but are public for optimization reasons // We use this storage to hold generic small objects. Usually that's a pointer // but sometimes it's an on-demand constructed Property<...>. // Caution: only put objects in here which are trivially copyable, movable diff --git a/Firmware/fibre/cpp/interfaces_template.j2 b/Firmware/fibre/cpp/interfaces_template.j2 index 60c92b35..f4eb3163 100644 --- a/Firmware/fibre/cpp/interfaces_template.j2 +++ b/Firmware/fibre/cpp/interfaces_template.j2 @@ -41,10 +41,13 @@ public: [%- if property.type.fullname.startswith("fibre.Property") %] [%- if not property.c_getter and not property.c_setter %] template static inline auto get_[[property.name]](T* obj) { return [[property.type.c_name]]{&obj->[[property.c_name]]}; } + template static inline void get_[[property.name]](T* obj, void* ptr) { new (ptr) [[property.type.c_name]]{&obj->[[property.c_name]]}; }[# these are for the set_endpoint_from_float function. This is unmaintainable and should go away #] [%- elif not property.c_setter %] template static inline auto get_[[property.name]](T* obj) { return [[property.type.c_name]]{obj, [](void* ctx){ return ([[property.type.value_type.c_name]])((T*)ctx)->[[property.c_getter]]; }}; } + template static inline void get_[[property.name]](T* obj, void* ptr) { new (ptr) [[property.type.c_name]]{obj, [](void* ctx){ return ([[property.type.value_type.c_name]])((T*)ctx)->[[property.c_getter]]; }}; } [%- else %] template static inline auto get_[[property.name]](T* obj) { return [[property.type.c_name]]{obj, [](void* ctx){ return ([[property.type.value_type.c_name]])((T*)ctx)->[[property.c_getter]]; }, [](void* ctx, [[property.type.value_type.c_name]] value){ ((T*)ctx)->[[property.c_setter]](value); }}; } + template static inline void get_[[property.name]](T* obj, void* ptr) { new (ptr) [[property.type.c_name]]{obj, [](void* ctx){ return ([[property.type.value_type.c_name]])((T*)ctx)->[[property.c_getter]]; }, [](void* ctx, [[property.type.value_type.c_name]] value){ ((T*)ctx)->[[property.c_setter]](value); }}; } [%- endif %] [%- else %] template static inline auto get_[[property.name]](T* obj) { return &obj->[[property.c_name]]; } @@ -58,10 +61,12 @@ public: [%- for k, arg in func.in.items() | skip_first %] [[arg.type.c_name]] [[func.name | to_snake_case]]_in_[[arg.name]]_; // for internal use by Fibre template static auto get_[[func.name | to_snake_case]]_in_[[arg.name]]_(T* obj) { return Property<[[arg.type.c_name]]>{&obj->[[func.name | to_snake_case]]_in_[[arg.name]]_}; } + template static void get_[[func.name | to_snake_case]]_in_[[arg.name]]_(T* obj, void* ptr) { new (ptr) Property<[[arg.type.c_name]]>{&obj->[[func.name | to_snake_case]]_in_[[arg.name]]_}; } [%- endfor %] [%- for k, arg in func.out.items() %] [[arg.type.c_name]] [[func.name | to_snake_case]]_out_[[arg.name]]_; // for internal use by Fibre template static auto get_[[func.name | to_snake_case]]_out_[[arg.name]]_(T* obj) { return Property{&obj->[[func.name | to_snake_case]]_out_[[arg.name]]_}; } + template static void get_[[func.name | to_snake_case]]_out_[[arg.name]]_(T* obj, void* ptr) { new (ptr) Property{&obj->[[func.name | to_snake_case]]_out_[[arg.name]]_}; } [%- endfor %] [%- endfor %] }; From 8a3213cb881e9148b68c8c6a770893d0234fa8d0 Mon Sep 17 00:00:00 2001 From: Samuel Sadok Date: Fri, 22 May 2020 23:53:53 +0200 Subject: [PATCH 14/28] increase test tolerance (yes that's gonna be a thing now) --- tools/odrive/tests/analog_input_test.py | 3 ++- tools/odrive/tests/pwm_input_test.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/tools/odrive/tests/analog_input_test.py b/tools/odrive/tests/analog_input_test.py index e21553a3..8932e5e7 100644 --- a/tools/odrive/tests/analog_input_test.py +++ b/tools/odrive/tests/analog_input_test.py @@ -103,7 +103,8 @@ class TestAnalogInput(): # Expect mean error to be at most 2% (of the full scale). # Expect there to be less than 2% outliers, where an outlier is anything that is more than 5% (of full scale) away from the expected value. full_range = abs(max_val - min_val) - slope, offset, fitted_curve = fit_sawtooth(data, min_val, max_val) + slope, offset, fitted_curve = fit_sawtooth(data, min_val, max_val, sigma=20) + save_log(np.concatenate([data, np.array([fitted_curve]).transpose()], 1)) test_assert_eq(slope, (max_val - min_val) / period, accuracy=0.005) test_curve_fit(data, fitted_curve, max_mean_err = full_range * 0.02, inlier_range = full_range * 0.05, max_outliers = len(data[:,0]) * 0.02) diff --git a/tools/odrive/tests/pwm_input_test.py b/tools/odrive/tests/pwm_input_test.py index 0adc9243..a1945997 100644 --- a/tools/odrive/tests/pwm_input_test.py +++ b/tools/odrive/tests/pwm_input_test.py @@ -81,7 +81,7 @@ class TestPwmInput(): full_scale = max_val - min_val slope, offset, fitted_curve = fit_sawtooth(data, min_val, max_val) test_assert_eq(slope, full_scale / 1.0, accuracy=0.001) - test_curve_fit(data, fitted_curve, max_mean_err = full_scale * 0.02, inlier_range = full_scale * 0.05, max_outliers = len(data[:,0]) * 0.01) + test_curve_fit(data, fitted_curve, max_mean_err = full_scale * 0.05, inlier_range = full_scale * 0.05, max_outliers = len(data[:,0]) * 0.01) From b4213a83e324a1e8e401ba9131bb36237cad7b5e Mon Sep 17 00:00:00 2001 From: Samuel Sadok Date: Fri, 29 May 2020 09:54:42 +0200 Subject: [PATCH 15/28] upgrade interface_generator.py for doc autogen --- Firmware/interface_generator.py | 124 +++++++++++++++++++++++++------- Firmware/odrive-interface.yaml | 6 +- 2 files changed, 101 insertions(+), 29 deletions(-) diff --git a/Firmware/interface_generator.py b/Firmware/interface_generator.py index 1acfc747..e83c91d8 100644 --- a/Firmware/interface_generator.py +++ b/Firmware/interface_generator.py @@ -16,6 +16,8 @@ definitions: properties: c_is_class: {type: boolean} c_name: {type: string} + brief: {type: string} + doc: {type: string} functions: type: object additionalProperties: {"$ref": "#/definitions/function"} @@ -64,6 +66,7 @@ definitions: properties: in: {type: object} out: {type: object} + brief: {type: string} doc: {type: string} __line__: {type: object} __column__: {type: object} @@ -132,17 +135,17 @@ def to_snake_case(s): return '_'.join(get_words(s)).lower() def to_kebab_case(s): return '-'.join(get_words(s)).lower() value_types = { - 'bool': {'builtin': True, 'fullname': 'bool', 'name': 'bool', 'c_name': 'bool'}, - 'float32': {'builtin': True, 'fullname': 'float32', 'name': 'float32', 'c_name': 'float'}, - 'uint8': {'builtin': True, 'fullname': 'uint8', 'name': 'uint8', 'c_name': 'uint8_t'}, - 'uint16': {'builtin': True, 'fullname': 'uint16', 'name': 'uint16', 'c_name': 'uint16_t'}, - 'uint32': {'builtin': True, 'fullname': 'uint32', 'name': 'uint32', 'c_name': 'uint32_t'}, - 'uint64': {'builtin': True, 'fullname': 'uint64', 'name': 'uint64', 'c_name': 'uint64_t'}, - 'int8': {'builtin': True, 'fullname': 'int8', 'name': 'int8', 'c_name': 'int8_t'}, - 'int16': {'builtin': True, 'fullname': 'int16', 'name': 'int16', 'c_name': 'int16_t'}, - 'int32': {'builtin': True, 'fullname': 'int32', 'name': 'int32', 'c_name': 'int32_t'}, - 'int64': {'builtin': True, 'fullname': 'int64', 'name': 'int64', 'c_name': 'int64_t'}, - 'endpoint_ref': {'builtin': True, 'fullname': 'endpoint_ref', 'name': 'endpoint_ref', 'c_name': 'endpoint_ref_t'}, + 'bool': {'builtin': True, 'fullname': 'bool', 'name': 'bool', 'c_name': 'bool', 'py_type': 'bool'}, + 'float32': {'builtin': True, 'fullname': 'float32', 'name': 'float32', 'c_name': 'float', 'py_type': 'float'}, + 'uint8': {'builtin': True, 'fullname': 'uint8', 'name': 'uint8', 'c_name': 'uint8_t', 'py_type': 'int'}, + 'uint16': {'builtin': True, 'fullname': 'uint16', 'name': 'uint16', 'c_name': 'uint16_t', 'py_type': 'int'}, + 'uint32': {'builtin': True, 'fullname': 'uint32', 'name': 'uint32', 'c_name': 'uint32_t', 'py_type': 'int'}, + 'uint64': {'builtin': True, 'fullname': 'uint64', 'name': 'uint64', 'c_name': 'uint64_t', 'py_type': 'int'}, + 'int8': {'builtin': True, 'fullname': 'int8', 'name': 'int8', 'c_name': 'int8_t', 'py_type': 'int'}, + 'int16': {'builtin': True, 'fullname': 'int16', 'name': 'int16', 'c_name': 'int16_t', 'py_type': 'int'}, + 'int32': {'builtin': True, 'fullname': 'int32', 'name': 'int32', 'c_name': 'int32_t', 'py_type': 'int'}, + 'int64': {'builtin': True, 'fullname': 'int64', 'name': 'int64', 'c_name': 'int64_t', 'py_type': 'int'}, + 'endpoint_ref': {'builtin': True, 'fullname': 'endpoint_ref', 'name': 'endpoint_ref', 'c_name': 'endpoint_ref_t', 'py_type': '[not implemented]'}, } enums = {} @@ -234,7 +237,7 @@ def regularize_func(path, name, elem, prepend_args): for n, arg in get_dict(elem, 'out').items()} return elem -def regularize_attribute(path, name, elem, c_is_class): +def regularize_attribute(parent, name, elem, c_is_class): if elem is None: elem = {} if isinstance(elem, str): @@ -249,7 +252,8 @@ def regularize_attribute(path, name, elem, c_is_class): if 'nullflag' in elem: elem['type']['nullflag'] = elem.pop('nullflag') elem['name'] = name - elem['fullname'] = join_name(path, name) + elem['fullname'] = join_name(parent['fullname'], name) + elem['parent'] = parent elem['typeargs'] = elem.get('typeargs', {}) elem['c_name'] = elem.get('c_name', None) or (elem['name'] + ('_' if c_is_class else '')) if ('c_getter' in elem) or ('c_setter' in elem): @@ -263,11 +267,11 @@ def regularize_attribute(path, name, elem, c_is_class): if elem['typeargs']['fibre.Property.mode'] == 'readonly' and 'c_setter' in elem: elem.pop('c_setter') elif ('flags' in elem['type']) or ('values' in elem['type']): elem['typeargs']['fibre.Property.mode'] = elem['typeargs'].get('fibre.Property.mode', None) or 'readwrite' - elem['typeargs']['fibre.Property.type'] = regularize_valuetype(path, to_pascal_case(name), elem['type']) + elem['typeargs']['fibre.Property.type'] = regularize_valuetype(parent['fullname'], to_pascal_case(name), elem['type']) elem['type'] = 'fibre.Property' if elem['typeargs']['fibre.Property.mode'] == 'readonly' and 'c_setter' in elem: elem.pop('c_setter') else: - elem['type'] = regularize_interface(path, to_pascal_case(name), elem['type']) + elem['type'] = regularize_interface(parent['fullname'], to_pascal_case(name), elem['type']) return elem @@ -288,7 +292,7 @@ def regularize_interface(path, name, elem): if not 'c_is_class' in elem: raise Exception(elem) treat_as_class = elem['c_is_class'] # TODO: add command line arg to make this selectively optional - elem['attributes'] = {name: regularize_attribute(path, name, prop, treat_as_class) + elem['attributes'] = {name: regularize_attribute(elem, name, prop, treat_as_class) for name, prop in get_dict(elem, 'attributes').items()} elem['interfaces'] = [] elem['enums'] = [] @@ -308,6 +312,7 @@ def regularize_valuetype(path, name, elem): bit = 0 for k, v in elem['flags'].items(): elem['flags'][k] = elem['flags'][k] or {} + elem['flags'][k]['name'] = k current_bit = elem['flags'][k].get('bit', bit) elem['flags'][k]['bit'] = current_bit elem['flags'][k]['value'] = 0 if current_bit is None else (1 << current_bit) @@ -323,6 +328,7 @@ def regularize_valuetype(path, name, elem): val = 0 for k, v in elem['values'].items(): elem['values'][k] = elem['values'][k] or {} + elem['values'][k]['name'] = k val = elem['values'][k].get('value', val) elem['values'][k]['value'] = val val += 1 @@ -477,8 +483,11 @@ parser.add_argument("-d", "--definitions", type=argparse.FileType('r'), nargs='+ help="the YAML interface definition file(s) used to generate the code") parser.add_argument("-t", "--template", type=argparse.FileType('r'), help="the code template") -parser.add_argument("-o", "--output", type=argparse.FileType('w'), default='-', +group = parser.add_mutually_exclusive_group(required=True) +group.add_argument("-o", "--output", type=argparse.FileType('w'), help="path of the generated output") +group.add_argument("--outputs", type=str, + help="path pattern for the generated outputs. One output is generated for each interface. Use # as placeholder for the interface name.") parser.add_argument("--generate-endpoints", type=str, nargs='?', help="if specified, an endpoint table will be generated and passed to the template for the specified interface") args = parser.parse_args() @@ -490,7 +499,6 @@ if args.version: definition_files = args.definitions template_file = args.template -output_file = args.output # Load definition files @@ -525,6 +533,11 @@ if args.verbose: print('Known interfaces: ' + ''.join([('\n ' + k) for k in interfaces.keys()])) print('Known value types: ' + ''.join([('\n ' + k) for k in value_types.keys()])) +clashing_names = list(set(value_types.keys()).intersection(set(interfaces.keys()))) +if len(clashing_names): + print(f"**Error**: Found both an interface and a value type with the name {clashing_names[0]}. This is not allowed, interfaces and value types (such as enums) share the same namespace.", file=sys.stderr) + sys.exit(1) + # Resolve all types into references for _, item in list(interfaces.items()): for _, prop in item['attributes'].items(): @@ -575,6 +588,46 @@ env = jinja2.Environment( variable_start_string='[[', variable_end_string=']]' ) +def tokenize(text, interface, interface_transform, value_type_transform, attribute_transform): + """ + Looks for referencable tokens (interface names, value type names or + attribute names) in a documentation text and runs them through the provided + processing functions. + Tokens are detected by enclosing back-ticks (`). + + interface: The interface type object that defines the scope in which the + tokens should be detected. + interface_transform: A function that takes an interface object as an argument + and returns a string. + value_type_transform: A function that takes a value type object as an argument + and returns a string. + attribute_transform: A function that takes the token strin and an attribute + object as arguments and returns a string. + """ + if text is None or isinstance(text, jinja2.runtime.Undefined): + return text + + def token_transform(token): + token = token.groups()[0] + token_list = split_name(token) + + # Check if this is an attribute reference + attr_intf = interface + for name in token_list: + if not name in attr_intf['attributes']: + attr = None + break + attr = attr_intf['attributes'][name] + attr_intf = attr['type'] + + if not attr is None: + return attribute_transform(token, attr) + + print(f'Warning: cannot resolve "{token}" in ' + interface['fullname']) + return "`" + token + "`" + + return re.sub(r'`([A-Za-z\._]+)`', token_transform, text) + env.filters['to_pascal_case'] = to_pascal_case env.filters['to_camel_case'] = to_camel_case env.filters['to_macro_case'] = to_macro_case @@ -583,15 +636,34 @@ env.filters['to_kebab_case'] = to_kebab_case env.filters['first'] = lambda x: next(iter(x)) env.filters['skip_first'] = lambda x: list(x)[1:] env.filters['to_c_string'] = lambda x: '\n'.join(('"' + line.replace('"', '\\"') + '"') for line in json.dumps(x, separators=(',', ':')).replace('{"name"', '\n{"name"').split('\n')) +env.filters['tokenize'] = tokenize template = env.from_string(template_file.read()) -output = template.render( - interfaces = interfaces, - value_types = value_types, - toplevel_interfaces = toplevel_interfaces, - endpoints = endpoints, - embedded_endpoint_definitions = embedded_endpoint_definitions -) +template_args = { + 'interfaces': interfaces, + 'value_types': value_types, + 'toplevel_interfaces': toplevel_interfaces, + 'endpoints': endpoints, + 'embedded_endpoint_definitions': embedded_endpoint_definitions +} -output_file.write(output) +if not args.output is None: + output = template.render(**template_args) + args.output.write(output) +else: + assert('#' in args.outputs) + + for k, intf in interfaces.items(): + if split_name(k)[0] == 'fibre': + continue # TODO: remove special case + output = template.render(interface = intf, **template_args) + with open(args.outputs.replace('#', k.lower()), 'w') as output_file: + output_file.write(output) + + for k, enum in value_types.items(): + if enum.get('builtin', False) or not enum.get('is_enum', False): + continue + output = template.render(enum = enum, **template_args) + with open(args.outputs.replace('#', k.lower()), 'w') as output_file: + output_file.write(output) diff --git a/Firmware/odrive-interface.yaml b/Firmware/odrive-interface.yaml index 93158017..8d319fe8 100644 --- a/Firmware/odrive-interface.yaml +++ b/Firmware/odrive-interface.yaml @@ -272,8 +272,8 @@ interfaces: ramp_distance: float32 accel: float32 vel: float32 - sensorless_ramp: LockinState - general_lockin: LockinState + sensorless_ramp: LockinConfig + general_lockin: LockinConfig can_node_id: type: uint32 doc: Both axes will have the same id to start @@ -292,7 +292,7 @@ interfaces: clear_errors: doc: Check the watchdog timer for expiration. Also sets the watchdog error bit if expired. - Axis.LockinState: + Axis.LockinConfig: c_is_class: False attributes: current: From 6902fd6b34ce0137b48dcf3ba26057c13163c875 Mon Sep 17 00:00:00 2001 From: Samuel Sadok Date: Fri, 29 May 2020 10:19:10 +0200 Subject: [PATCH 16/28] rename Odrive interface to ODrive --- Firmware/MotorControl/odrive_main.h | 2 +- Firmware/Tupfile.lua | 2 +- Firmware/communication/ascii_protocol.cpp | 2 +- Firmware/communication/interface_can.hpp | 2 +- Firmware/interface_generator.py | 9 ++++++--- Firmware/odrive-interface.yaml | 8 +++++--- tools/odrive/enums.py | 4 ++-- 7 files changed, 17 insertions(+), 12 deletions(-) diff --git a/Firmware/MotorControl/odrive_main.h b/Firmware/MotorControl/odrive_main.h index 20b1124a..62bd4f57 100644 --- a/Firmware/MotorControl/odrive_main.h +++ b/Firmware/MotorControl/odrive_main.h @@ -214,7 +214,7 @@ enum TimingLog_t { // general system functions defined in main.cpp -class ODrive : public OdriveIntf { +class ODrive : public ODriveIntf { public: void save_configuration() override; void erase_configuration() override; diff --git a/Firmware/Tupfile.lua b/Firmware/Tupfile.lua index 2d4b7341..5f8843e9 100644 --- a/Firmware/Tupfile.lua +++ b/Firmware/Tupfile.lua @@ -3,7 +3,7 @@ tup.include('build.lua') tup.frule{inputs={'fibre/cpp/interfaces_template.j2'}, command='python3 interface_generator.py --definitions odrive-interface.yaml --template %f --output %o', outputs='autogen/interfaces.hpp'} tup.frule{inputs={'fibre/cpp/function_stubs_template.j2'}, command='python3 interface_generator.py --definitions odrive-interface.yaml --template %f --output %o', outputs='autogen/function_stubs.hpp'} -tup.frule{inputs={'fibre/cpp/endpoints_template.j2'}, command='python3 interface_generator.py --definitions odrive-interface.yaml --generate-endpoints Odrive --template %f --output %o', outputs='autogen/endpoints.hpp'} +tup.frule{inputs={'fibre/cpp/endpoints_template.j2'}, command='python3 interface_generator.py --definitions odrive-interface.yaml --generate-endpoints ODrive --template %f --output %o', outputs='autogen/endpoints.hpp'} tup.frule{inputs={'fibre/cpp/type_info_template.j2'}, command='python3 interface_generator.py --definitions odrive-interface.yaml --template %f --output %o', outputs='autogen/type_info.hpp'} tup.frule{command='python3 interface_generator.py --definitions odrive-interface.yaml --template enums_template.j2 --output ../tools/odrive/enums.py'} diff --git a/Firmware/communication/ascii_protocol.cpp b/Firmware/communication/ascii_protocol.cpp index 632ceea3..39f4d891 100644 --- a/Firmware/communication/ascii_protocol.cpp +++ b/Firmware/communication/ascii_protocol.cpp @@ -29,7 +29,7 @@ /* Private variables ---------------------------------------------------------*/ -static Introspectable root_obj = OdriveTypeInfo::make_introspectable(odrv); +static Introspectable root_obj = ODriveTypeInfo::make_introspectable(odrv); /* Private function prototypes -----------------------------------------------*/ /* Function implementations --------------------------------------------------*/ diff --git a/Firmware/communication/interface_can.hpp b/Firmware/communication/interface_can.hpp index b4864611..19855047 100644 --- a/Firmware/communication/interface_can.hpp +++ b/Firmware/communication/interface_can.hpp @@ -19,7 +19,7 @@ enum { CAN_BAUD_1M = 1000000 }; -class ODriveCAN : public OdriveIntf::CanIntf { +class ODriveCAN : public ODriveIntf::CanIntf { public: struct Config_t { uint32_t baud_rate = CAN_BAUD_250K; diff --git a/Firmware/interface_generator.py b/Firmware/interface_generator.py index e83c91d8..14184f1f 100644 --- a/Firmware/interface_generator.py +++ b/Firmware/interface_generator.py @@ -78,6 +78,7 @@ properties: ns: {type: string} version: {type: string} summary: {type: string} + dictionary: {type: array, items: {type: string}} interfaces: type: object additionalProperties: { "$ref": "#/definitions/interface" } @@ -105,13 +106,14 @@ class SafeLineLoader(yaml.SafeLoader): # #mapping['__column__'] = node.start_mark.column + 1 # return mapping - +dictionary = [] def get_words(string): """ Splits a string in PascalCase into a list of lower case words """ - return [w.lower() for w in re.findall('[a-z0-9]+|[A-Z][a-z0-9]*', string)] + regex = ''.join((re.escape(w) + '|') for w in dictionary) + '[a-z0-9]+|[A-Z][a-z0-9]*' + return [(w if w in dictionary else w.lower()) for w in re.findall(regex, string)] def join_name(*names, delimiter: str = '.'): """ @@ -128,7 +130,7 @@ def split_name(name, delimiter: str = '.'): yield c if (parenthesis_depth == 0) or (c != delimiter) else ':' return [part.replace(':', '.') for part in ''.join(replace_delimiter_in_parentheses()).split('.')] -def to_pascal_case(s): return ''.join([w.title() for w in get_words(s)]) +def to_pascal_case(s): return ''.join([(w.title() if not w in dictionary else w) for w in get_words(s)]) def to_camel_case(s): return ''.join([(c.lower() if i == 0 else c) for i, c in enumerate(''.join([w.title() for w in get_words(s)]))]) def to_macro_case(s): return '_'.join(get_words(s)).upper() def to_snake_case(s): return '_'.join(get_words(s)).lower() @@ -519,6 +521,7 @@ for definition_file in definition_files: raise Exception(err.message + '\nat ' + str(list(err.absolute_path))) interfaces = {**interfaces, **get_dict(file_content, 'interfaces')} value_types = {**value_types, **get_dict(file_content, 'valuetypes')} + dictionary += file_content.get('dictionary', None) or [] # Preprocess definitions diff --git a/Firmware/odrive-interface.yaml b/Firmware/odrive-interface.yaml index 8d319fe8..36309b82 100644 --- a/Firmware/odrive-interface.yaml +++ b/Firmware/odrive-interface.yaml @@ -3,8 +3,10 @@ version: 0.0.1 ns: com.odriverobotics summary: ODrive Interface Definitions +dictionary: [ODrive] # Prevent the word 'ODrive' from being detected as two words 'O' and 'Drive' + interfaces: - Odrive: + ODrive: c_is_class: True attributes: vbus_voltage: readonly float32 @@ -162,7 +164,7 @@ interfaces: reboot: enter_dfu_mode: - Odrive.Can: + ODrive.Can: c_is_class: True attributes: error: @@ -625,7 +627,7 @@ interfaces: valuetypes: - Odrive.Can.Protocol: + ODrive.Can.Protocol: values: {Simple: } Axis.AxisState: # TODO: remove redundant "Axis" in name diff --git a/tools/odrive/enums.py b/tools/odrive/enums.py index 55bc64e3..466f11b6 100644 --- a/tools/odrive/enums.py +++ b/tools/odrive/enums.py @@ -1,7 +1,7 @@ # TODO: This file is dangerous because the enums could potentially change between API versions. Should transmit as part of the JSON. -# Odrive.Can.Protocol +# ODrive.Can.Protocol PROTOCOL_SIMPLE = 0 # Axis.AxisState @@ -47,7 +47,7 @@ MOTOR_TYPE_HIGH_CURRENT = 0 MOTOR_TYPE_GIMBAL = 2 MOTOR_TYPE_ACIM = 3 -# Odrive.Can.Error +# ODrive.Can.Error CAN_ERROR_NONE = 0x00000000 CAN_ERROR_DUPLICATE_CAN_IDS = 0x00000001 From fb7cabef5b54c96deea5f479f9a8c09260a4816b Mon Sep 17 00:00:00 2001 From: Samuel Sadok Date: Fri, 29 May 2020 10:24:36 +0200 Subject: [PATCH 17/28] make all other interfaces children of ODrive --- Firmware/MotorControl/axis.hpp | 2 +- Firmware/MotorControl/controller.hpp | 2 +- Firmware/MotorControl/encoder.hpp | 2 +- Firmware/MotorControl/motor.hpp | 2 +- .../MotorControl/sensorless_estimator.hpp | 2 +- Firmware/odrive-interface.yaml | 28 +++++++++---------- tools/odrive/enums.py | 26 ++++++++--------- 7 files changed, 32 insertions(+), 32 deletions(-) diff --git a/Firmware/MotorControl/axis.hpp b/Firmware/MotorControl/axis.hpp index 598da2a8..480de844 100644 --- a/Firmware/MotorControl/axis.hpp +++ b/Firmware/MotorControl/axis.hpp @@ -5,7 +5,7 @@ #error "This file should not be included directly. Include odrive_main.h instead." #endif -class Axis : public AxisIntf { +class Axis : public ODriveIntf::AxisIntf { public: struct LockinConfig_t { float current = 10.0f; // [A] diff --git a/Firmware/MotorControl/controller.hpp b/Firmware/MotorControl/controller.hpp index 66f2dbb1..be84d159 100644 --- a/Firmware/MotorControl/controller.hpp +++ b/Firmware/MotorControl/controller.hpp @@ -5,7 +5,7 @@ #error "This file should not be included directly. Include odrive_main.h instead." #endif -class Controller : public ControllerIntf { +class Controller : public ODriveIntf::ControllerIntf { public: typedef struct { uint32_t index = 0; diff --git a/Firmware/MotorControl/encoder.hpp b/Firmware/MotorControl/encoder.hpp index 77f40068..cb97a45b 100644 --- a/Firmware/MotorControl/encoder.hpp +++ b/Firmware/MotorControl/encoder.hpp @@ -5,7 +5,7 @@ #error "This file should not be included directly. Include odrive_main.h instead." #endif -class Encoder : public EncoderIntf { +class Encoder : public ODriveIntf::EncoderIntf { public: const uint32_t MODE_FLAG_ABS = 0x100; diff --git a/Firmware/MotorControl/motor.hpp b/Firmware/MotorControl/motor.hpp index a7e87139..6e449111 100644 --- a/Firmware/MotorControl/motor.hpp +++ b/Firmware/MotorControl/motor.hpp @@ -7,7 +7,7 @@ #include "drv8301.h" -class Motor : public MotorIntf { +class Motor : public ODriveIntf::MotorIntf { public: struct Iph_BC_t { float phB; diff --git a/Firmware/MotorControl/sensorless_estimator.hpp b/Firmware/MotorControl/sensorless_estimator.hpp index 99c339be..95992ae0 100644 --- a/Firmware/MotorControl/sensorless_estimator.hpp +++ b/Firmware/MotorControl/sensorless_estimator.hpp @@ -1,7 +1,7 @@ #ifndef __SENSORLESS_ESTIMATOR_HPP #define __SENSORLESS_ESTIMATOR_HPP -class SensorlessEstimator : public SensorlessEstimatorIntf { +class SensorlessEstimator : public ODriveIntf::SensorlessEstimatorIntf { public: struct Config_t { float observer_gain = 1000.0f; // [rad/s] diff --git a/Firmware/odrive-interface.yaml b/Firmware/odrive-interface.yaml index 36309b82..f17aca63 100644 --- a/Firmware/odrive-interface.yaml +++ b/Firmware/odrive-interface.yaml @@ -178,14 +178,14 @@ interfaces: functions: set_baud_rate: {in: {baudRate: uint32}} - Endpoint: + ODrive.Endpoint: c_is_class: False attributes: endpoint: endpoint_ref min: float32 max: float32 - Axis: + ODrive.Axis: c_is_class: True attributes: error: @@ -294,7 +294,7 @@ interfaces: clear_errors: doc: Check the watchdog timer for expiration. Also sets the watchdog error bit if expired. - Axis.LockinConfig: + ODrive.Axis.LockinConfig: c_is_class: False attributes: current: @@ -320,7 +320,7 @@ interfaces: finish_on_enc_idx: bool - Motor: + ODrive.Motor: c_is_class: True attributes: error: @@ -443,7 +443,7 @@ interfaces: acim_autoflux_decay_gain: float32 - Controller: + ODrive.Controller: c_is_class: True attributes: error: @@ -527,7 +527,7 @@ interfaces: start_anticogging_calibration: - Encoder: + ODrive.Encoder: c_is_class: True attributes: error: @@ -580,7 +580,7 @@ interfaces: set_linear_count: {in: {count: int32}} - SensorlessEstimator: + ODrive.SensorlessEstimator: c_is_class: True attributes: error: @@ -600,7 +600,7 @@ interfaces: pm_flux_linkage: float32 - TrapezoidalTrajectory: + ODrive.TrapezoidalTrajectory: c_is_class: True attributes: config: @@ -611,7 +611,7 @@ interfaces: decel_limit: float32 - Endstop: + ODrive.Endstop: c_is_class: True attributes: endstop_state: readonly bool @@ -630,7 +630,7 @@ valuetypes: ODrive.Can.Protocol: values: {Simple: } - Axis.AxisState: # TODO: remove redundant "Axis" in name + ODrive.Axis.AxisState: # TODO: remove redundant "Axis" in name values: Undefined: doc: will fall through to idle @@ -656,7 +656,7 @@ valuetypes: Homing: doc: run axis homing function - Encoder.Mode: + ODrive.Encoder.Mode: values: Incremental: Hall: @@ -671,7 +671,7 @@ valuetypes: value: 0x102 doc: not yet implemented - Controller.ControlMode: + ODrive.Controller.ControlMode: values: # Note: these should be sorted from lowest level of control to # highest level of control, to allow "<" style comparisons. @@ -680,7 +680,7 @@ valuetypes: VelocityControl: PositionControl: - Controller.InputMode: + ODrive.Controller.InputMode: values: Inactive: Passthrough: @@ -692,7 +692,7 @@ valuetypes: Mirror: - Motor.MotorType: + ODrive.Motor.MotorType: values: HighCurrent: #LowCurrent: # not implemented diff --git a/tools/odrive/enums.py b/tools/odrive/enums.py index 466f11b6..93a5cfe2 100644 --- a/tools/odrive/enums.py +++ b/tools/odrive/enums.py @@ -4,7 +4,7 @@ # ODrive.Can.Protocol PROTOCOL_SIMPLE = 0 -# Axis.AxisState +# ODrive.Axis.AxisState AXIS_STATE_UNDEFINED = 0 AXIS_STATE_IDLE = 1 AXIS_STATE_STARTUP_SEQUENCE = 2 @@ -18,7 +18,7 @@ AXIS_STATE_LOCKIN_SPIN = 9 AXIS_STATE_ENCODER_DIR_FIND = 10 AXIS_STATE_HOMING = 11 -# Encoder.Mode +# ODrive.Encoder.Mode ENCODER_MODE_INCREMENTAL = 0 ENCODER_MODE_HALL = 1 ENCODER_MODE_SINCOS = 2 @@ -26,13 +26,13 @@ ENCODER_MODE_SPI_ABS_CUI = 256 ENCODER_MODE_SPI_ABS_AMS = 257 ENCODER_MODE_SPI_ABS_AEAT = 258 -# Controller.ControlMode +# ODrive.Controller.ControlMode CONTROL_MODE_VOLTAGE_CONTROL = 0 CONTROL_MODE_CURRENT_CONTROL = 1 CONTROL_MODE_VELOCITY_CONTROL = 2 CONTROL_MODE_POSITION_CONTROL = 3 -# Controller.InputMode +# ODrive.Controller.InputMode INPUT_MODE_INACTIVE = 0 INPUT_MODE_PASSTHROUGH = 1 INPUT_MODE_VEL_RAMP = 2 @@ -42,7 +42,7 @@ INPUT_MODE_TRAP_TRAJ = 5 INPUT_MODE_CURRENT_RAMP = 6 INPUT_MODE_MIRROR = 7 -# Motor.MotorType +# ODrive.Motor.MotorType MOTOR_TYPE_HIGH_CURRENT = 0 MOTOR_TYPE_GIMBAL = 2 MOTOR_TYPE_ACIM = 3 @@ -51,7 +51,7 @@ MOTOR_TYPE_ACIM = 3 CAN_ERROR_NONE = 0x00000000 CAN_ERROR_DUPLICATE_CAN_IDS = 0x00000001 -# Axis.Error +# ODrive.Axis.Error AXIS_ERROR_NONE = 0x00000000 AXIS_ERROR_INVALID_STATE = 0x00000001 AXIS_ERROR_DC_BUS_UNDER_VOLTAGE = 0x00000002 @@ -70,13 +70,13 @@ AXIS_ERROR_MAX_ENDSTOP_PRESSED = 0x00002000 AXIS_ERROR_ESTOP_REQUESTED = 0x00004000 AXIS_ERROR_HOMING_WITHOUT_ENDSTOP = 0x00020000 -# Axis.LockinState +# ODrive.Axis.LockinState LOCKIN_STATE_INACTIVE = 0 LOCKIN_STATE_RAMP = 1 LOCKIN_STATE_ACCELERATE = 2 LOCKIN_STATE_CONST_VEL = 3 -# Motor.Error +# ODrive.Motor.Error MOTOR_ERROR_NONE = 0x00000000 MOTOR_ERROR_PHASE_RESISTANCE_OUT_OF_RANGE = 0x00000001 MOTOR_ERROR_PHASE_INDUCTANCE_OUT_OF_RANGE = 0x00000002 @@ -95,13 +95,13 @@ MOTOR_ERROR_BRAKE_DUTY_CYCLE_NAN = 0x00002000 MOTOR_ERROR_DC_BUS_OVER_REGEN_CURRENT = 0x00004000 MOTOR_ERROR_DC_BUS_OVER_CURRENT = 0x00008000 -# Motor.ArmedState +# ODrive.Motor.ArmedState ARMED_STATE_DISARMED = 0 ARMED_STATE_WAITING_FOR_TIMINGS = 1 ARMED_STATE_WAITING_FOR_UPDATE = 2 ARMED_STATE_ARMED = 3 -# Motor.GateDriver.DrvFault +# ODrive.Motor.GateDriver.DrvFault DRV_FAULT_NO_FAULT = 0x00000000 DRV_FAULT_FET_LOW_C_OVERCURRENT = 0x00000001 DRV_FAULT_FET_HIGH_C_OVERCURRENT = 0x00000002 @@ -115,7 +115,7 @@ DRV_FAULT_P_VDD_UNDERVOLTAGE = 0x00000100 DRV_FAULT_G_VDD_UNDERVOLTAGE = 0x00000200 DRV_FAULT_G_VDD_OVERVOLTAGE = 0x00000400 -# Controller.Error +# ODrive.Controller.Error CONTROLLER_ERROR_NONE = 0x00000000 CONTROLLER_ERROR_OVERSPEED = 0x00000001 CONTROLLER_ERROR_INVALID_INPUT_MODE = 0x00000002 @@ -124,7 +124,7 @@ CONTROLLER_ERROR_INVALID_MIRROR_AXIS = 0x00000008 CONTROLLER_ERROR_INVALID_LOAD_ENCODER = 0x00000010 CONTROLLER_ERROR_INVALID_ESTIMATE = 0x00000020 -# Encoder.Error +# ODrive.Encoder.Error ENCODER_ERROR_NONE = 0x00000000 ENCODER_ERROR_UNSTABLE_GAIN = 0x00000001 ENCODER_ERROR_CPR_POLEPAIRS_MISMATCH = 0x00000002 @@ -136,6 +136,6 @@ ENCODER_ERROR_ABS_SPI_TIMEOUT = 0x00000040 ENCODER_ERROR_ABS_SPI_COM_FAIL = 0x00000080 ENCODER_ERROR_ABS_SPI_NOT_READY = 0x00000100 -# SensorlessEstimator.Error +# ODrive.SensorlessEstimator.Error SENSORLESS_ESTIMATOR_ERROR_NONE = 0x00000000 SENSORLESS_ESTIMATOR_ERROR_UNSTABLE_GAIN = 0x00000001 From 592b5f48c775c2ba47a1a27170a1fe3e2c325682 Mon Sep 17 00:00:00 2001 From: Samuel Sadok Date: Wed, 3 Jun 2020 17:53:23 +0200 Subject: [PATCH 18/28] fix compilation improve Python3 install check, remove python string interpolation which is not supported by older python versions remove string interpolation --- Firmware/Tupfile.lua | 11 ++++++++--- Firmware/interface_generator.py | 8 ++++---- docs/developer-guide.md | 9 ++++++--- 3 files changed, 18 insertions(+), 10 deletions(-) diff --git a/Firmware/Tupfile.lua b/Firmware/Tupfile.lua index 8f707d7e..147cbb30 100644 --- a/Firmware/Tupfile.lua +++ b/Firmware/Tupfile.lua @@ -7,9 +7,9 @@ tup.include('build.lua') -- On some systems this may return a python2 command if Python3 is not installed. function find_python3() success, python_version = run_now("python3 --version") - if success then return "python3" end + if success and string.match(python_version, "Python 3") then return "python3" end success, python_version = run_now("python --version") - if success then return "python" end + if success and string.match(python_version, "Python 3") then return "python" end error("Python 3 not found.") end @@ -20,7 +20,12 @@ tup.frule{inputs={'fibre/cpp/interfaces_template.j2'}, command=python_command..' tup.frule{inputs={'fibre/cpp/function_stubs_template.j2'}, command=python_command..' interface_generator.py --definitions odrive-interface.yaml --template %f --output %o', outputs='autogen/function_stubs.hpp'} tup.frule{inputs={'fibre/cpp/endpoints_template.j2'}, command=python_command..' interface_generator.py --definitions odrive-interface.yaml --generate-endpoints ODrive --template %f --output %o', outputs='autogen/endpoints.hpp'} tup.frule{inputs={'fibre/cpp/type_info_template.j2'}, command=python_command..' interface_generator.py --definitions odrive-interface.yaml --template %f --output %o', outputs='autogen/type_info.hpp'} -tup.frule{command=python_command..' interface_generator.py --definitions odrive-interface.yaml --template enums_template.j2 --output ../tools/odrive/enums.py'} + +-- Note: we currently check this file into source control for two reasons: +-- - Don't require tup to run in order to use odrivetool from the repo +-- - On Windows, tup is unhappy with writing outside of the tup directory +-- TODO: use CI to verify that on PRs the enums.py file is consistent with the YAML. +--tup.frule{command=python_command..' interface_generator.py --definitions odrive-interface.yaml --template enums_template.j2 --output ../tools/odrive/enums.py'} tup.frule{ command=python_command..' ../tools/odrive/version.py --output %o', diff --git a/Firmware/interface_generator.py b/Firmware/interface_generator.py index 14184f1f..4dc89906 100644 --- a/Firmware/interface_generator.py +++ b/Firmware/interface_generator.py @@ -365,7 +365,7 @@ def resolve_interface(scope, name, typeargs): elif probe_name in generics: return generics[probe_name](typeargs) - raise Exception(f'could not resolve type {name} in {join_name(*scope)}. Known interfaces are: {list(interfaces.keys())}. Known value types are: {list(value_types.keys())}') + raise Exception('could not resolve type {} in {}. Known interfaces are: {}. Known value types are: {}'.format(name, join_name(*scope), list(interfaces.keys()), list(value_types.keys()))) def resolve_valuetype(scope, name): """ @@ -381,7 +381,7 @@ def resolve_valuetype(scope, name): if probe_name in value_types: return value_types[probe_name] - raise Exception(f'could not resolve type {name} in {join_name(*scope)}. Known value types are: {list(value_types.keys())}') + raise Exception('could not resolve type {} in {}. Known value types are: {}'.format(name, join_name(*scope), list(value_types.keys()))) def map_to_fibre01_type(t): @@ -538,7 +538,7 @@ if args.verbose: clashing_names = list(set(value_types.keys()).intersection(set(interfaces.keys()))) if len(clashing_names): - print(f"**Error**: Found both an interface and a value type with the name {clashing_names[0]}. This is not allowed, interfaces and value types (such as enums) share the same namespace.", file=sys.stderr) + print("**Error**: Found both an interface and a value type with the name {}. This is not allowed, interfaces and value types (such as enums) share the same namespace.".format(clashing_names[0]), file=sys.stderr) sys.exit(1) # Resolve all types into references @@ -626,7 +626,7 @@ def tokenize(text, interface, interface_transform, value_type_transform, attribu if not attr is None: return attribute_transform(token, attr) - print(f'Warning: cannot resolve "{token}" in ' + interface['fullname']) + print('Warning: cannot resolve "{}" in {}'.format(token, interface['fullname'])) return "`" + token + "`" return re.sub(r'`([A-Za-z\._]+)`', token_transform, text) diff --git a/docs/developer-guide.md b/docs/developer-guide.md index 868c233d..93d306bb 100644 --- a/docs/developer-guide.md +++ b/docs/developer-guide.md @@ -35,7 +35,7 @@ The recommended tools for ODrive development are: * **ARM GNU Compiler**: For cross-compiling code * **ARM GDB**: For debugging the code and stepping through on the device * **OpenOCD**: For flashing the ODrive with the STLink/v2 programmer - * **Python**: For running the Python tools (`odrivetool`). Also required for compiling firmware. + * **Python 3**, along with the packages `PyYAML`, `Jinja2` and `jsonschema`: For running the Python tools (`odrivetool`). Also required for compiling firmware. See below for specific installation instructions for your OS. @@ -57,6 +57,7 @@ sudo apt-get update sudo apt-get install gcc-arm-embedded sudo apt-get install openocd sudo add-apt-repository ppa:jonathonf/tup && sudo apt-get update && sudo apt-get install tup +sudo apt-get install python3 python3-yaml python3-jinja2 python3-jsonschema ``` #### Linux (Ubuntu >= 20.04) @@ -64,6 +65,7 @@ sudo add-apt-repository ppa:jonathonf/tup && sudo apt-get update && sudo apt-get sudo apt install gcc-arm-embedded sudo apt install openocd sudo apt install tup +sudo apt install python3 python3-yaml python3-jinja2 python3-jsonschema ``` #### Arch Linux @@ -71,6 +73,7 @@ sudo apt install tup sudo pacman -S arm-none-eabi-gcc arm-none-eabi-binutils sudo pacman -S arm-none-eabi-gdb sudo pacman -S tup +sudo pacman -S python python-yaml python-jinja python-jsonschema ``` * [OpenOCD AUR package](https://aur.archlinux.org/packages/openocd/) @@ -80,6 +83,7 @@ First install [Homebrew](https://brew.sh/). Then you can run these commands in T brew install armmbed/formulae/arm-none-eabi-gcc brew cask install osxfuse && brew install tup brew install openocd +pip install PyYAML Jinja2 jsonschema ``` #### Windows @@ -93,6 +97,7 @@ Some instructions in this document may assume that you're using a bash command p * [Tup](http://gittup.org/tup/index.html) * [GNU MCU Eclipse's Windows Build Tools](https://github.com/gnu-mcu-eclipse/windows-build-tools/releases) * [Python 3](https://www.python.org/downloads/) + * Install Python packages: `pip install PyYAML Jinja2 jsonschema` * [OpenOCD](https://github.com/xpack-dev-tools/openocd-xpack/releases/). * [ST-Link/V2 Drivers](http://www.st.com/web/en/catalog/tools/FM147/SC1887/PF260219) @@ -127,8 +132,6 @@ You can also modify the compile-time defaults for all `.config` parameters. You 2. Connect the ODrive via USB and power it up. 3. Flash the firmware using [odrivetool dfu](odrivetool#device-firmware-update). -If you get `/bin/sh: 1: python: not found` while running `make`, change the tup file command to use `python3` - ### Flashing using an STLink/v2 programmer * Connect `GND`, `SWD`, and `SWC` on connector J2 to the programmer. Note: Always plug in `GND` first! From 0541cb2518dcf7f3fe1e9ef3bd46df69b36700df Mon Sep 17 00:00:00 2001 From: Samuel Sadok Date: Wed, 3 Jun 2020 17:55:54 +0200 Subject: [PATCH 19/28] amend changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 86db8832..d480c4aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,7 @@ Please add a note of your changes below this heading if you make a Pull Request. * Using an STM32F405 .svd file allows CortexDebug to view registers during debugging * Added scripts for building via docker. * Added ability to change uart baudrate via fibre +* Introduced `odrive-interface.yaml` as a root source for the ODrive's API. `odrivetool` connects much faster as a side effect. ### Changed * Changed ratiometric `motor.config.current_lim_tolerance` to absolute `motor.config.current_lim_margin` From b529bddd947d7b25699a846dc4cfc37027af7235 Mon Sep 17 00:00:00 2001 From: Samuel Sadok Date: Wed, 3 Jun 2020 18:19:04 +0200 Subject: [PATCH 20/28] add note to enums.py --- {Firmware => tools}/enums_template.j2 | 0 tools/odrive/enums.py | 2 ++ 2 files changed, 2 insertions(+) rename {Firmware => tools}/enums_template.j2 (100%) diff --git a/Firmware/enums_template.j2 b/tools/enums_template.j2 similarity index 100% rename from Firmware/enums_template.j2 rename to tools/enums_template.j2 diff --git a/tools/odrive/enums.py b/tools/odrive/enums.py index 93a5cfe2..36040da9 100644 --- a/tools/odrive/enums.py +++ b/tools/odrive/enums.py @@ -1,5 +1,7 @@ # TODO: This file is dangerous because the enums could potentially change between API versions. Should transmit as part of the JSON. +# To regenerate this file, nagivate to the top level of the ODrive repository and run: +# python Firmware/interface_generator.py --definitions Firmware/odrive-interface.yaml --template tools/enums_template.j2 --output tools/odrive/enums.py # ODrive.Can.Protocol PROTOCOL_SIMPLE = 0 From 3aeb790e15780f2b9084e0ca25bdfe8d23531c88 Mon Sep 17 00:00:00 2001 From: Samuel Sadok Date: Wed, 3 Jun 2020 18:19:19 +0200 Subject: [PATCH 21/28] attempt to fix travis build --- .travis.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 6efeeeb9..2e86de28 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,7 +12,11 @@ sudo: false addons: apt: packages: - libc6-i386 + - libc6-i386 + - python3 + - python3-yaml + - python3-jinja2 + - python3-jsonschema cache: directories: From 23a993ac42f96392f7ceb47411fb20381f80c7d7 Mon Sep 17 00:00:00 2001 From: Samuel Sadok Date: Wed, 3 Jun 2020 18:24:54 +0200 Subject: [PATCH 22/28] switch to jsonschema draft 4 --- Firmware/interface_generator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Firmware/interface_generator.py b/Firmware/interface_generator.py index 4dc89906..2ad0b4a3 100644 --- a/Firmware/interface_generator.py +++ b/Firmware/interface_generator.py @@ -9,7 +9,7 @@ import argparse import sys # This schema describes what we expect interface definition files to look like -validator = jsonschema.Draft7Validator(yaml.safe_load(""" +validator = jsonschema.Draft4Validator(yaml.safe_load(""" definitions: interface: type: object From c53ec7e9a56786c8cb4d41d4dc1275d4f4e1e4e5 Mon Sep 17 00:00:00 2001 From: Samuel Sadok Date: Thu, 4 Jun 2020 10:12:09 +0200 Subject: [PATCH 23/28] resolve compile issues --- .../fibre/cpp/include/fibre/cpp_utils.hpp | 65 ------------------- Firmware/fibre/cpp/include/fibre/protocol.hpp | 3 +- 2 files changed, 2 insertions(+), 66 deletions(-) diff --git a/Firmware/fibre/cpp/include/fibre/cpp_utils.hpp b/Firmware/fibre/cpp/include/fibre/cpp_utils.hpp index af28a07f..6a81858d 100644 --- a/Firmware/fibre/cpp/include/fibre/cpp_utils.hpp +++ b/Firmware/fibre/cpp/include/fibre/cpp_utils.hpp @@ -882,71 +882,6 @@ TRet* dynamic_get(size_t i, const std::tuple& t) { return dynamic_get_impl, TRet, Ts...>::get(i, t); } -/* Hex to numbers ------------------------------------------------------------*/ - -template -constexpr size_t hex_digits() { - return (std::numeric_limits::digits + 3) / 4; -} - -/* @brief Converts a hexadecimal digit to a uint8_t. -* @param output If not null, the digit's value is stored in this output -* Returns true if the char is a valid hex digit, false otherwise -*/ -static bool hex_digit_to_byte(char ch, uint8_t* output) { - uint8_t nil_output = 0; - if (!output) - output = &nil_output; - if (ch >= '0' && ch <= '9') - return (*output) = ch - '0', true; - if (ch >= 'a' && ch <= 'f') - return (*output) = ch - 'a' + 10, true; - if (ch >= 'A' && ch <= 'F') - return (*output) = ch - 'A' + 10, true; - return false; -} - -/* @brief Converts a hex string to an integer -* @param output If not null, the result is stored in this output -* Returns true if the string represents a valid hex value, false otherwise. -*/ -template -bool hex_string_to_int(const char * str, size_t length, TInt* output) { - constexpr size_t N_DIGITS = hex_digits(); - TInt result = 0; - if (length > N_DIGITS) - length = N_DIGITS; - for (size_t i = 0; i < length && str[i]; i++) { - uint8_t digit = 0; - if (!hex_digit_to_byte(str[i], &digit)) - return false; - result <<= 4; - result += digit; - } - if (output) - *output = result; - return true; -} - -template -bool hex_string_to_int(const char * str, TInt* output) { - return hex_string_to_int(str, hex_digits(), output); -} - -template -bool hex_string_to_int_arr(const char * str, size_t length, TInt (&output)[ICount]) { - for (size_t i = 0; i < ICount; i++) { - if (!hex_string_to_int(&str[i * hex_digits()], &output[i])) - return false; - } - return true; -} - -template -bool hex_string_to_int_arr(const char * str, TInt (&output)[ICount]) { - return hex_string_to_int_arr(str, hex_digits() * ICount, output); -} - template class simple_iterator : std::iterator { diff --git a/Firmware/fibre/cpp/include/fibre/protocol.hpp b/Firmware/fibre/cpp/include/fibre/protocol.hpp index a03a596e..09bac8c4 100644 --- a/Firmware/fibre/cpp/include/fibre/protocol.hpp +++ b/Firmware/fibre/cpp/include/fibre/protocol.hpp @@ -386,7 +386,8 @@ template<> struct Codec { return int_val.has_value() ? std::optional(*reinterpret_cast(&int_val.value())) : std::nullopt; } static bool encode(float value, bufptr_t* buffer) { - return Codec::encode(*reinterpret_cast(&value), buffer); + void* ptr = &value; + return Codec::encode(*reinterpret_cast(ptr), buffer); } }; template From bcaa5cca9fa0b4a40edb7173f8fdaf836d454ce2 Mon Sep 17 00:00:00 2001 From: Samuel Sadok Date: Thu, 4 Jun 2020 13:02:39 +0200 Subject: [PATCH 24/28] make interface generator python 3.5 compatible. Python 3.5 is the default version of Ubuntu 16.04 which is what our Travis CI runs on. - Python <3.6 does not keep the order of normal dicts. - Python 3.5 does not use UTF-8 encoding for files by default. --- Firmware/interface_generator.py | 74 ++++++++++++++++++--------------- 1 file changed, 41 insertions(+), 33 deletions(-) diff --git a/Firmware/interface_generator.py b/Firmware/interface_generator.py index 2ad0b4a3..cb8c6fa5 100644 --- a/Firmware/interface_generator.py +++ b/Firmware/interface_generator.py @@ -7,6 +7,7 @@ import jsonschema import re import argparse import sys +from collections import OrderedDict # This schema describes what we expect interface definition files to look like validator = jsonschema.Draft4Validator(yaml.safe_load(""" @@ -106,6 +107,13 @@ class SafeLineLoader(yaml.SafeLoader): # #mapping['__column__'] = node.start_mark.column + 1 # return mapping +# Ensure that dicts remain ordered, even in Python <3.6 +# source: https://stackoverflow.com/a/21912744/3621512 +def construct_mapping(loader, node): + loader.flatten_mapping(node) + return OrderedDict(loader.construct_pairs(node)) +SafeLineLoader.add_constructor(yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG, construct_mapping) + dictionary = [] def get_words(string): @@ -136,7 +144,7 @@ def to_macro_case(s): return '_'.join(get_words(s)).upper() def to_snake_case(s): return '_'.join(get_words(s)).lower() def to_kebab_case(s): return '-'.join(get_words(s)).lower() -value_types = { +value_types = OrderedDict({ 'bool': {'builtin': True, 'fullname': 'bool', 'name': 'bool', 'c_name': 'bool', 'py_type': 'bool'}, 'float32': {'builtin': True, 'fullname': 'float32', 'name': 'float32', 'c_name': 'float', 'py_type': 'float'}, 'uint8': {'builtin': True, 'fullname': 'uint8', 'name': 'uint8', 'c_name': 'uint8_t', 'py_type': 'int'}, @@ -148,11 +156,11 @@ value_types = { 'int32': {'builtin': True, 'fullname': 'int32', 'name': 'int32', 'c_name': 'int32_t', 'py_type': 'int'}, 'int64': {'builtin': True, 'fullname': 'int64', 'name': 'int64', 'c_name': 'int64_t', 'py_type': 'int'}, 'endpoint_ref': {'builtin': True, 'fullname': 'endpoint_ref', 'name': 'endpoint_ref', 'c_name': 'endpoint_ref_t', 'py_type': '[not implemented]'}, -} +}) -enums = {} +enums = OrderedDict() -interfaces = {} +interfaces = OrderedDict() def make_property_type(typeargs): value_type = resolve_valuetype('', typeargs['fibre.Property.type']) @@ -171,23 +179,23 @@ def make_property_type(typeargs): 'value_type': value_type, # TODO: should be a metaarg 'mode': mode, # TODO: should be a metaarg 'builtin': True, - 'attributes': {}, - 'functions': {} + 'attributes': OrderedDict(), + 'functions': OrderedDict() } if mode != 'readonly': prop_type['functions']['exchange'] = { 'name': 'exchange', 'fullname': join_name(fullname, 'exchange'), - 'in': {'obj': {'name': 'obj', 'type': {'c_name': c_name}}, 'value': {'name': 'value', 'type': value_type, 'optional': True}}, - 'out': {'value': {'name': 'value', 'type': value_type}}, + 'in': OrderedDict([('obj', {'name': 'obj', 'type': {'c_name': c_name}}), ('value', {'name': 'value', 'type': value_type, 'optional': True})]), + 'out': OrderedDict([('value', {'name': 'value', 'type': value_type})]), #'implementation': 'fibre_property_exchange<' + value_type['c_name'] + '>' } else: prop_type['functions']['read'] = { 'name': 'read', 'fullname': join_name(fullname, 'read'), - 'in': {'obj': {'name': 'obj', 'type': {'c_name': c_name}}}, - 'out': {'value': {'name': 'value', 'type': value_type}}, + 'in': OrderedDict([('obj', {'name': 'obj', 'type': {'c_name': c_name}})]), + 'out': OrderedDict([('value', {'name': 'value', 'type': value_type})]), #'implementation': 'fibre_property_read<' + value_type['c_name'] + '>' } @@ -216,7 +224,7 @@ def make_ref_type(interface): return ref_type def get_dict(elem, key): - return elem.get(key, None) or {} + return elem.get(key, None) or OrderedDict() def regularize_arg(path, name, elem): if elem is None: @@ -233,10 +241,10 @@ def regularize_func(path, name, elem, prepend_args): elem = {} elem['name'] = name elem['fullname'] = path = join_name(path, name) - elem['in'] = {n: regularize_arg(path, n, arg) - for n, arg in {**prepend_args, **get_dict(elem, 'in')}.items()} - elem['out'] = {n: regularize_arg(path, n, arg) - for n, arg in get_dict(elem, 'out').items()} + elem['in'] = OrderedDict((n, regularize_arg(path, n, arg)) + for n, arg in (*prepend_args.items(), *get_dict(elem, 'in').items())) + elem['out'] = OrderedDict((n, regularize_arg(path, n, arg)) + for n, arg in get_dict(elem, 'out').items()) return elem def regularize_attribute(parent, name, elem, c_is_class): @@ -289,13 +297,13 @@ def regularize_interface(path, name, elem): elem['fullname'] = path = join_name(path, name) elem['c_name'] = elem.get('c_name', elem['fullname'].replace('.', 'Intf::')) + 'Intf' interfaces[path] = elem - elem['functions'] = {name: regularize_func(path, name, func, {'obj': {'type': make_ref_type(elem)}}) - for name, func in get_dict(elem, 'functions').items()} + elem['functions'] = OrderedDict((name, regularize_func(path, name, func, {'obj': {'type': make_ref_type(elem)}})) + for name, func in get_dict(elem, 'functions').items()) if not 'c_is_class' in elem: raise Exception(elem) treat_as_class = elem['c_is_class'] # TODO: add command line arg to make this selectively optional - elem['attributes'] = {name: regularize_attribute(elem, name, prop, treat_as_class) - for name, prop in get_dict(elem, 'attributes').items()} + elem['attributes'] = OrderedDict((name, regularize_attribute(elem, name, prop, treat_as_class)) + for name, prop in get_dict(elem, 'attributes').items()) elem['interfaces'] = [] elem['enums'] = [] return elem @@ -313,14 +321,14 @@ def regularize_valuetype(path, name, elem): if 'flags' in elem: # treat as flags bit = 0 for k, v in elem['flags'].items(): - elem['flags'][k] = elem['flags'][k] or {} + elem['flags'][k] = elem['flags'][k] or OrderedDict() elem['flags'][k]['name'] = k current_bit = elem['flags'][k].get('bit', bit) elem['flags'][k]['bit'] = current_bit elem['flags'][k]['value'] = 0 if current_bit is None else (1 << current_bit) bit = bit if current_bit is None else current_bit + 1 if 'nullflag' in elem: - elem['flags'] = {elem['nullflag']: {'value': 0, 'bit': None}, **elem['flags']} + elem['flags'] = OrderedDict([(elem['nullflag'], {'value': 0, 'bit': None}), *elem['flags'].items()]) elem['values'] = elem['flags'] elem['is_flags'] = True elem['is_enum'] = True @@ -329,7 +337,7 @@ def regularize_valuetype(path, name, elem): elif 'values' in elem: # treat as enum val = 0 for k, v in elem['values'].items(): - elem['values'][k] = elem['values'][k] or {} + elem['values'][k] = elem['values'][k] or OrderedDict() elem['values'][k]['name'] = k val = elem['values'][k].get('value', val) elem['values'][k]['value'] = val @@ -397,8 +405,8 @@ def generate_endpoint_for_property(prop, attr_bindto, idx): endpoint = { 'id': idx, 'function': prop_intf['functions']['read' if prop['type']['mode'] == 'readonly' else 'exchange'], - 'in_bindings': {'obj': attr_bindto}, - 'out_bindings': [] + 'in_bindings': OrderedDict([('obj', attr_bindto)]), + 'out_bindings': OrderedDict() } endpoint_definition = { 'name': prop['name'], @@ -442,8 +450,8 @@ def generate_endpoint_table(intf, bindto, idx): endpoints.append({ 'id': idx + cnt, 'function': func, - 'in_bindings': {**{'obj': bindto}, **{k_arg: '(' + bindto + ')->' + func['name'] + '_in_' + k_arg + '_' for k_arg in list(func['in'].keys())[1:]}}, - 'out_bindings': {k_arg: '&(' + bindto + ')->' + func['name'] + '_out_' + k_arg + '_' for k_arg in func['out'].keys()}, + 'in_bindings': OrderedDict([('obj', bindto), *[(k_arg, '(' + bindto + ')->' + func['name'] + '_in_' + k_arg + '_') for k_arg in list(func['in'].keys())[1:]]]), + 'out_bindings': OrderedDict((k_arg, '&(' + bindto + ')->' + func['name'] + '_out_' + k_arg + '_') for k_arg in func['out'].keys()), }) in_def = [] out_def = [] @@ -481,12 +489,12 @@ parser.add_argument("--version", action="store_true", help="print version information") parser.add_argument("-v", "--verbose", action="store_true", help="print debug information (on stderr)") -parser.add_argument("-d", "--definitions", type=argparse.FileType('r'), nargs='+', +parser.add_argument("-d", "--definitions", type=argparse.FileType('r', encoding='utf-8'), nargs='+', help="the YAML interface definition file(s) used to generate the code") -parser.add_argument("-t", "--template", type=argparse.FileType('r'), +parser.add_argument("-t", "--template", type=argparse.FileType('r', encoding='utf-8'), help="the code template") group = parser.add_mutually_exclusive_group(required=True) -group.add_argument("-o", "--output", type=argparse.FileType('w'), +group.add_argument("-o", "--output", type=argparse.FileType('w', encoding='utf-8'), help="path of the generated output") group.add_argument("--outputs", type=str, help="path pattern for the generated outputs. One output is generated for each interface. Use # as placeholder for the interface name.") @@ -519,8 +527,8 @@ for definition_file in definition_files: #instance = err.instance.get(re.findall("([^']*)' (?:was|were) unexpected\)", err.message)[0], err.instance) # TODO: print line number raise Exception(err.message + '\nat ' + str(list(err.absolute_path))) - interfaces = {**interfaces, **get_dict(file_content, 'interfaces')} - value_types = {**value_types, **get_dict(file_content, 'valuetypes')} + interfaces.update(get_dict(file_content, 'interfaces')) + value_types.update(get_dict(file_content, 'valuetypes')) dictionary += file_content.get('dictionary', None) or [] @@ -661,12 +669,12 @@ else: if split_name(k)[0] == 'fibre': continue # TODO: remove special case output = template.render(interface = intf, **template_args) - with open(args.outputs.replace('#', k.lower()), 'w') as output_file: + with open(args.outputs.replace('#', k.lower()), 'w', encoding='utf-8') as output_file: output_file.write(output) for k, enum in value_types.items(): if enum.get('builtin', False) or not enum.get('is_enum', False): continue output = template.render(enum = enum, **template_args) - with open(args.outputs.replace('#', k.lower()), 'w') as output_file: + with open(args.outputs.replace('#', k.lower()), 'w', encoding='utf-8') as output_file: output_file.write(output) From 61bc1322d11e632a7bf4a099a4337cecdddd7696 Mon Sep 17 00:00:00 2001 From: Samuel Sadok Date: Wed, 10 Jun 2020 13:20:17 +0200 Subject: [PATCH 25/28] Add help text for missing dependencies --- Firmware/Tupfile.lua | 12 +++++++----- Firmware/{ => fibre/tools}/interface_generator.py | 0 Firmware/interface_generator_stub.py | 12 ++++++++++++ tools/enums_template.j2 | 2 ++ tools/odrive/enums.py | 2 +- 5 files changed, 22 insertions(+), 6 deletions(-) rename Firmware/{ => fibre/tools}/interface_generator.py (100%) create mode 100644 Firmware/interface_generator_stub.py diff --git a/Firmware/Tupfile.lua b/Firmware/Tupfile.lua index 51a1e5c7..35725d80 100644 --- a/Firmware/Tupfile.lua +++ b/Firmware/Tupfile.lua @@ -16,16 +16,18 @@ end python_command = find_python3() print('Using python command "'..python_command..'"') -tup.frule{inputs={'fibre/cpp/interfaces_template.j2'}, command=python_command..' interface_generator.py --definitions odrive-interface.yaml --template %f --output %o', outputs='autogen/interfaces.hpp'} -tup.frule{inputs={'fibre/cpp/function_stubs_template.j2'}, command=python_command..' interface_generator.py --definitions odrive-interface.yaml --template %f --output %o', outputs='autogen/function_stubs.hpp'} -tup.frule{inputs={'fibre/cpp/endpoints_template.j2'}, command=python_command..' interface_generator.py --definitions odrive-interface.yaml --generate-endpoints ODrive --template %f --output %o', outputs='autogen/endpoints.hpp'} -tup.frule{inputs={'fibre/cpp/type_info_template.j2'}, command=python_command..' interface_generator.py --definitions odrive-interface.yaml --template %f --output %o', outputs='autogen/type_info.hpp'} +run_now("") + +tup.frule{inputs={'fibre/cpp/interfaces_template.j2'}, command=python_command..' interface_generator_stub.py --definitions odrive-interface.yaml --template %f --output %o', outputs='autogen/interfaces.hpp'} +tup.frule{inputs={'fibre/cpp/function_stubs_template.j2'}, command=python_command..' interface_generator_stub.py --definitions odrive-interface.yaml --template %f --output %o', outputs='autogen/function_stubs.hpp'} +tup.frule{inputs={'fibre/cpp/endpoints_template.j2'}, command=python_command..' interface_generator_stub.py --definitions odrive-interface.yaml --generate-endpoints ODrive --template %f --output %o', outputs='autogen/endpoints.hpp'} +tup.frule{inputs={'fibre/cpp/type_info_template.j2'}, command=python_command..' interface_generator_stub.py --definitions odrive-interface.yaml --template %f --output %o', outputs='autogen/type_info.hpp'} -- Note: we currently check this file into source control for two reasons: -- - Don't require tup to run in order to use odrivetool from the repo -- - On Windows, tup is unhappy with writing outside of the tup directory -- TODO: use CI to verify that on PRs the enums.py file is consistent with the YAML. ---tup.frule{command=python_command..' interface_generator.py --definitions odrive-interface.yaml --template enums_template.j2 --output ../tools/odrive/enums.py'} +--tup.frule{command=python_command..' interface_generator_stub.py --definitions odrive-interface.yaml --template enums_template.j2 --output ../tools/odrive/enums.py'} tup.frule{ command=python_command..' ../tools/odrive/version.py --output %o', diff --git a/Firmware/interface_generator.py b/Firmware/fibre/tools/interface_generator.py similarity index 100% rename from Firmware/interface_generator.py rename to Firmware/fibre/tools/interface_generator.py diff --git a/Firmware/interface_generator_stub.py b/Firmware/interface_generator_stub.py new file mode 100644 index 00000000..cdbc91a8 --- /dev/null +++ b/Firmware/interface_generator_stub.py @@ -0,0 +1,12 @@ +#!/bin/python3 + +import sys +import os + +try: + exec(open(os.path.join(os.path.dirname(os.path.realpath(__file__)), 'fibre', 'tools', 'interface_generator.py')).read()) +except ModuleNotFoundError as ex: + print(str(ex), file=sys.stderr) + print("Note that there are new compile-time dependencies since around v0.5.1.", file=sys.stderr) + print("Check out https://github.com/madcowswe/ODrive/blob/devel/docs/developer-guide.md#prerequisites for details.", file=sys.stderr) + exit(1) diff --git a/tools/enums_template.j2 b/tools/enums_template.j2 index 28105c3c..bb20ca37 100644 --- a/tools/enums_template.j2 +++ b/tools/enums_template.j2 @@ -1,5 +1,7 @@ # TODO: This file is dangerous because the enums could potentially change between API versions. Should transmit as part of the JSON. +# To regenerate this file, nagivate to the top level of the ODrive repository and run: +# python Firmware/interface_generator_stub.py --definitions Firmware/odrive-interface.yaml --template tools/enums_template.j2 --output tools/odrive/enums.py [%- for _, enum in value_types.items() %] [%- if enum.is_enum %] diff --git a/tools/odrive/enums.py b/tools/odrive/enums.py index 36040da9..2fe32b24 100644 --- a/tools/odrive/enums.py +++ b/tools/odrive/enums.py @@ -1,7 +1,7 @@ # TODO: This file is dangerous because the enums could potentially change between API versions. Should transmit as part of the JSON. # To regenerate this file, nagivate to the top level of the ODrive repository and run: -# python Firmware/interface_generator.py --definitions Firmware/odrive-interface.yaml --template tools/enums_template.j2 --output tools/odrive/enums.py +# python Firmware/interface_generator_stub.py --definitions Firmware/odrive-interface.yaml --template tools/enums_template.j2 --output tools/odrive/enums.py # ODrive.Can.Protocol PROTOCOL_SIMPLE = 0 From 6380c2b2011d1464d85862536172548a733c76e3 Mon Sep 17 00:00:00 2001 From: Samuel Sadok Date: Wed, 10 Jun 2020 14:05:52 +0200 Subject: [PATCH 26/28] add fibre HWIL tests --- docs/testing.md | 1 + tools/odrive/tests/fibre_test.py | 56 +++++++++++++++++++++++++++ tools/odrive/tests/uart_ascii_test.py | 9 +++++ 3 files changed, 66 insertions(+) create mode 100644 tools/odrive/tests/fibre_test.py diff --git a/docs/testing.md b/docs/testing.md index 1c4e42c3..5ba1160b 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -17,6 +17,7 @@ The testing facility consists of the following components: - `can_test.py`: Partial coverage of the commands described in [CAN Protocol](can-protocol) - `closed_loop_test.py`: Velocity control, position control (TODO: sensorless control), brake regen current hard limit, current control with velocity limiting - `encoder_test.py`: Incremental encoder, hall effect encoder, sin/cos encoder, SPI encoders (AMS, CUI) + - `fibre_test.py`: General USB protocol tests - `nvm_test.py`: Configuration storage - `pwm_input_test.py`: PWM input - `step_dir_test.py`: Step/dir input diff --git a/tools/odrive/tests/fibre_test.py b/tools/odrive/tests/fibre_test.py new file mode 100644 index 00000000..75801a5b --- /dev/null +++ b/tools/odrive/tests/fibre_test.py @@ -0,0 +1,56 @@ + +import test_runner + +import time + +from fibre.utils import Logger +from odrive.enums import * +from test_runner import * + +class FibreFunctionalTest(): + """ + Tests basic protocol functionality. + """ + + def get_test_cases(self, testrig: TestRig): + return testrig.get_components(ODriveComponent) + + def run_test(self, odrive: ODriveComponent, logger: Logger): + # Test property read/write + odrive.handle.test_property = 42 + test_assert_eq(odrive.handle.test_property, 42) + odrive.handle.test_property = 0xffffffff + test_assert_eq(odrive.handle.test_property, 0xffffffff) + + # Test function call + val = odrive.handle.get_adc_voltage(0) + test_assert_within(val, 0.01, 3.29) + + # Test custom setter (aka property write hook) + odrive.handle.axis0.motor.config.phase_resistance = 1 + odrive.handle.axis0.motor.config.phase_inductance = 1 + odrive.handle.axis0.motor.config.current_control_bandwidth = 1000 + old_gain = odrive.handle.axis0.motor.current_control.p_gain + test_assert_eq(old_gain, 1000, accuracy=0.0001) # must be non-zero for subsequent check to work + odrive.handle.axis0.motor.config.current_control_bandwidth /= 2 + test_assert_eq(odrive.handle.axis0.motor.current_control.p_gain, old_gain / 2, accuracy=0.0001) + +class FibreBurnInTest(): + """ + Tests continuous usage of the protocol. + """ + + def get_test_cases(self, testrig: TestRig): + return testrig.get_components(ODriveComponent) + + def run_test(self, odrive: ODriveComponent, logger: Logger): + data = record_log(lambda: [odrive.handle.vbus_voltage], duration=10.0) + expected_data = np.mean(data[:,1]) * np.ones(data[:,1].size) + test_curve_fit(data, expected_data, max_mean_err = 0.1, inlier_range = 0.5, max_outliers = 0) + + +if __name__ == '__main__': + test_runner.run([ + FibreFunctionalTest(), + FibreBurnInTest(), + ]) diff --git a/tools/odrive/tests/uart_ascii_test.py b/tools/odrive/tests/uart_ascii_test.py index 17cf85f8..ee9a3fa4 100644 --- a/tools/odrive/tests/uart_ascii_test.py +++ b/tools/odrive/tests/uart_ascii_test.py @@ -89,6 +89,15 @@ class TestUartAscii(): response = int(ser.readline().strip()) test_assert_eq(response, 12345) + # Test custom setter (aka property write hook) + odrive.handle.axis0.motor.config.phase_resistance = 1 + odrive.handle.axis0.motor.config.phase_inductance = 1 + odrive.handle.axis0.motor.config.current_control_bandwidth = 1000 + old_gain = odrive.handle.axis0.motor.current_control.p_gain + test_assert_eq(old_gain, 1000, accuracy=0.0001) # must be non-zero for subsequent check to work + ser.write('w axis0.motor.config.current_control_bandwidth {}\n'.format(odrive.handle.axis0.motor.config.current_control_bandwidth / 2).encode('ascii')) + test_assert_eq(ser.readline(), b'') + test_assert_eq(odrive.handle.axis0.motor.current_control.p_gain, old_gain / 2, accuracy=0.0001) # Test 'c', 'v', 'p', 'q' and 'f' commands From 8c6ab4bd3084af8a77b49343939f11ebc5797071 Mon Sep 17 00:00:00 2001 From: Samuel Sadok Date: Wed, 10 Jun 2020 15:36:47 +0200 Subject: [PATCH 27/28] add compile prerequisites to CI --- .github/workflows/compile.yaml | 12 +++++++++--- Firmware/interface_generator_stub.py | 2 +- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/.github/workflows/compile.yaml b/.github/workflows/compile.yaml index 45847d91..eefd6d22 100644 --- a/.github/workflows/compile.yaml +++ b/.github/workflows/compile.yaml @@ -31,7 +31,7 @@ jobs: steps: - uses: actions/checkout@v2 - - name: Install ARM GCC and tup (Debian) + - name: Install prerequisites (Debian) if: startsWith(matrix.os, 'ubuntu-') run: | DEBIAN_VERSION="$(lsb_release --release --short)" @@ -52,11 +52,15 @@ jobs: sudo apt-get install tup - - name: Install ARM GCC and tup (macOS) + sudo apt install python3 python3-yaml python3-jinja2 python3-jsonschema + + - name: Install prerequisites (macOS) if: startsWith(matrix.os, 'macOS-') run: | brew install armmbed/formulae/arm-none-eabi-gcc brew cask install osxfuse && brew install tup + pip3 install PyYAML Jinja2 jsonschema + - name: Cache chocolatey uses: actions/cache@v2 @@ -67,7 +71,7 @@ jobs: restore-keys: | ${{ runner.os }}-gcc-arm-embedded - - name: Install ARM GCC and tup (Windows) + - name: Install prerequisites (Windows) if: startsWith(matrix.os, 'windows-') run: | Invoke-WebRequest -Uri "http://gittup.org/tup/win32/tup-latest.zip" -OutFile ".\tup-latest.zip" @@ -75,6 +79,8 @@ jobs: echo "::add-path::$(Resolve-Path .)\tup-latest" choco install gcc-arm-embedded # downloads https://developer.arm.com/-/media/Files/downloads/gnu-rm/9-2019q4/gcc-arm-none-eabi-9-2019-q4-major-win32.zip + + pip install PyYAML Jinja2 jsonschema - name: Prepare Compilation run: | diff --git a/Firmware/interface_generator_stub.py b/Firmware/interface_generator_stub.py index cdbc91a8..d5b22093 100644 --- a/Firmware/interface_generator_stub.py +++ b/Firmware/interface_generator_stub.py @@ -5,7 +5,7 @@ import os try: exec(open(os.path.join(os.path.dirname(os.path.realpath(__file__)), 'fibre', 'tools', 'interface_generator.py')).read()) -except ModuleNotFoundError as ex: +except ImportError as ex: print(str(ex), file=sys.stderr) print("Note that there are new compile-time dependencies since around v0.5.1.", file=sys.stderr) print("Check out https://github.com/madcowswe/ODrive/blob/devel/docs/developer-guide.md#prerequisites for details.", file=sys.stderr) From b88f66ab99e514b2bb7d70373da3d725c1b7115c Mon Sep 17 00:00:00 2001 From: Samuel Sadok Date: Wed, 10 Jun 2020 16:09:18 +0200 Subject: [PATCH 28/28] update macOS build instructions --- docs/developer-guide.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/developer-guide.md b/docs/developer-guide.md index 93d306bb..b169b1f7 100644 --- a/docs/developer-guide.md +++ b/docs/developer-guide.md @@ -83,7 +83,7 @@ First install [Homebrew](https://brew.sh/). Then you can run these commands in T brew install armmbed/formulae/arm-none-eabi-gcc brew cask install osxfuse && brew install tup brew install openocd -pip install PyYAML Jinja2 jsonschema +pip3 install PyYAML Jinja2 jsonschema ``` #### Windows