diff --git a/src/firmware/app.c b/src/firmware/app.c index f633063..16b3e0b 100644 --- a/src/firmware/app.c +++ b/src/firmware/app.c @@ -1,905 +1,954 @@ -/* - * bbs-fw - * - * Copyright (C) Daniel Nilsson, 2022. - * - * Released under the GPL License, Version 3 - */ - -#include "app.h" -#include "fwconfig.h" -#include "cfgstore.h" -#include "motor.h" -#include "sensors.h" -#include "throttle.h" -#include "lights.h" -#include "uart.h" -#include "eventlog.h" -#include "util.h" -#include "system.h" - -// Compile time options - -// Number of PAS sensor pulses to engage cruise mode, -// there are 24 pulses per revolution. -#define CRUISE_ENGAGE_PAS_PULSES 12 - -// Number of PAS sensor pulses to disengage curise mode -// by pedaling backwards. There are 24 pulses per revolution. -#define CRUISE_DISENGAGE_PAS_PULSES 4 - - -typedef struct -{ - assist_level_t level; - - // cached precomputed values - // --------------------------------- - - // speed - int32_t max_wheel_speed_rpm_x10; - int16_t speed_ramp_low_limit_rpm_x10; - int16_t speed_ramp_high_limit_rpm_x10; - - // pas - uint8_t keep_current_target_percent; - uint16_t keep_current_ramp_start_rpm_x10; - uint16_t keep_current_ramp_end_rpm_x10; - -} assist_level_data_t; - -static uint8_t assist_level; -static uint8_t operation_mode; -static uint16_t global_max_speed_rpm; - -static uint16_t lvc_voltage_x100; -static uint16_t lvc_ramp_down_start_voltage_x100; - -static assist_level_data_t assist_level_data; -static uint16_t speed_limit_ramp_interval_rpm_x10; - -static bool cruise_paused; -static int8_t temperature_contr_c; -static int8_t temperature_motor_c; - -static uint8_t ramp_up_target_current; -static uint32_t last_ramp_up_increment_ms; -static uint16_t ramp_up_current_interval_ms; - -static uint8_t ramp_down_target_current; -static uint32_t last_ramp_down_decrement_ms; - -static uint32_t power_blocked_until_ms; - -void apply_pas_cadence(uint8_t* target_current, uint8_t throttle_percent); -#if HAS_TORQUE_SENSOR -void apply_pas_torque(uint8_t* target_current); -#endif - -void apply_cruise(uint8_t* target_current, uint8_t throttle_percent); -bool apply_throttle(uint8_t* target_current, uint8_t throttle_percent); -void apply_current_ramp_up(uint8_t* target_current); -void apply_current_ramp_down(uint8_t* target_current); -void apply_speed_limit(uint8_t* target_current); -void apply_thermal_limit(uint8_t* target_current); -void apply_low_voltage_limit(uint8_t* target_current); -void apply_shift_sensor_interrupt(uint8_t* target_current); -void apply_brake(uint8_t* target_current); - -bool check_power_block(); -void block_power_for(uint16_t ms); - -void reload_assist_params(); - -uint16_t convert_wheel_speed_kph_to_rpm(uint8_t speed_kph); - -void app_init() -{ - motor_disable(); - lights_disable(); - lights_set(g_config.lights_always_on); - - lvc_voltage_x100 = g_config.low_cut_off_v * 100u; - - uint16_t voltage_range_x100 = - EXPAND_U16(g_config.max_battery_x100v_u16h, g_config.max_battery_x100v_u16l) - lvc_voltage_x100; - lvc_ramp_down_start_voltage_x100 = (uint16_t)(lvc_voltage_x100 + - ((voltage_range_x100 * LVC_RAMP_DOWN_OFFSET_PERCENT) / 100)); - - global_max_speed_rpm = 0; - temperature_contr_c = 0; - temperature_motor_c = 0; - ramp_up_target_current = 0; - last_ramp_up_increment_ms = 0; - ramp_up_current_interval_ms = (g_config.max_current_amps * 10u) / g_config.current_ramp_amps_s; - - power_blocked_until_ms = 0; - - speed_limit_ramp_interval_rpm_x10 = convert_wheel_speed_kph_to_rpm(SPEED_LIMIT_RAMP_DOWN_INTERVAL_KPH) * 10; - - cruise_paused = true; - operation_mode = OPERATION_MODE_DEFAULT; - - app_set_wheel_max_speed_rpm(convert_wheel_speed_kph_to_rpm(g_config.max_speed_kph)); - app_set_assist_level(g_config.assist_startup_level); - reload_assist_params(); - - if (g_config.assist_mode_select == ASSIST_MODE_SELECT_BRAKE_BOOT && brake_is_activated()) - { - app_set_operation_mode(OPERATION_MODE_SPORT); - } -} - -void app_process() -{ - uint8_t target_current = 0; - bool throttle_override = false; - - if (check_power_block()) - { - target_current = 0; - } - else if (assist_level == ASSIST_PUSH && g_config.use_push_walk) - { - target_current = 10; - } - else - { - uint8_t throttle_percent = throttle_read(); - - apply_pas_cadence(&target_current, throttle_percent); -#if HAS_TORQUE_SENSOR - apply_pas_torque(&target_current); -#endif // HAS_TORQUE_SENSOR - - apply_cruise(&target_current, throttle_percent); - - // order is important, ramp up shall not affect throttle - apply_current_ramp_up(&target_current); - - throttle_override = apply_throttle(&target_current, throttle_percent); - } - - apply_current_ramp_down(&target_current); - - // do not apply speed limit if throttle speed override active - if (!throttle_override || - !(assist_level_data.level.flags & ASSIST_FLAG_PAS) || - !(assist_level_data.level.flags & ASSIST_FLAG_OVERRIDE_SPEED)) - { - apply_speed_limit(&target_current); - } - - apply_thermal_limit(&target_current); - apply_low_voltage_limit(&target_current); -#if HAS_SHIFT_SENSOR_SUPPORT - apply_shift_sensor_interrupt(&target_current); -#endif - - apply_brake(&target_current); - - // override target cadence if configured in assist level - if (throttle_override && - (assist_level_data.level.flags & ASSIST_FLAG_PAS) && - (assist_level_data.level.flags & ASSIST_FLAG_OVERRIDE_CADENCE)) - { - motor_set_target_speed(THROTTLE_CADENCE_OVERRIDE_PERCENT); - } - else - { - motor_set_target_speed(assist_level_data.level.max_cadence_percent); - } - - motor_set_target_current(target_current); - - if (target_current > 0) - { - motor_enable(); - } - else - { - motor_disable(); - - // force reset current ramps - ramp_up_target_current = 0; - ramp_down_target_current = 0; - last_ramp_up_increment_ms = 0; - last_ramp_down_decrement_ms = 0; - } - - if (motor_status() & MOTOR_ERROR_LVC) - { - lights_disable(); - } - else - { - lights_enable(); - } -} - - -void app_set_assist_level(uint8_t level) -{ - if (assist_level != level) - { - if (assist_level == ASSIST_PUSH && g_config.use_push_walk) - { - // When releasig push walk mode pedals may have been rotating - // with the motor, block motor power for 2 seconds to prevent PAS - // sensor from incorrectly applying power if returning to a PAS level. - block_power_for(1000); - } - - assist_level = level; - eventlog_write_data(EVT_DATA_ASSIST_LEVEL, assist_level); - reload_assist_params(); - } -} - -void app_set_lights(bool on) -{ - static bool last_light_state = false; - - if ( // it's ok to write ugly code if you say it's ugly... - (g_config.assist_mode_select == ASSIST_MODE_SELECT_LIGHTS) || - (assist_level == ASSIST_0 && g_config.assist_mode_select == ASSIST_MODE_SELECT_PAS0_LIGHT) || - (assist_level == ASSIST_1 && g_config.assist_mode_select == ASSIST_MODE_SELECT_PAS1_LIGHT) || - (assist_level == ASSIST_2 && g_config.assist_mode_select == ASSIST_MODE_SELECT_PAS2_LIGHT) || - (assist_level == ASSIST_3 && g_config.assist_mode_select == ASSIST_MODE_SELECT_PAS3_LIGHT) || - (assist_level == ASSIST_4 && g_config.assist_mode_select == ASSIST_MODE_SELECT_PAS4_LIGHT) || - (assist_level == ASSIST_5 && g_config.assist_mode_select == ASSIST_MODE_SELECT_PAS5_LIGHT) || - (assist_level == ASSIST_6 && g_config.assist_mode_select == ASSIST_MODE_SELECT_PAS6_LIGHT) || - (assist_level == ASSIST_7 && g_config.assist_mode_select == ASSIST_MODE_SELECT_PAS7_LIGHT) || - (assist_level == ASSIST_8 && g_config.assist_mode_select == ASSIST_MODE_SELECT_PAS8_LIGHT) || - (assist_level == ASSIST_9 && g_config.assist_mode_select == ASSIST_MODE_SELECT_PAS9_LIGHT) - ) - { - if (on) - { - app_set_operation_mode(OPERATION_MODE_SPORT); - } - else - { - app_set_operation_mode(OPERATION_MODE_DEFAULT); - } - } - else - { - if (g_config.lights_always_on) - { - on = true; - } - - if (last_light_state != on) - { - last_light_state = on; - eventlog_write_data(EVT_DATA_LIGHTS, on); - lights_set(on); - } - } -} - -void app_set_operation_mode(uint8_t mode) -{ - if (operation_mode != mode) - { - operation_mode = mode; - eventlog_write_data(EVT_DATA_OPERATION_MODE, operation_mode); - reload_assist_params(); - } -} - -void app_set_wheel_max_speed_rpm(uint16_t value) -{ - if (global_max_speed_rpm != value) - { - global_max_speed_rpm = value; - eventlog_write_data(EVT_DATA_WHEEL_SPEED_PPM, value); - reload_assist_params(); - } -} - -uint8_t app_get_assist_level() -{ - return assist_level; -} - -uint8_t app_get_status_code() -{ - uint16_t motor = motor_status(); - - if (motor & MOTOR_ERROR_HALL_SENSOR) - { - return STATUS_ERROR_HALL_SENSOR; - } - - if (motor & MOTOR_ERROR_CURRENT_SENSE) - { - return STATUS_ERROR_CURRENT_SENSE; - } - - if (motor & MOTOR_ERROR_POWER_RESET) - { - // Phase line error code reused, cause and meaning - // of MOTOR_ERROR_POWER_RESET triggered on bbs02 is currently unknown - return STATUS_ERROR_PHASE_LINE; - } - - if (!throttle_ok()) - { - return STATUS_ERROR_THROTTLE; - } - - if (!torque_sensor_ok()) - { - return STATUS_ERROR_TORQUE_SENSOR; - } - - if (temperature_motor_c > MAX_TEMPERATURE) - { - return STATUS_ERROR_MOTOR_OVER_TEMP; - } - - if (temperature_contr_c > MAX_TEMPERATURE) - { - return STATUS_ERROR_CONTROLLER_OVER_TEMP; - } - - // Disable LVC error since it is not shown on display in original firmware - // Uncomment if you want to enable - // if (motor & MOTOR_ERROR_LVC) - // { - // return STATUS_ERROR_LVC; - // } - - if (brake_is_activated()) - { - return STATUS_BRAKING; - } - - return STATUS_NORMAL; -} - -uint8_t app_get_temperature() -{ - int8_t temp_max = MAX(temperature_contr_c, temperature_motor_c); - - if (temp_max < 0) - { - return 0; - } - - return (uint8_t)temp_max; -} - - -void apply_pas_cadence(uint8_t* target_current, uint8_t throttle_percent) -{ - if ((assist_level_data.level.flags & ASSIST_FLAG_PAS) && !(assist_level_data.level.flags & ASSIST_FLAG_PAS_TORQUE)) - { - if (pas_is_pedaling_forwards() && pas_get_pulse_counter() > g_config.pas_start_delay_pulses) - { - if (assist_level_data.level.flags & ASSIST_FLAG_PAS_VARIABLE) - { - uint8_t current = (uint8_t)MAP16(throttle_percent, 0, 100, 0, assist_level_data.level.target_current_percent); - if (current > *target_current) - { - *target_current = current; - } - } - else - { - if (assist_level_data.level.target_current_percent > *target_current) - { - *target_current = assist_level_data.level.target_current_percent; - } - - // apply "keep current" ramp - if (g_config.pas_keep_current_percent < 100) - { - if (*target_current > assist_level_data.keep_current_target_percent && - pas_get_cadence_rpm_x10() > assist_level_data.keep_current_ramp_start_rpm_x10) - { - uint32_t cadence = MIN(pas_get_cadence_rpm_x10(), assist_level_data.keep_current_ramp_end_rpm_x10); - - // ramp down current towards keep_current_target_percent with rpm above keep_current_ramp_start_rpm_x10 - *target_current = MAP32( - cadence, // in - assist_level_data.keep_current_ramp_start_rpm_x10, // in_min - assist_level_data.keep_current_ramp_end_rpm_x10, // in_max - *target_current, // out_min - assist_level_data.keep_current_target_percent); // out_max - } - } - } - } - } -} - -#if HAS_TORQUE_SENSOR -void apply_pas_torque(uint8_t* target_current) -{ - if ((assist_level_data.level.flags & ASSIST_FLAG_PAS) && (assist_level_data.level.flags & ASSIST_FLAG_PAS_TORQUE)) - { - if (pas_is_pedaling_forwards() && (pas_get_pulse_counter() > g_config.pas_start_delay_pulses || speed_sensor_is_moving())) - { - uint16_t torque_nm_x100 = torque_sensor_get_nm_x100(); - uint16_t cadence_rpm_x10 = pas_get_cadence_rpm_x10(); - if (cadence_rpm_x10 < TORQUE_POWER_LOWER_RPM_X10) - { - cadence_rpm_x10 = TORQUE_POWER_LOWER_RPM_X10; - } - - uint16_t pedal_power_w_x10 = (uint16_t)(((uint32_t)torque_nm_x100 * cadence_rpm_x10) / 955); - uint16_t target_current_amp_x100 = (uint16_t)(((uint32_t)10 * pedal_power_w_x10 * - assist_level_data.level.torque_amplification_factor_x10) / motor_get_battery_voltage_x10()); - uint16_t max_current_amp_x100 = g_config.max_current_amps * 100; - - // limit target to ensure no overflow in map result - if (target_current_amp_x100 > max_current_amp_x100) - { - target_current_amp_x100 = max_current_amp_x100; - } - uint8_t tmp_percent = (uint8_t)MAP32(target_current_amp_x100, 0, max_current_amp_x100, 0, 100); - - // minimum 1 percent current if pedaling - if (tmp_percent < 1) - { - tmp_percent = 1; - } - // limit to maximum assist current for set level - else if (tmp_percent > assist_level_data.level.target_current_percent) - { - tmp_percent = assist_level_data.level.target_current_percent; - } - - if (tmp_percent > *target_current) - { - *target_current = tmp_percent; - } - } - } -} -#endif - -void apply_cruise(uint8_t* target_current, uint8_t throttle_percent) -{ - static bool cruise_block_throttle_return = false; - - if ((assist_level_data.level.flags & ASSIST_FLAG_CRUISE) && throttle_ok()) - { - // pause cruise if brake activated - if (brake_is_activated()) - { - cruise_paused = true; - cruise_block_throttle_return = true; - } - - // pause cruise if started pedaling backwards - else if (pas_is_pedaling_backwards() && pas_get_pulse_counter() > CRUISE_DISENGAGE_PAS_PULSES) - { - cruise_paused = true; - cruise_block_throttle_return = true; - } - - // pause cruise if throttle touched while cruise active - else if (!cruise_paused && !cruise_block_throttle_return && throttle_percent > 0) - { - cruise_paused = true; - cruise_block_throttle_return = true; - } - - // unpause cruise if pedaling forward while engaging throttle > 50% - else if (cruise_paused && !cruise_block_throttle_return && throttle_percent > 50 && pas_is_pedaling_forwards() && pas_get_pulse_counter() > CRUISE_ENGAGE_PAS_PULSES) - { - cruise_paused = false; - cruise_block_throttle_return = true; - } - - // reset flag tracking throttle to make sure throttle returns to idle position before engage/disenage cruise with throttle touch - else if (cruise_block_throttle_return && throttle_percent == 0) - { - cruise_block_throttle_return = false; - } - - if (cruise_paused) - { - *target_current = 0; - } - else - { - if (assist_level_data.level.target_current_percent > *target_current) - { - *target_current = assist_level_data.level.target_current_percent; - } - } - } -} - -bool apply_throttle(uint8_t* target_current, uint8_t throttle_percent) -{ - if ((assist_level_data.level.flags & ASSIST_FLAG_THROTTLE) && throttle_percent > 0 && throttle_ok()) - { - uint8_t current = (uint8_t)MAP16(throttle_percent, 0, 100, g_config.throttle_start_percent, assist_level_data.level.max_throttle_current_percent); - if (current >= *target_current) - { - *target_current = current; - - return true; // return true if overrides previous set target current - } - } - - return false; -} - -void apply_current_ramp_up(uint8_t* target_current) -{ - if (*target_current > ramp_up_target_current) - { - uint32_t now = system_ms(); - uint16_t time_diff = now - last_ramp_up_increment_ms; - - if (time_diff >= ramp_up_current_interval_ms) - { - ++ramp_up_target_current; - - if (last_ramp_up_increment_ms == 0) - { - last_ramp_up_increment_ms = now; - } - else - { - // offset for time overshoot to not accumulate large ramp error - last_ramp_up_increment_ms = now - (uint8_t)(time_diff - ramp_up_current_interval_ms); - } - } - - *target_current = ramp_up_target_current; - } - else - { - ramp_up_target_current = *target_current; - } -} - -void apply_current_ramp_down(uint8_t* target_current) -{ - // apply fast ramp down if coming from high target current (> 50%) - if (*target_current < ramp_down_target_current) - { - uint32_t now = system_ms(); - uint16_t time_diff = now - last_ramp_down_decrement_ms; - - if (time_diff >= 10) - { - uint8_t diff = ramp_down_target_current - *target_current; - - if (diff >= CURRENT_RAMP_DOWN_PERCENT_10MS) - { - ramp_down_target_current -= CURRENT_RAMP_DOWN_PERCENT_10MS; - } - else - { - ramp_down_target_current -= diff; - } - - if (last_ramp_down_decrement_ms == 0) - { - last_ramp_down_decrement_ms = now; - } - else - { - // offset for time overshoot to not accumulate large ramp error - last_ramp_down_decrement_ms = now - (uint8_t)(time_diff - 10); - } - } - - *target_current = ramp_down_target_current; - } - else - { - ramp_down_target_current = *target_current; - } -} - -void apply_speed_limit(uint8_t* target_current) -{ - static bool speed_limiting = false; - - if (g_config.use_speed_sensor && assist_level_data.max_wheel_speed_rpm_x10 > 0) - { - int16_t current_speed_rpm_x10 = speed_sensor_get_rpm_x10(); - - if (current_speed_rpm_x10 < assist_level_data.speed_ramp_low_limit_rpm_x10) - { - // no limiting - if (speed_limiting) - { - speed_limiting = false; - eventlog_write_data(EVT_DATA_SPEED_LIMITING, 0); - } - } - else - { - if (current_speed_rpm_x10 > assist_level_data.speed_ramp_high_limit_rpm_x10) - { - if (*target_current > 1) - { - *target_current = 1; - } - } - else - { - // linear ramp down when approaching max speed. - uint8_t tmp = (uint8_t)MAP32(current_speed_rpm_x10, assist_level_data.speed_ramp_low_limit_rpm_x10, assist_level_data.speed_ramp_high_limit_rpm_x10, *target_current, 1); - if (*target_current > tmp) - { - *target_current = tmp; - } - } - - if (!speed_limiting) - { - speed_limiting = true; - eventlog_write_data(EVT_DATA_SPEED_LIMITING, 1); - } - } - } -} - -void apply_thermal_limit(uint8_t* target_current) -{ - static uint32_t next_log_temp_ms = 10000; - - static bool temperature_limiting = false; - - int16_t temp_contr_x100 = temperature_contr_x100(); - temperature_contr_c = temp_contr_x100 / 100; - - int16_t temp_motor_x100 = temperature_motor_x100(); - temperature_motor_c = temp_motor_x100 / 100; - - int16_t max_temp_x100 = MAX(temp_contr_x100, temp_motor_x100); - int8_t max_temp = MAX(temperature_contr_c, temperature_motor_c); - - if (eventlog_is_enabled() && g_config.use_temperature_sensor && system_ms() > next_log_temp_ms) - { - next_log_temp_ms = system_ms() + 10000; - eventlog_write_data(EVT_DATA_TEMPERATURE, (uint16_t)temperature_motor_c << 8 | temperature_contr_c); - } - - if (max_temp >= (MAX_TEMPERATURE - MAX_TEMPERATURE_RAMP_DOWN_INTERVAL)) - { - if (!temperature_limiting) - { - temperature_limiting = true; - eventlog_write_data(EVT_DATA_THERMAL_LIMITING, 1); - } - - if (max_temp_x100 > MAX_TEMPERATURE * 100) - { - max_temp_x100 = MAX_TEMPERATURE * 100; - } - - uint8_t tmp = (uint8_t)MAP32( - max_temp_x100, // value - (MAX_TEMPERATURE - MAX_TEMPERATURE_RAMP_DOWN_INTERVAL) * 100, // in_min - MAX_TEMPERATURE * 100, // in_max - 100, // out_min - MAX_TEMPERATURE_LOW_CURRENT_PERCENT // out_max - ); - - if (*target_current > tmp) - { - *target_current = tmp; - } - } - else - { - if (temperature_limiting) - { - temperature_limiting = false; - eventlog_write_data(EVT_DATA_THERMAL_LIMITING, 0); - } - } -} - -void apply_low_voltage_limit(uint8_t* target_current) -{ - static uint32_t next_log_volt_ms = 10000; - static bool lvc_limiting = false; - - static uint32_t next_voltage_reading_ms = 125; - static int32_t flt_min_bat_volt_x100 = 100 * 100; - - if (system_ms() > next_voltage_reading_ms) - { - next_voltage_reading_ms = system_ms() + 125; - int32_t voltage_reading_x100 = motor_get_battery_voltage_x10() * 10ul; - - if (voltage_reading_x100 < flt_min_bat_volt_x100) - { - flt_min_bat_volt_x100 = EXPONENTIAL_FILTER(flt_min_bat_volt_x100, voltage_reading_x100, 8); - } - - if (eventlog_is_enabled() && system_ms() > next_log_volt_ms) - { - next_log_volt_ms = system_ms() + 10000; - eventlog_write_data(EVT_DATA_VOLTAGE, (uint16_t)voltage_reading_x100); - } - } - - uint16_t voltage_x100 = flt_min_bat_volt_x100; - - if (voltage_x100 <= lvc_ramp_down_start_voltage_x100) - { - if (!lvc_limiting) - { - eventlog_write_data(EVT_DATA_LVC_LIMITING, voltage_x100); - lvc_limiting = true; - } - - if (voltage_x100 < lvc_voltage_x100) - { - voltage_x100 = lvc_voltage_x100; - } - - // ramp down power until 20% when approaching lvc - uint8_t tmp = (uint8_t)MAP32( - voltage_x100, // value - lvc_voltage_x100, // in_min - lvc_ramp_down_start_voltage_x100, // in_max - LVC_LOW_CURRENT_PERCENT, // out_min - 100 // out_max - ); - - if (*target_current > tmp) - { - *target_current = tmp; - } - } -} - -#if HAS_SHIFT_SENSOR_SUPPORT -void apply_shift_sensor_interrupt(uint8_t* target_current) -{ - static uint32_t shift_sensor_act_ms = 0; - static bool shift_sensor_last = false; - static bool shift_sensor_interrupting = false; - static bool shift_sensor_logged = false; - - // Exit immediately if shift interrupts disabled. - if (!g_config.use_shift_sensor) - { - return; - } - - bool active = shift_sensor_is_activated(); - if (active) - { - // Check for new pulse from the gear sensor during shift interrupt - if (!shift_sensor_last && shift_sensor_interrupting) - { - // Consecutive gear change, do restart. - shift_sensor_interrupting = false; - } - if (!shift_sensor_interrupting) - { - uint16_t duration_ms = EXPAND_U16( - g_config.shift_interrupt_duration_ms_u16h, - g_config.shift_interrupt_duration_ms_u16l - ); - shift_sensor_act_ms = system_ms() + duration_ms; - shift_sensor_interrupting = true; - } - shift_sensor_last = true; - } - else - { - shift_sensor_last = false; - } - - if (!shift_sensor_interrupting) - { - return; - } - - if (system_ms() >= shift_sensor_act_ms) - { - // Shift is finished, reset function state. - shift_sensor_interrupting = false; - // Logging is skipped, unless current has been clamped during shift interrupt. - if (shift_sensor_logged) - { - shift_sensor_logged = false; - eventlog_write_data(EVT_DATA_SHIFT_SENSOR, 0); - } - return; - } - - if ((*target_current) > g_config.shift_interrupt_current_threshold_percent) - { - if (!shift_sensor_logged) - { - // Logging only once per shifting interrupt. - shift_sensor_logged = true; - eventlog_write_data(EVT_DATA_SHIFT_SENSOR, 1); - } - // Set target current based on desired current threshold during shift. - *target_current = g_config.shift_interrupt_current_threshold_percent; - } -} -#endif - -void apply_brake(uint8_t* target_current) -{ - if (brake_is_activated()) - { - *target_current = 0; - } -} - -bool check_power_block() -{ - if (power_blocked_until_ms != 0) - { - // power block is active, check if time to release - if (system_ms() > power_blocked_until_ms) - { - power_blocked_until_ms = 0; - return false; - } - - return true; - } - - return false; -} - -void block_power_for(uint16_t ms) -{ - power_blocked_until_ms = system_ms() + ms; -} - - -void reload_assist_params() -{ - if (assist_level < ASSIST_PUSH) - { - assist_level_data.level = g_config.assist_levels[operation_mode][assist_level]; - - assist_level_data.max_wheel_speed_rpm_x10 = ((uint32_t)((uint32_t)global_max_speed_rpm * assist_level_data.level.max_speed_percent) / 10); - - if (assist_level_data.level.flags & ASSIST_FLAG_PAS) - { - assist_level_data.keep_current_target_percent = (uint8_t)((uint16_t)g_config.pas_keep_current_percent * assist_level_data.level.target_current_percent / 100); - assist_level_data.keep_current_ramp_start_rpm_x10 = g_config.pas_keep_current_cadence_rpm * 10; - assist_level_data.keep_current_ramp_end_rpm_x10 = (uint16_t)(((uint32_t)assist_level_data.level.max_cadence_percent * MAX_CADENCE_RPM_X10) / 100); - } - - // pause cruise if swiching level - cruise_paused = true; - } - // only apply push walk params if push walk is active in config, - // otherwise data of previous assist level is kept. - else if (assist_level == ASSIST_PUSH && g_config.use_push_walk) - { - assist_level_data.level.flags = 0; - assist_level_data.level.target_current_percent = 0; - assist_level_data.level.max_speed_percent = 0; - assist_level_data.level.max_cadence_percent = 15; - assist_level_data.level.max_throttle_current_percent = 0; - - assist_level_data.max_wheel_speed_rpm_x10 = convert_wheel_speed_kph_to_rpm(WALK_MODE_SPEED_KPH) * 10; - } - - // compute speed ramp intervals - assist_level_data.speed_ramp_low_limit_rpm_x10 = assist_level_data.max_wheel_speed_rpm_x10 - speed_limit_ramp_interval_rpm_x10; - assist_level_data.speed_ramp_high_limit_rpm_x10 = assist_level_data.max_wheel_speed_rpm_x10 + speed_limit_ramp_interval_rpm_x10; -} - -uint16_t convert_wheel_speed_kph_to_rpm(uint8_t speed_kph) -{ - float radius_mm = EXPAND_U16(g_config.wheel_size_inch_x10_u16h, g_config.wheel_size_inch_x10_u16l) * 1.27f; // g_config.wheel_size_inch_x10 / 2.f * 2.54f; - return (uint16_t)(25000.f / (3 * 3.14159f * radius_mm) * speed_kph); -} +/* + * bbs-fw + * + * Copyright (C) Daniel Nilsson, 2022. + * + * Released under the GPL License, Version 3 + */ + +#include "app.h" +#include "fwconfig.h" +#include "cfgstore.h" +#include "motor.h" +#include "sensors.h" +#include "throttle.h" +#include "lights.h" +#include "uart.h" +#include "eventlog.h" +#include "util.h" +#include "system.h" + +// Compile time options + +// Number of PAS sensor pulses to engage cruise mode, +// there are 24 pulses per revolution. +#define CRUISE_ENGAGE_PAS_PULSES 12 + +// Number of PAS sensor pulses to disengage curise mode +// by pedaling backwards. There are 24 pulses per revolution. +#define CRUISE_DISENGAGE_PAS_PULSES 4 + + +typedef struct +{ + assist_level_t level; + + // cached precomputed values + // --------------------------------- + + // speed + int32_t max_wheel_speed_rpm_x10; + + // pas + uint8_t keep_current_target_percent; + uint16_t keep_current_ramp_start_rpm_x10; + uint16_t keep_current_ramp_end_rpm_x10; + +} assist_level_data_t; + +static uint8_t assist_level; +static uint8_t operation_mode; +static uint16_t global_speed_limit_rpm; +static int32_t global_throttle_speed_limit_rpm_x10; + +static uint16_t lvc_voltage_x100; +static uint16_t lvc_ramp_down_start_voltage_x100; + +static assist_level_data_t assist_level_data; +static uint16_t speed_limit_ramp_interval_rpm_x10; + +static bool cruise_paused; +static int8_t temperature_contr_c; +static int8_t temperature_motor_c; + +static uint16_t ramp_up_current_interval_ms; +static uint32_t power_blocked_until_ms; + +void apply_pas_cadence(uint8_t* target_current, uint8_t throttle_percent); +#if HAS_TORQUE_SENSOR +void apply_pas_torque(uint8_t* target_current); +#endif + +void apply_cruise(uint8_t* target_current, uint8_t throttle_percent); +bool apply_throttle(uint8_t* target_current, uint8_t throttle_percent); +bool apply_speed_limit(uint8_t* target_current, uint8_t throttle_percent, bool throttle_override); +bool apply_thermal_limit(uint8_t* target_current); +bool apply_low_voltage_limit(uint8_t* target_current); +bool apply_shift_sensor_interrupt(uint8_t* target_current); +bool apply_brake(uint8_t* target_current); +void apply_current_ramp_up(uint8_t* target_current, bool enable); +void apply_current_ramp_down(uint8_t* target_current, bool enable); + +bool check_power_block(); +void block_power_for(uint16_t ms); + +void reload_assist_params(); + +uint16_t convert_wheel_speed_kph_to_rpm(uint8_t speed_kph); + +void app_init() +{ + motor_disable(); + lights_disable(); + lights_set(g_config.lights_always_on); + + lvc_voltage_x100 = g_config.low_cut_off_v * 100u; + + uint16_t voltage_range_x100 = + EXPAND_U16(g_config.max_battery_x100v_u16h, g_config.max_battery_x100v_u16l) - lvc_voltage_x100; + lvc_ramp_down_start_voltage_x100 = (uint16_t)(lvc_voltage_x100 + + ((voltage_range_x100 * LVC_RAMP_DOWN_OFFSET_PERCENT) / 100)); + + global_speed_limit_rpm = 0; + global_throttle_speed_limit_rpm_x10 = 0; + temperature_contr_c = 0; + temperature_motor_c = 0; + + ramp_up_current_interval_ms = (g_config.max_current_amps * 10u) / g_config.current_ramp_amps_s; + power_blocked_until_ms = 0; + + speed_limit_ramp_interval_rpm_x10 = convert_wheel_speed_kph_to_rpm(SPEED_LIMIT_RAMP_DOWN_INTERVAL_KPH) * 10; + + cruise_paused = true; + operation_mode = OPERATION_MODE_DEFAULT; + + app_set_wheel_max_speed_rpm(convert_wheel_speed_kph_to_rpm(g_config.max_speed_kph)); + app_set_assist_level(g_config.assist_startup_level); + reload_assist_params(); + + if (g_config.assist_mode_select == ASSIST_MODE_SELECT_BRAKE_BOOT && brake_is_activated()) + { + app_set_operation_mode(OPERATION_MODE_SPORT); + } +} + +void app_process() +{ + uint8_t target_current = 0; + uint8_t target_cadence = assist_level_data.level.max_cadence_percent; + uint8_t throttle_percent = throttle_read(); + bool throttle_override = false; + + if (check_power_block()) + { + target_current = 0; + } + else if (assist_level == ASSIST_PUSH && g_config.use_push_walk) + { + target_current = 10; + } + else + { + apply_pas_cadence(&target_current, throttle_percent); +#if HAS_TORQUE_SENSOR + apply_pas_torque(&target_current); +#endif // HAS_TORQUE_SENSOR + + apply_cruise(&target_current, throttle_percent); + + throttle_override = apply_throttle(&target_current, throttle_percent); + + // override target cadence if configured in assist level + if (throttle_override && + (assist_level_data.level.flags & ASSIST_FLAG_PAS) && + (assist_level_data.level.flags & ASSIST_FLAG_OVERRIDE_CADENCE)) + { + target_cadence = THROTTLE_CADENCE_OVERRIDE_PERCENT; + } + } + + bool speed_limiting = apply_speed_limit(&target_current, throttle_percent, throttle_override); + bool thermal_limiting = apply_thermal_limit(&target_current); + bool lvc_limiting = apply_low_voltage_limit(&target_current); + bool shift_limiting = +#if HAS_SHIFT_SENSOR_SUPPORT + apply_shift_sensor_interrupt(&target_current); +#else + false; +#endif + bool is_limiting = speed_limiting || thermal_limiting || lvc_limiting || shift_limiting; + bool is_braking = apply_brake(&target_current); + + apply_current_ramp_up(&target_current, is_limiting || !throttle_override); + apply_current_ramp_down(&target_current, !is_braking); + + motor_set_target_speed(target_cadence); + motor_set_target_current(target_current); + + if (target_current > 0) + { + motor_enable(); + } + else + { + motor_disable(); + } + + if (motor_status() & MOTOR_ERROR_LVC) + { + lights_disable(); + } + else + { + lights_enable(); + } +} + + +void app_set_assist_level(uint8_t level) +{ + if (assist_level != level) + { + if (assist_level == ASSIST_PUSH && g_config.use_push_walk) + { + // When releasig push walk mode pedals may have been rotating + // with the motor, block motor power for 2 seconds to prevent PAS + // sensor from incorrectly applying power if returning to a PAS level. + block_power_for(1000); + } + + assist_level = level; + eventlog_write_data(EVT_DATA_ASSIST_LEVEL, assist_level); + reload_assist_params(); + } +} + +void app_set_lights(bool on) +{ + static bool last_light_state = false; + + if ( // it's ok to write ugly code if you say it's ugly... + (g_config.assist_mode_select == ASSIST_MODE_SELECT_LIGHTS) || + (assist_level == ASSIST_0 && g_config.assist_mode_select == ASSIST_MODE_SELECT_PAS0_LIGHT) || + (assist_level == ASSIST_1 && g_config.assist_mode_select == ASSIST_MODE_SELECT_PAS1_LIGHT) || + (assist_level == ASSIST_2 && g_config.assist_mode_select == ASSIST_MODE_SELECT_PAS2_LIGHT) || + (assist_level == ASSIST_3 && g_config.assist_mode_select == ASSIST_MODE_SELECT_PAS3_LIGHT) || + (assist_level == ASSIST_4 && g_config.assist_mode_select == ASSIST_MODE_SELECT_PAS4_LIGHT) || + (assist_level == ASSIST_5 && g_config.assist_mode_select == ASSIST_MODE_SELECT_PAS5_LIGHT) || + (assist_level == ASSIST_6 && g_config.assist_mode_select == ASSIST_MODE_SELECT_PAS6_LIGHT) || + (assist_level == ASSIST_7 && g_config.assist_mode_select == ASSIST_MODE_SELECT_PAS7_LIGHT) || + (assist_level == ASSIST_8 && g_config.assist_mode_select == ASSIST_MODE_SELECT_PAS8_LIGHT) || + (assist_level == ASSIST_9 && g_config.assist_mode_select == ASSIST_MODE_SELECT_PAS9_LIGHT) + ) + { + if (on) + { + app_set_operation_mode(OPERATION_MODE_SPORT); + } + else + { + app_set_operation_mode(OPERATION_MODE_DEFAULT); + } + } + else + { + if (g_config.lights_always_on) + { + on = true; + } + + if (last_light_state != on) + { + last_light_state = on; + eventlog_write_data(EVT_DATA_LIGHTS, on); + lights_set(on); + } + } +} + +void app_set_operation_mode(uint8_t mode) +{ + if (operation_mode != mode) + { + operation_mode = mode; + eventlog_write_data(EVT_DATA_OPERATION_MODE, operation_mode); + reload_assist_params(); + } +} + +void app_set_wheel_max_speed_rpm(uint16_t value) +{ + if (global_speed_limit_rpm != value) + { + global_speed_limit_rpm = value; + global_throttle_speed_limit_rpm_x10 = ((int32_t)global_speed_limit_rpm * + g_config.throttle_global_spd_lim_percent) / 10; + + eventlog_write_data(EVT_DATA_WHEEL_SPEED_PPM, value); + reload_assist_params(); + } +} + +uint8_t app_get_assist_level() +{ + return assist_level; +} + +uint8_t app_get_status_code() +{ + uint16_t motor = motor_status(); + + if (motor & MOTOR_ERROR_HALL_SENSOR) + { + return STATUS_ERROR_HALL_SENSOR; + } + + if (motor & MOTOR_ERROR_CURRENT_SENSE) + { + return STATUS_ERROR_CURRENT_SENSE; + } + + if (motor & MOTOR_ERROR_POWER_RESET) + { + // Phase line error code reused, cause and meaning + // of MOTOR_ERROR_POWER_RESET triggered on bbs02 is currently unknown + return STATUS_ERROR_PHASE_LINE; + } + + if (!throttle_ok()) + { + return STATUS_ERROR_THROTTLE; + } + + if (!torque_sensor_ok()) + { + return STATUS_ERROR_TORQUE_SENSOR; + } + + if (temperature_motor_c > MAX_TEMPERATURE) + { + return STATUS_ERROR_MOTOR_OVER_TEMP; + } + + if (temperature_contr_c > MAX_TEMPERATURE) + { + return STATUS_ERROR_CONTROLLER_OVER_TEMP; + } + + // Disable LVC error since it is not shown on display in original firmware + // Uncomment if you want to enable + // if (motor & MOTOR_ERROR_LVC) + // { + // return STATUS_ERROR_LVC; + // } + + if (brake_is_activated()) + { + return STATUS_BRAKING; + } + + return STATUS_NORMAL; +} + +uint8_t app_get_temperature() +{ + int8_t temp_max = MAX(temperature_contr_c, temperature_motor_c); + + if (temp_max < 0) + { + return 0; + } + + return (uint8_t)temp_max; +} + + +void apply_pas_cadence(uint8_t* target_current, uint8_t throttle_percent) +{ + if ((assist_level_data.level.flags & ASSIST_FLAG_PAS) && !(assist_level_data.level.flags & ASSIST_FLAG_PAS_TORQUE)) + { + if (pas_is_pedaling_forwards() && pas_get_pulse_counter() > g_config.pas_start_delay_pulses) + { + if (assist_level_data.level.flags & ASSIST_FLAG_PAS_VARIABLE) + { + uint8_t current = (uint8_t)MAP16(throttle_percent, 0, 100, 0, assist_level_data.level.target_current_percent); + if (current > *target_current) + { + *target_current = current; + } + } + else + { + if (assist_level_data.level.target_current_percent > *target_current) + { + *target_current = assist_level_data.level.target_current_percent; + } + + // apply "keep current" ramp + if (g_config.pas_keep_current_percent < 100) + { + if (*target_current > assist_level_data.keep_current_target_percent && + pas_get_cadence_rpm_x10() > assist_level_data.keep_current_ramp_start_rpm_x10) + { + uint32_t cadence = MIN(pas_get_cadence_rpm_x10(), assist_level_data.keep_current_ramp_end_rpm_x10); + + // ramp down current towards keep_current_target_percent with rpm above keep_current_ramp_start_rpm_x10 + *target_current = MAP32( + cadence, // in + assist_level_data.keep_current_ramp_start_rpm_x10, // in_min + assist_level_data.keep_current_ramp_end_rpm_x10, // in_max + *target_current, // out_min + assist_level_data.keep_current_target_percent); // out_max + } + } + } + } + } +} + +#if HAS_TORQUE_SENSOR +void apply_pas_torque(uint8_t* target_current) +{ + if ((assist_level_data.level.flags & ASSIST_FLAG_PAS) && (assist_level_data.level.flags & ASSIST_FLAG_PAS_TORQUE)) + { + if (pas_is_pedaling_forwards() && (pas_get_pulse_counter() > g_config.pas_start_delay_pulses || speed_sensor_is_moving())) + { + uint16_t torque_nm_x100 = torque_sensor_get_nm_x100(); + uint16_t cadence_rpm_x10 = pas_get_cadence_rpm_x10(); + if (cadence_rpm_x10 < TORQUE_POWER_LOWER_RPM_X10) + { + cadence_rpm_x10 = TORQUE_POWER_LOWER_RPM_X10; + } + + uint16_t pedal_power_w_x10 = (uint16_t)(((uint32_t)torque_nm_x100 * cadence_rpm_x10) / 955); + uint16_t target_current_amp_x100 = (uint16_t)(((uint32_t)10 * pedal_power_w_x10 * + assist_level_data.level.torque_amplification_factor_x10) / motor_get_battery_voltage_x10()); + uint16_t max_current_amp_x100 = g_config.max_current_amps * 100; + + // limit target to ensure no overflow in map result + if (target_current_amp_x100 > max_current_amp_x100) + { + target_current_amp_x100 = max_current_amp_x100; + } + uint8_t tmp_percent = (uint8_t)MAP32(target_current_amp_x100, 0, max_current_amp_x100, 0, 100); + + // minimum 1 percent current if pedaling + if (tmp_percent < 1) + { + tmp_percent = 1; + } + // limit to maximum assist current for set level + else if (tmp_percent > assist_level_data.level.target_current_percent) + { + tmp_percent = assist_level_data.level.target_current_percent; + } + + if (tmp_percent > *target_current) + { + *target_current = tmp_percent; + } + } + } +} +#endif + +void apply_cruise(uint8_t* target_current, uint8_t throttle_percent) +{ + static bool cruise_block_throttle_return = false; + + if ((assist_level_data.level.flags & ASSIST_FLAG_CRUISE) && throttle_ok()) + { + // pause cruise if brake activated + if (brake_is_activated()) + { + cruise_paused = true; + cruise_block_throttle_return = true; + } + + // pause cruise if started pedaling backwards + else if (pas_is_pedaling_backwards() && pas_get_pulse_counter() > CRUISE_DISENGAGE_PAS_PULSES) + { + cruise_paused = true; + cruise_block_throttle_return = true; + } + + // pause cruise if throttle touched while cruise active + else if (!cruise_paused && !cruise_block_throttle_return && throttle_percent > 0) + { + cruise_paused = true; + cruise_block_throttle_return = true; + } + + // unpause cruise if pedaling forward while engaging throttle > 50% + else if (cruise_paused && !cruise_block_throttle_return && throttle_percent > 50 && pas_is_pedaling_forwards() && pas_get_pulse_counter() > CRUISE_ENGAGE_PAS_PULSES) + { + cruise_paused = false; + cruise_block_throttle_return = true; + } + + // reset flag tracking throttle to make sure throttle returns to idle position before engage/disenage cruise with throttle touch + else if (cruise_block_throttle_return && throttle_percent == 0) + { + cruise_block_throttle_return = false; + } + + if (cruise_paused) + { + *target_current = 0; + } + else + { + if (assist_level_data.level.target_current_percent > *target_current) + { + *target_current = assist_level_data.level.target_current_percent; + } + } + } +} + +bool apply_throttle(uint8_t* target_current, uint8_t throttle_percent) +{ + if ((assist_level_data.level.flags & ASSIST_FLAG_THROTTLE) && throttle_percent > 0 && throttle_ok()) + { + uint8_t current = (uint8_t)MAP16(throttle_percent, 0, 100, g_config.throttle_start_percent, assist_level_data.level.max_throttle_current_percent); + + // Throttle always overrides PAS if global speed limit is configured for throttle. + bool global_throttle_limit_active = + g_config.throttle_global_spd_lim_percent > 0 && + ( + g_config.throttle_global_spd_lim_opt == THROTTLE_GLOBAL_SPEED_LIMIT_ENABLED || + (g_config.throttle_global_spd_lim_opt == THROTTLE_GLOBAL_SPEED_LIMIT_STD_LVLS && operation_mode == OPERATION_MODE_DEFAULT) + ); + + if (current >= *target_current || global_throttle_limit_active) + { + *target_current = current; + return true; + } + } + + return false; +} + + +bool apply_speed_limit(uint8_t* target_current, uint8_t throttle_percent, bool throttle_override) +{ + static bool speed_limiting = false; + + if (!g_config.use_speed_sensor) + { + return false; + } + + bool global_throttle_limit_active = + assist_level != ASSIST_PUSH && + throttle_percent > 0 && + g_config.throttle_global_spd_lim_percent > 0 && + ( + g_config.throttle_global_spd_lim_opt == THROTTLE_GLOBAL_SPEED_LIMIT_ENABLED || + (g_config.throttle_global_spd_lim_opt == THROTTLE_GLOBAL_SPEED_LIMIT_STD_LVLS && operation_mode == OPERATION_MODE_DEFAULT) + ); + + bool throttle_speed_override_active = !global_throttle_limit_active && throttle_override && + (assist_level_data.level.flags & ASSIST_FLAG_PAS) && + (assist_level_data.level.flags & ASSIST_FLAG_OVERRIDE_SPEED); + + int32_t max_speed_rpm_x10; + if (global_throttle_limit_active) + { + // use configured global throttle override speed limit + max_speed_rpm_x10 = global_throttle_speed_limit_rpm_x10; + } + else if (throttle_speed_override_active) + { + // override assist level speed limit to global speed limit + max_speed_rpm_x10 = global_speed_limit_rpm * 10; + } + else + { + // normal operation, use configured assist level speed limit + max_speed_rpm_x10 = assist_level_data.max_wheel_speed_rpm_x10; + } + + int32_t max_speed_ramp_low_rpm_x10 = max_speed_rpm_x10 - speed_limit_ramp_interval_rpm_x10; + int32_t max_speed_ramp_high_rpm_x10 = max_speed_rpm_x10 + speed_limit_ramp_interval_rpm_x10; + + if (max_speed_rpm_x10 > 0) + { + int16_t current_speed_rpm_x10 = speed_sensor_get_rpm_x10(); + + if (current_speed_rpm_x10 < max_speed_ramp_low_rpm_x10) + { + // no limiting + if (speed_limiting) + { + speed_limiting = false; + eventlog_write_data(EVT_DATA_SPEED_LIMITING, 0); + } + } + else + { + if (!speed_limiting) + { + speed_limiting = true; + eventlog_write_data(EVT_DATA_SPEED_LIMITING, 1); + } + + if (current_speed_rpm_x10 > max_speed_ramp_high_rpm_x10) + { + if (*target_current > 1) + { + *target_current = 1; + return true; + } + } + else + { + // linear ramp down when approaching max speed. + uint8_t tmp = (uint8_t)MAP32(current_speed_rpm_x10, max_speed_ramp_low_rpm_x10, max_speed_ramp_high_rpm_x10, *target_current, 1); + if (*target_current > tmp) + { + *target_current = tmp; + return true; + } + } + } + } + + return false; +} + +bool apply_thermal_limit(uint8_t* target_current) +{ + static uint32_t next_log_temp_ms = 10000; + + static bool temperature_limiting = false; + + int16_t temp_contr_x100 = temperature_contr_x100(); + temperature_contr_c = temp_contr_x100 / 100; + + int16_t temp_motor_x100 = temperature_motor_x100(); + temperature_motor_c = temp_motor_x100 / 100; + + int16_t max_temp_x100 = MAX(temp_contr_x100, temp_motor_x100); + int8_t max_temp = MAX(temperature_contr_c, temperature_motor_c); + + if (eventlog_is_enabled() && g_config.use_temperature_sensor && system_ms() > next_log_temp_ms) + { + next_log_temp_ms = system_ms() + 10000; + eventlog_write_data(EVT_DATA_TEMPERATURE, (uint16_t)temperature_motor_c << 8 | temperature_contr_c); + } + + if (max_temp >= (MAX_TEMPERATURE - MAX_TEMPERATURE_RAMP_DOWN_INTERVAL)) + { + if (!temperature_limiting) + { + temperature_limiting = true; + eventlog_write_data(EVT_DATA_THERMAL_LIMITING, 1); + } + + if (max_temp_x100 > MAX_TEMPERATURE * 100) + { + max_temp_x100 = MAX_TEMPERATURE * 100; + } + + uint8_t tmp = (uint8_t)MAP32( + max_temp_x100, // value + (MAX_TEMPERATURE - MAX_TEMPERATURE_RAMP_DOWN_INTERVAL) * 100, // in_min + MAX_TEMPERATURE * 100, // in_max + 100, // out_min + MAX_TEMPERATURE_LOW_CURRENT_PERCENT // out_max + ); + + if (*target_current > tmp) + { + *target_current = tmp; + return true; + } + } + else + { + if (temperature_limiting) + { + temperature_limiting = false; + eventlog_write_data(EVT_DATA_THERMAL_LIMITING, 0); + } + } + + return false; +} + +bool apply_low_voltage_limit(uint8_t* target_current) +{ + static uint32_t next_log_volt_ms = 10000; + static bool lvc_limiting = false; + + static uint32_t next_voltage_reading_ms = 125; + static int32_t flt_min_bat_volt_x100 = 100 * 100; + + if (system_ms() > next_voltage_reading_ms) + { + next_voltage_reading_ms = system_ms() + 125; + int32_t voltage_reading_x100 = motor_get_battery_voltage_x10() * 10ul; + + if (voltage_reading_x100 < flt_min_bat_volt_x100) + { + flt_min_bat_volt_x100 = EXPONENTIAL_FILTER(flt_min_bat_volt_x100, voltage_reading_x100, 8); + } + + if (eventlog_is_enabled() && system_ms() > next_log_volt_ms) + { + next_log_volt_ms = system_ms() + 10000; + eventlog_write_data(EVT_DATA_VOLTAGE, (uint16_t)voltage_reading_x100); + } + } + + uint16_t voltage_x100 = flt_min_bat_volt_x100; + + if (voltage_x100 <= lvc_ramp_down_start_voltage_x100) + { + if (!lvc_limiting) + { + eventlog_write_data(EVT_DATA_LVC_LIMITING, voltage_x100); + lvc_limiting = true; + } + + if (voltage_x100 < lvc_voltage_x100) + { + voltage_x100 = lvc_voltage_x100; + } + + // ramp down power until 20% when approaching lvc + uint8_t tmp = (uint8_t)MAP32( + voltage_x100, // value + lvc_voltage_x100, // in_min + lvc_ramp_down_start_voltage_x100, // in_max + LVC_LOW_CURRENT_PERCENT, // out_min + 100 // out_max + ); + + if (*target_current > tmp) + { + *target_current = tmp; + return true; + } + } + + return false; +} + +#if HAS_SHIFT_SENSOR_SUPPORT +bool apply_shift_sensor_interrupt(uint8_t* target_current) +{ + static uint32_t shift_sensor_act_ms = 0; + static bool shift_sensor_last = false; + static bool shift_sensor_interrupting = false; + static bool shift_sensor_logged = false; + + // Exit immediately if shift interrupts disabled. + if (!g_config.use_shift_sensor) + { + return false; + } + + bool active = shift_sensor_is_activated(); + if (active) + { + // Check for new pulse from the gear sensor during shift interrupt + if (!shift_sensor_last && shift_sensor_interrupting) + { + // Consecutive gear change, do restart. + shift_sensor_interrupting = false; + } + if (!shift_sensor_interrupting) + { + uint16_t duration_ms = EXPAND_U16( + g_config.shift_interrupt_duration_ms_u16h, + g_config.shift_interrupt_duration_ms_u16l + ); + shift_sensor_act_ms = system_ms() + duration_ms; + shift_sensor_interrupting = true; + } + shift_sensor_last = true; + } + else + { + shift_sensor_last = false; + } + + if (!shift_sensor_interrupting) + { + return false; + } + + if (system_ms() >= shift_sensor_act_ms) + { + // Shift is finished, reset function state. + shift_sensor_interrupting = false; + // Logging is skipped, unless current has been clamped during shift interrupt. + if (shift_sensor_logged) + { + shift_sensor_logged = false; + eventlog_write_data(EVT_DATA_SHIFT_SENSOR, 0); + } + return false; + } + + if ((*target_current) > g_config.shift_interrupt_current_threshold_percent) + { + if (!shift_sensor_logged) + { + // Logging only once per shifting interrupt. + shift_sensor_logged = true; + eventlog_write_data(EVT_DATA_SHIFT_SENSOR, 1); + } + // Set target current based on desired current threshold during shift. + *target_current = g_config.shift_interrupt_current_threshold_percent; + + return true; + } + + return false; +} +#endif + +bool apply_brake(uint8_t* target_current) +{ + if (brake_is_activated()) + { + *target_current = 0; + return true; + } + + return false; +} + +void apply_current_ramp_up(uint8_t* target_current, bool enable) +{ + static uint8_t ramp_up_target_current = 0; + static uint32_t last_ramp_up_increment_ms = 0; + + if (enable && *target_current > ramp_up_target_current) + { + uint32_t now = system_ms(); + uint16_t time_diff = now - last_ramp_up_increment_ms; + + if (time_diff >= ramp_up_current_interval_ms) + { + ++ramp_up_target_current; + + if (last_ramp_up_increment_ms == 0) + { + last_ramp_up_increment_ms = now; + } + else + { + // offset for time overshoot to not accumulate large ramp error + last_ramp_up_increment_ms = now - (uint8_t)(time_diff - ramp_up_current_interval_ms); + } + } + + *target_current = ramp_up_target_current; + } + else + { + ramp_up_target_current = *target_current; + last_ramp_up_increment_ms = 0; + } +} + +void apply_current_ramp_down(uint8_t* target_current, bool enable) +{ + static uint8_t ramp_down_target_current = 0; + static uint32_t last_ramp_down_decrement_ms = 0; + + // apply fast ramp down if coming from high target current (> 50%) + if (enable && *target_current < ramp_down_target_current) + { + uint32_t now = system_ms(); + uint16_t time_diff = now - last_ramp_down_decrement_ms; + + if (time_diff >= 10) + { + uint8_t diff = ramp_down_target_current - *target_current; + + if (diff >= CURRENT_RAMP_DOWN_PERCENT_10MS) + { + ramp_down_target_current -= CURRENT_RAMP_DOWN_PERCENT_10MS; + } + else + { + ramp_down_target_current -= diff; + } + + if (last_ramp_down_decrement_ms == 0) + { + last_ramp_down_decrement_ms = now; + } + else + { + // offset for time overshoot to not accumulate large ramp error + last_ramp_down_decrement_ms = now - (uint8_t)(time_diff - 10); + } + } + + *target_current = ramp_down_target_current; + } + else + { + ramp_down_target_current = *target_current; + last_ramp_down_decrement_ms = 0; + } +} + + +bool check_power_block() +{ + if (power_blocked_until_ms != 0) + { + // power block is active, check if time to release + if (system_ms() > power_blocked_until_ms) + { + power_blocked_until_ms = 0; + return false; + } + + return true; + } + + return false; +} + +void block_power_for(uint16_t ms) +{ + power_blocked_until_ms = system_ms() + ms; +} + + +void reload_assist_params() +{ + if (assist_level < ASSIST_PUSH) + { + assist_level_data.level = g_config.assist_levels[operation_mode][assist_level]; + + assist_level_data.max_wheel_speed_rpm_x10 = ((int32_t)global_speed_limit_rpm * assist_level_data.level.max_speed_percent) / 10; + + if (assist_level_data.level.flags & ASSIST_FLAG_PAS) + { + assist_level_data.keep_current_target_percent = (uint8_t)((uint16_t)g_config.pas_keep_current_percent * assist_level_data.level.target_current_percent / 100); + assist_level_data.keep_current_ramp_start_rpm_x10 = g_config.pas_keep_current_cadence_rpm * 10; + assist_level_data.keep_current_ramp_end_rpm_x10 = (uint16_t)(((uint32_t)assist_level_data.level.max_cadence_percent * MAX_CADENCE_RPM_X10) / 100); + } + + // pause cruise if swiching level + cruise_paused = true; + } + // only apply push walk params if push walk is active in config, + // otherwise data of previous assist level is kept. + else if (assist_level == ASSIST_PUSH && g_config.use_push_walk) + { + assist_level_data.level.flags = 0; + assist_level_data.level.target_current_percent = 0; + assist_level_data.level.max_speed_percent = 0; + assist_level_data.level.max_cadence_percent = 15; + assist_level_data.level.max_throttle_current_percent = 0; + + assist_level_data.max_wheel_speed_rpm_x10 = convert_wheel_speed_kph_to_rpm(WALK_MODE_SPEED_KPH) * 10; + } +} + +uint16_t convert_wheel_speed_kph_to_rpm(uint8_t speed_kph) +{ + float radius_mm = EXPAND_U16(g_config.wheel_size_inch_x10_u16h, g_config.wheel_size_inch_x10_u16l) * 1.27f; // g_config.wheel_size_inch_x10 / 2.f * 2.54f; + return (uint16_t)(25000.f / (3 * 3.14159f * radius_mm) * speed_kph); +} diff --git a/src/firmware/cfgstore.c b/src/firmware/cfgstore.c index 03fff72..a8a6073 100644 --- a/src/firmware/cfgstore.c +++ b/src/firmware/cfgstore.c @@ -180,10 +180,11 @@ static void load_default_config() g_config.throttle_start_voltage_mv_u16l = (uint8_t)900; g_config.throttle_start_voltage_mv_u16h = (uint8_t)(900 >> 8); - g_config.throttle_end_voltage_mv_u16l = (uint8_t)3600; g_config.throttle_end_voltage_mv_u16h = (uint8_t)(3600 >> 8); g_config.throttle_start_percent = 1; + g_config.throttle_global_spd_lim_opt = THROTTLE_GLOBAL_SPEED_LIMIT_DISABLED; + g_config.throttle_global_spd_lim_percent = 100; g_config.shift_interrupt_duration_ms_u16l = (uint8_t)600; g_config.shift_interrupt_duration_ms_u16h = (uint8_t)(600 >> 8); diff --git a/src/firmware/cfgstore.h b/src/firmware/cfgstore.h index deff1ac..f867914 100644 --- a/src/firmware/cfgstore.h +++ b/src/firmware/cfgstore.h @@ -45,6 +45,9 @@ #define WALK_MODE_DATA_REQUESTED_POWER 2 #define WALK_MODE_DATA_BATTERY_PERCENT 3 +#define THROTTLE_GLOBAL_SPEED_LIMIT_DISABLED 0 +#define THROTTLE_GLOBAL_SPEED_LIMIT_ENABLED 1 +#define THROTTLE_GLOBAL_SPEED_LIMIT_STD_LVLS 2 #define CONFIG_VERSION 4 #define PSTATE_VERSION 1 @@ -81,8 +84,6 @@ typedef struct uint8_t use_shift_sensor; uint8_t use_push_walk; uint8_t use_temperature_sensor; - - // lights uint8_t lights_always_on; // speed sensor @@ -102,6 +103,8 @@ typedef struct uint8_t throttle_end_voltage_mv_u16l; uint8_t throttle_end_voltage_mv_u16h; uint8_t throttle_start_percent; + uint8_t throttle_global_spd_lim_opt; + uint8_t throttle_global_spd_lim_percent; // shift interrupt options uint8_t shift_interrupt_duration_ms_u16l; diff --git a/src/tool/Model/Configuration.cs b/src/tool/Model/Configuration.cs index 450f6f1..0e9bc77 100644 --- a/src/tool/Model/Configuration.cs +++ b/src/tool/Model/Configuration.cs @@ -1,735 +1,761 @@ -using System; -using System.IO; -using System.Linq; -using System.Text; -using System.Xml; -using System.Xml.Serialization; - -namespace BBSFW.Model -{ - - [XmlRoot("BBSFW", Namespace ="https://github.com/danielnilsson9/bbs-fw")] - public class Configuration - { - public const int CurrentVersion = 4; - public const int MinVersion = 1; - public const int MaxVersion = CurrentVersion; - - public const int ByteSizeV1 = 120; - public const int ByteSizeV2 = 124; - public const int ByteSizeV3 = 149; - public const int ByteSizeV4 = 150; - - public enum Feature - { - ShiftSensor, - TorqueSensor, - ControllerTemperatureSensor, - MotorTemperatureSensor - } - - - public static int GetByteSize(int version) - { - switch (version) - { - case 1: - return ByteSizeV1; - case 2: - return ByteSizeV2; - case 3: - return ByteSizeV3; - case 4: - return ByteSizeV4; - } - - return 0; - } - - public enum AssistModeSelect - { - Off = 0, - Standard = 1, - Lights = 2, - Pas0AndLights = 3, - Pas1AndLights = 4, - Pas2AndLights = 5, - Pas3AndLights = 6, - Pas4AndLights = 7, - Pas5AndLights = 8, - Pas6AndLights = 9, - Pas7AndLights = 10, - Pas8AndLights = 11, - Pas9AndLights = 12, - BrakesOnBoot = 13 - } - - [Flags] - public enum AssistFlagsType : byte - { - None = 0x00, - Pas = 0x01, - Throttle = 0x02, - Cruise = 0x04, - - PasVariable = 0x08, - PasTorque = 0x10, - CadenceOverride = 0x20, - SpeedOverride = 0x40 - }; - - public enum TemperatureSensor - { - Disabled = 0x00, - Controller = 0x01, - Motor = 0x02, - All = 0x03 - } - - public enum WalkModeData - { - Speed = 0, - Temperature = 1, - RequestedPower = 2, - BatteryPercent = 3 - } - - public class AssistLevel - { - [XmlAttribute] - public AssistFlagsType Type; - - [XmlAttribute] - public uint MaxCurrentPercent; - - [XmlAttribute] - public uint MaxThrottlePercent; - - [XmlAttribute] - public uint MaxCadencePercent; - - [XmlAttribute] - public uint MaxSpeedPercent; - - [XmlAttribute] - public float TorqueAmplificationFactor; - } - - - [XmlIgnore] - public BbsfwConnection.Controller Target { get; private set; } - - public uint MaxCurrentLimitAmps - { - get - { - switch (Target) - { - case BbsfwConnection.Controller.BBSHD: - return 33; - case BbsfwConnection.Controller.BBS02: - return 30; - case BbsfwConnection.Controller.TSDZ2: - return 20; - } - - return 50; - } - } - - // hmi - [XmlIgnore] - public bool UseFreedomUnits; - - // global - public uint MaxCurrentAmps; - public uint CurrentRampAmpsSecond; - public float MaxBatteryVolts; - public uint LowCutoffVolts; - public uint MaxSpeedKph; - - // externals - public bool UseSpeedSensor; - public bool UseShiftSensor; - public bool UsePushWalk; +using System; +using System.IO; +using System.Linq; +using System.Text; +using System.Xml; +using System.Xml.Serialization; + +namespace BBSFW.Model +{ + + [XmlRoot("BBSFW", Namespace ="https://github.com/danielnilsson9/bbs-fw")] + public class Configuration + { + public const int CurrentVersion = 4; + public const int MinVersion = 1; + public const int MaxVersion = CurrentVersion; + + public const int ByteSizeV1 = 120; + public const int ByteSizeV2 = 124; + public const int ByteSizeV3 = 149; + public const int ByteSizeV4 = 152; + + public enum Feature + { + ShiftSensor, + TorqueSensor, + ControllerTemperatureSensor, + MotorTemperatureSensor + } + + + public static int GetByteSize(int version) + { + switch (version) + { + case 1: + return ByteSizeV1; + case 2: + return ByteSizeV2; + case 3: + return ByteSizeV3; + case 4: + return ByteSizeV4; + } + + return 0; + } + + public enum AssistModeSelect + { + Off = 0, + Standard = 1, + Lights = 2, + Pas0AndLights = 3, + Pas1AndLights = 4, + Pas2AndLights = 5, + Pas3AndLights = 6, + Pas4AndLights = 7, + Pas5AndLights = 8, + Pas6AndLights = 9, + Pas7AndLights = 10, + Pas8AndLights = 11, + Pas9AndLights = 12, + BrakesOnBoot = 13 + } + + [Flags] + public enum AssistFlagsType : byte + { + None = 0x00, + Pas = 0x01, + Throttle = 0x02, + Cruise = 0x04, + + PasVariable = 0x08, + PasTorque = 0x10, + CadenceOverride = 0x20, + SpeedOverride = 0x40 + }; + + public enum ThrottleGlobalSpeedLimit + { + Disabled = 0x00, + Enabled = 0x01, + StandardLevels = 0x02 + } + + public enum TemperatureSensor + { + Disabled = 0x00, + Controller = 0x01, + Motor = 0x02, + All = 0x03 + } + + public enum WalkModeData + { + Speed = 0, + Temperature = 1, + RequestedPower = 2, + BatteryPercent = 3 + } + + + public class AssistLevel + { + [XmlAttribute] + public AssistFlagsType Type; + + [XmlAttribute] + public uint MaxCurrentPercent; + + [XmlAttribute] + public uint MaxThrottlePercent; + + [XmlAttribute] + public uint MaxCadencePercent; + + [XmlAttribute] + public uint MaxSpeedPercent; + + [XmlAttribute] + public float TorqueAmplificationFactor; + } + + + [XmlIgnore] + public BbsfwConnection.Controller Target { get; private set; } + + public uint MaxCurrentLimitAmps + { + get + { + switch (Target) + { + case BbsfwConnection.Controller.BBSHD: + return 33; + case BbsfwConnection.Controller.BBS02: + return 30; + case BbsfwConnection.Controller.TSDZ2: + return 20; + } + + return 50; + } + } + + // hmi + [XmlIgnore] + public bool UseFreedomUnits; + + // global + public uint MaxCurrentAmps; + public uint CurrentRampAmpsSecond; + public float MaxBatteryVolts; + public uint LowCutoffVolts; + public uint MaxSpeedKph; + + // externals + public bool UseSpeedSensor; + public bool UseShiftSensor; + public bool UsePushWalk; public TemperatureSensor UseTemperatureSensor; // lights public bool LightsAlwaysOn; // speed sensor - public float WheelSizeInch; - public uint NumWheelSensorSignals; - - // pas options - public uint PasStartDelayPulses; - public uint PasStopDelayMilliseconds; - public uint PasKeepCurrentPercent; - public uint PasKeepCurrentCadenceRpm; - - // throttle options - public uint ThrottleStartMillivolts; - public uint ThrottleEndMillivolts; - public uint ThrottleStartPercent; - - // shift interrupt options - public uint ShiftInterruptDuration; - public uint ShiftInterruptCurrentThresholdPercent; - - // misc - public WalkModeData WalkModeDataDisplay; - - // assists options - public AssistModeSelect AssistModeSelection; - public uint AssistStartupLevel; - - public AssistLevel[] StandardAssistLevels = new AssistLevel[10]; - public AssistLevel[] SportAssistLevels = new AssistLevel[10]; - - - public Configuration() : this(BbsfwConnection.Controller.Unknown) - { } - - public Configuration(BbsfwConnection.Controller target) - { - Target = target; - - UseFreedomUnits = Properties.Settings.Default.UseFreedomUnits; - MaxCurrentAmps = 0; - CurrentRampAmpsSecond = 0; - MaxBatteryVolts = 0; - LowCutoffVolts = 0; - - UseSpeedSensor = false; - UseShiftSensor = false; - UsePushWalk = false; - UseTemperatureSensor = TemperatureSensor.All; - - LightsAlwaysOn = false; - - WheelSizeInch = 0; - NumWheelSensorSignals = 0; - MaxSpeedKph = 0; - - PasStartDelayPulses = 0; - PasStopDelayMilliseconds = 0; - PasKeepCurrentPercent = 0; - PasKeepCurrentCadenceRpm = 0; - - ThrottleStartMillivolts = 0; - ThrottleEndMillivolts = 0; - ThrottleStartPercent = 0; - - ShiftInterruptDuration = 0; - ShiftInterruptCurrentThresholdPercent = 0; - - WalkModeDataDisplay = WalkModeData.Speed; - - AssistModeSelection = AssistModeSelect.Off; - AssistStartupLevel = 0; - - for (int i = 0; i < StandardAssistLevels.Length; ++i) - { - StandardAssistLevels[i] = new AssistLevel(); - } - - for (int i = 0; i < SportAssistLevels.Length; ++i) - { - SportAssistLevels[i] = new AssistLevel(); - } - } - - public bool IsFeatureSupported(Feature feature) - { - if (Target == BbsfwConnection.Controller.Unknown) - { - return true; - } - - switch (feature) - { - case Feature.ShiftSensor: - return new[] { BbsfwConnection.Controller.BBSHD, BbsfwConnection.Controller.BBS02 }.Contains(Target); - case Feature.TorqueSensor: - return new[] { BbsfwConnection.Controller.TSDZ2 }.Contains(Target); - case Feature.ControllerTemperatureSensor: - return new[] { BbsfwConnection.Controller.BBSHD, BbsfwConnection.Controller.BBS02 }.Contains(Target); - case Feature.MotorTemperatureSensor: - return new[] { BbsfwConnection.Controller.BBSHD }.Contains(Target); - } - - return false; - } - - public bool ParseFromBufferV1(byte[] buffer) - { - if (buffer.Length != ByteSizeV1) - { - return false; - } - - using (var s = new MemoryStream(buffer)) - { - var br = new BinaryReader(s); - - UseFreedomUnits = br.ReadBoolean(); - - MaxCurrentAmps = br.ReadByte(); - CurrentRampAmpsSecond = br.ReadByte(); - LowCutoffVolts = br.ReadByte(); - MaxSpeedKph = br.ReadByte(); - - UseSpeedSensor = br.ReadBoolean(); - /* UseDisplay = */ br.ReadBoolean(); - UsePushWalk = br.ReadBoolean(); - - WheelSizeInch = br.ReadUInt16() / 10f; - NumWheelSensorSignals = br.ReadByte(); - - PasStartDelayPulses = br.ReadByte(); - PasStopDelayMilliseconds = br.ReadByte() * 10u; - - ThrottleStartMillivolts = br.ReadUInt16(); - ThrottleEndMillivolts = br.ReadUInt16(); - ThrottleStartPercent = br.ReadByte(); - - AssistModeSelection = (AssistModeSelect)br.ReadByte(); - AssistStartupLevel = br.ReadByte(); - - for (int i = 0; i < StandardAssistLevels.Length; ++i) - { - StandardAssistLevels[i].Type = (AssistFlagsType)br.ReadByte(); - StandardAssistLevels[i].MaxCurrentPercent = br.ReadByte(); - StandardAssistLevels[i].MaxThrottlePercent = br.ReadByte(); - StandardAssistLevels[i].MaxCadencePercent = br.ReadByte(); - StandardAssistLevels[i].MaxSpeedPercent = br.ReadByte(); - } - - for (int i = 0; i < SportAssistLevels.Length; ++i) - { - SportAssistLevels[i].Type = (AssistFlagsType)br.ReadByte(); - SportAssistLevels[i].MaxCurrentPercent = br.ReadByte(); - SportAssistLevels[i].MaxThrottlePercent = br.ReadByte(); - SportAssistLevels[i].MaxCadencePercent = br.ReadByte(); - SportAssistLevels[i].MaxSpeedPercent = br.ReadByte(); - } - } - - // apply default settings for non existing options in version - MaxBatteryVolts = 0f; - UseTemperatureSensor = TemperatureSensor.All; - WalkModeDataDisplay = WalkModeData.Speed; - PasKeepCurrentPercent = 100; - PasKeepCurrentCadenceRpm = 255; - UseShiftSensor = true; - ShiftInterruptDuration = 600; - ShiftInterruptCurrentThresholdPercent = 10; + public float WheelSizeInch; + public uint NumWheelSensorSignals; + + // pas options + public uint PasStartDelayPulses; + public uint PasStopDelayMilliseconds; + public uint PasKeepCurrentPercent; + public uint PasKeepCurrentCadenceRpm; + + // throttle options + public uint ThrottleStartMillivolts; + public uint ThrottleEndMillivolts; + public uint ThrottleStartPercent; + public ThrottleGlobalSpeedLimit ThrottleGlobalSpeedLimitOpt; + public uint ThrottleGlobalSpeedLimitPercent; + + // shift interrupt options + public uint ShiftInterruptDuration; + public uint ShiftInterruptCurrentThresholdPercent; + + // misc + public WalkModeData WalkModeDataDisplay; + + // assists options + public AssistModeSelect AssistModeSelection; + public uint AssistStartupLevel; + + public AssistLevel[] StandardAssistLevels = new AssistLevel[10]; + public AssistLevel[] SportAssistLevels = new AssistLevel[10]; + + + public Configuration() : this(BbsfwConnection.Controller.Unknown) + { } + + public Configuration(BbsfwConnection.Controller target) + { + Target = target; + + UseFreedomUnits = Properties.Settings.Default.UseFreedomUnits; + MaxCurrentAmps = 0; + CurrentRampAmpsSecond = 0; + MaxBatteryVolts = 0; + LowCutoffVolts = 0; + + UseSpeedSensor = false; + UseShiftSensor = false; + UsePushWalk = false; + UseTemperatureSensor = TemperatureSensor.All; + LightsAlwaysOn = false; - - return true; - } - - public bool ParseFromBufferV2(byte[] buffer) - { - if (buffer.Length != ByteSizeV2) - { - return false; - } - - using (var s = new MemoryStream(buffer)) - { - var br = new BinaryReader(s); - - UseFreedomUnits = br.ReadBoolean(); - - MaxCurrentAmps = br.ReadByte(); - CurrentRampAmpsSecond = br.ReadByte(); - MaxBatteryVolts = br.ReadUInt16() / 100f; - LowCutoffVolts = br.ReadByte(); - MaxSpeedKph = br.ReadByte(); - - UseSpeedSensor = br.ReadBoolean(); - /* UseDisplay = */ br.ReadBoolean(); - UsePushWalk = br.ReadBoolean(); - UseTemperatureSensor = (TemperatureSensor)br.ReadByte(); - - WheelSizeInch = br.ReadUInt16() / 10f; - NumWheelSensorSignals = br.ReadByte(); - - PasStartDelayPulses = br.ReadByte(); - PasStopDelayMilliseconds = br.ReadByte() * 10u; - PasKeepCurrentCadenceRpm = 255; - PasKeepCurrentPercent = 100; - - ThrottleStartMillivolts = br.ReadUInt16(); - ThrottleEndMillivolts = br.ReadUInt16(); - ThrottleStartPercent = br.ReadByte(); - - WalkModeDataDisplay = (WalkModeData)br.ReadByte(); - - AssistModeSelection = (AssistModeSelect)br.ReadByte(); - AssistStartupLevel = br.ReadByte(); - - for (int i = 0; i < StandardAssistLevels.Length; ++i) - { - StandardAssistLevels[i].Type = (AssistFlagsType)br.ReadByte(); - StandardAssistLevels[i].MaxCurrentPercent = br.ReadByte(); - StandardAssistLevels[i].MaxThrottlePercent = br.ReadByte(); - StandardAssistLevels[i].MaxCadencePercent = br.ReadByte(); - StandardAssistLevels[i].MaxSpeedPercent = br.ReadByte(); - } - - for (int i = 0; i < SportAssistLevels.Length; ++i) - { - SportAssistLevels[i].Type = (AssistFlagsType)br.ReadByte(); - SportAssistLevels[i].MaxCurrentPercent = br.ReadByte(); - SportAssistLevels[i].MaxThrottlePercent = br.ReadByte(); - SportAssistLevels[i].MaxCadencePercent = br.ReadByte(); - SportAssistLevels[i].MaxSpeedPercent = br.ReadByte(); - } - } - - // apply same default settings for non existing options in version - PasKeepCurrentPercent = 100; - PasKeepCurrentCadenceRpm = 255; - UseShiftSensor = true; - ShiftInterruptDuration = 600; - ShiftInterruptCurrentThresholdPercent = 10; + + WheelSizeInch = 0; + NumWheelSensorSignals = 0; + MaxSpeedKph = 0; + + PasStartDelayPulses = 0; + PasStopDelayMilliseconds = 0; + PasKeepCurrentPercent = 0; + PasKeepCurrentCadenceRpm = 0; + + ThrottleStartMillivolts = 0; + ThrottleEndMillivolts = 0; + ThrottleStartPercent = 0; + ThrottleGlobalSpeedLimitOpt = ThrottleGlobalSpeedLimit.Disabled; + ThrottleGlobalSpeedLimitPercent = 0; + + ShiftInterruptDuration = 0; + ShiftInterruptCurrentThresholdPercent = 0; + + WalkModeDataDisplay = WalkModeData.Speed; + + AssistModeSelection = AssistModeSelect.Off; + AssistStartupLevel = 0; + + for (int i = 0; i < StandardAssistLevels.Length; ++i) + { + StandardAssistLevels[i] = new AssistLevel(); + } + + for (int i = 0; i < SportAssistLevels.Length; ++i) + { + SportAssistLevels[i] = new AssistLevel(); + } + } + + public bool IsFeatureSupported(Feature feature) + { + if (Target == BbsfwConnection.Controller.Unknown) + { + return true; + } + + switch (feature) + { + case Feature.ShiftSensor: + return new[] { BbsfwConnection.Controller.BBSHD, BbsfwConnection.Controller.BBS02 }.Contains(Target); + case Feature.TorqueSensor: + return new[] { BbsfwConnection.Controller.TSDZ2 }.Contains(Target); + case Feature.ControllerTemperatureSensor: + return new[] { BbsfwConnection.Controller.BBSHD, BbsfwConnection.Controller.BBS02 }.Contains(Target); + case Feature.MotorTemperatureSensor: + return new[] { BbsfwConnection.Controller.BBSHD }.Contains(Target); + } + + return false; + } + + public bool ParseFromBufferV1(byte[] buffer) + { + if (buffer.Length != ByteSizeV1) + { + return false; + } + + using (var s = new MemoryStream(buffer)) + { + var br = new BinaryReader(s); + + UseFreedomUnits = br.ReadBoolean(); + + MaxCurrentAmps = br.ReadByte(); + CurrentRampAmpsSecond = br.ReadByte(); + LowCutoffVolts = br.ReadByte(); + MaxSpeedKph = br.ReadByte(); + + UseSpeedSensor = br.ReadBoolean(); + /* UseDisplay = */ br.ReadBoolean(); + UsePushWalk = br.ReadBoolean(); + + WheelSizeInch = br.ReadUInt16() / 10f; + NumWheelSensorSignals = br.ReadByte(); + + PasStartDelayPulses = br.ReadByte(); + PasStopDelayMilliseconds = br.ReadByte() * 10u; + + ThrottleStartMillivolts = br.ReadUInt16(); + ThrottleEndMillivolts = br.ReadUInt16(); + ThrottleStartPercent = br.ReadByte(); + + AssistModeSelection = (AssistModeSelect)br.ReadByte(); + AssistStartupLevel = br.ReadByte(); + + for (int i = 0; i < StandardAssistLevels.Length; ++i) + { + StandardAssistLevels[i].Type = (AssistFlagsType)br.ReadByte(); + StandardAssistLevels[i].MaxCurrentPercent = br.ReadByte(); + StandardAssistLevels[i].MaxThrottlePercent = br.ReadByte(); + StandardAssistLevels[i].MaxCadencePercent = br.ReadByte(); + StandardAssistLevels[i].MaxSpeedPercent = br.ReadByte(); + } + + for (int i = 0; i < SportAssistLevels.Length; ++i) + { + SportAssistLevels[i].Type = (AssistFlagsType)br.ReadByte(); + SportAssistLevels[i].MaxCurrentPercent = br.ReadByte(); + SportAssistLevels[i].MaxThrottlePercent = br.ReadByte(); + SportAssistLevels[i].MaxCadencePercent = br.ReadByte(); + SportAssistLevels[i].MaxSpeedPercent = br.ReadByte(); + } + } + + // apply default settings for non existing options in version + MaxBatteryVolts = 0f; + UseTemperatureSensor = TemperatureSensor.All; + WalkModeDataDisplay = WalkModeData.Speed; + PasKeepCurrentPercent = 100; + PasKeepCurrentCadenceRpm = 255; + UseShiftSensor = true; + ShiftInterruptDuration = 600; + ShiftInterruptCurrentThresholdPercent = 10; + LightsAlwaysOn = false; + ThrottleGlobalSpeedLimitOpt = ThrottleGlobalSpeedLimit.Disabled; + ThrottleGlobalSpeedLimitPercent = 100; + + return true; + } + + public bool ParseFromBufferV2(byte[] buffer) + { + if (buffer.Length != ByteSizeV2) + { + return false; + } + + using (var s = new MemoryStream(buffer)) + { + var br = new BinaryReader(s); + + UseFreedomUnits = br.ReadBoolean(); + + MaxCurrentAmps = br.ReadByte(); + CurrentRampAmpsSecond = br.ReadByte(); + MaxBatteryVolts = br.ReadUInt16() / 100f; + LowCutoffVolts = br.ReadByte(); + MaxSpeedKph = br.ReadByte(); + + UseSpeedSensor = br.ReadBoolean(); + /* UseDisplay = */ br.ReadBoolean(); + UsePushWalk = br.ReadBoolean(); + UseTemperatureSensor = (TemperatureSensor)br.ReadByte(); + + WheelSizeInch = br.ReadUInt16() / 10f; + NumWheelSensorSignals = br.ReadByte(); + + PasStartDelayPulses = br.ReadByte(); + PasStopDelayMilliseconds = br.ReadByte() * 10u; + PasKeepCurrentCadenceRpm = 255; + PasKeepCurrentPercent = 100; + + ThrottleStartMillivolts = br.ReadUInt16(); + ThrottleEndMillivolts = br.ReadUInt16(); + ThrottleStartPercent = br.ReadByte(); + + WalkModeDataDisplay = (WalkModeData)br.ReadByte(); + + AssistModeSelection = (AssistModeSelect)br.ReadByte(); + AssistStartupLevel = br.ReadByte(); + + for (int i = 0; i < StandardAssistLevels.Length; ++i) + { + StandardAssistLevels[i].Type = (AssistFlagsType)br.ReadByte(); + StandardAssistLevels[i].MaxCurrentPercent = br.ReadByte(); + StandardAssistLevels[i].MaxThrottlePercent = br.ReadByte(); + StandardAssistLevels[i].MaxCadencePercent = br.ReadByte(); + StandardAssistLevels[i].MaxSpeedPercent = br.ReadByte(); + } + + for (int i = 0; i < SportAssistLevels.Length; ++i) + { + SportAssistLevels[i].Type = (AssistFlagsType)br.ReadByte(); + SportAssistLevels[i].MaxCurrentPercent = br.ReadByte(); + SportAssistLevels[i].MaxThrottlePercent = br.ReadByte(); + SportAssistLevels[i].MaxCadencePercent = br.ReadByte(); + SportAssistLevels[i].MaxSpeedPercent = br.ReadByte(); + } + } + + // apply default settings for non existing options in version + PasKeepCurrentPercent = 100; + PasKeepCurrentCadenceRpm = 255; + UseShiftSensor = true; + ShiftInterruptDuration = 600; + ShiftInterruptCurrentThresholdPercent = 10; LightsAlwaysOn = false; - - return true; - } - - public bool ParseFromBufferV3(byte[] buffer) - { - if (buffer.Length != ByteSizeV3) - { - return false; - } - - using (var s = new MemoryStream(buffer)) - { - var br = new BinaryReader(s); - - UseFreedomUnits = br.ReadBoolean(); - - MaxCurrentAmps = br.ReadByte(); - CurrentRampAmpsSecond = br.ReadByte(); - MaxBatteryVolts = br.ReadUInt16() / 100f; - LowCutoffVolts = br.ReadByte(); - MaxSpeedKph = br.ReadByte(); - - UseSpeedSensor = br.ReadBoolean(); - UseShiftSensor = br.ReadBoolean(); - UsePushWalk = br.ReadBoolean(); - UseTemperatureSensor = (TemperatureSensor)br.ReadByte(); - - WheelSizeInch = br.ReadUInt16() / 10f; - NumWheelSensorSignals = br.ReadByte(); - - PasStartDelayPulses = br.ReadByte(); - PasStopDelayMilliseconds = br.ReadByte() * 10u; - PasKeepCurrentPercent = br.ReadByte(); - PasKeepCurrentCadenceRpm = br.ReadByte(); - - ThrottleStartMillivolts = br.ReadUInt16(); - ThrottleEndMillivolts = br.ReadUInt16(); - ThrottleStartPercent = br.ReadByte(); - - ShiftInterruptDuration = br.ReadUInt16(); - ShiftInterruptCurrentThresholdPercent = br.ReadByte(); - - WalkModeDataDisplay = (WalkModeData)br.ReadByte(); - - AssistModeSelection = (AssistModeSelect)br.ReadByte(); - AssistStartupLevel = br.ReadByte(); - - for (int i = 0; i < StandardAssistLevels.Length; ++i) - { - StandardAssistLevels[i].Type = (AssistFlagsType)br.ReadByte(); - StandardAssistLevels[i].MaxCurrentPercent = br.ReadByte(); - StandardAssistLevels[i].MaxThrottlePercent = br.ReadByte(); - StandardAssistLevels[i].MaxCadencePercent = br.ReadByte(); - StandardAssistLevels[i].MaxSpeedPercent = br.ReadByte(); - StandardAssistLevels[i].TorqueAmplificationFactor = br.ReadByte() / 10f; - } - - for (int i = 0; i < SportAssistLevels.Length; ++i) - { - SportAssistLevels[i].Type = (AssistFlagsType)br.ReadByte(); - SportAssistLevels[i].MaxCurrentPercent = br.ReadByte(); - SportAssistLevels[i].MaxThrottlePercent = br.ReadByte(); - SportAssistLevels[i].MaxCadencePercent = br.ReadByte(); - SportAssistLevels[i].MaxSpeedPercent = br.ReadByte(); - SportAssistLevels[i].TorqueAmplificationFactor = br.ReadByte() / 10f; - } + ThrottleGlobalSpeedLimitOpt = ThrottleGlobalSpeedLimit.Disabled; + ThrottleGlobalSpeedLimitPercent = 100; + + return true; + } + + public bool ParseFromBufferV3(byte[] buffer) + { + if (buffer.Length != ByteSizeV3) + { + return false; + } + + using (var s = new MemoryStream(buffer)) + { + var br = new BinaryReader(s); + + UseFreedomUnits = br.ReadBoolean(); + + MaxCurrentAmps = br.ReadByte(); + CurrentRampAmpsSecond = br.ReadByte(); + MaxBatteryVolts = br.ReadUInt16() / 100f; + LowCutoffVolts = br.ReadByte(); + MaxSpeedKph = br.ReadByte(); + + UseSpeedSensor = br.ReadBoolean(); + UseShiftSensor = br.ReadBoolean(); + UsePushWalk = br.ReadBoolean(); + UseTemperatureSensor = (TemperatureSensor)br.ReadByte(); + + WheelSizeInch = br.ReadUInt16() / 10f; + NumWheelSensorSignals = br.ReadByte(); + + PasStartDelayPulses = br.ReadByte(); + PasStopDelayMilliseconds = br.ReadByte() * 10u; + PasKeepCurrentPercent = br.ReadByte(); + PasKeepCurrentCadenceRpm = br.ReadByte(); + + ThrottleStartMillivolts = br.ReadUInt16(); + ThrottleEndMillivolts = br.ReadUInt16(); + ThrottleStartPercent = br.ReadByte(); + + ShiftInterruptDuration = br.ReadUInt16(); + ShiftInterruptCurrentThresholdPercent = br.ReadByte(); + + WalkModeDataDisplay = (WalkModeData)br.ReadByte(); + + AssistModeSelection = (AssistModeSelect)br.ReadByte(); + AssistStartupLevel = br.ReadByte(); + + for (int i = 0; i < StandardAssistLevels.Length; ++i) + { + StandardAssistLevels[i].Type = (AssistFlagsType)br.ReadByte(); + StandardAssistLevels[i].MaxCurrentPercent = br.ReadByte(); + StandardAssistLevels[i].MaxThrottlePercent = br.ReadByte(); + StandardAssistLevels[i].MaxCadencePercent = br.ReadByte(); + StandardAssistLevels[i].MaxSpeedPercent = br.ReadByte(); + StandardAssistLevels[i].TorqueAmplificationFactor = br.ReadByte() / 10f; + } + + for (int i = 0; i < SportAssistLevels.Length; ++i) + { + SportAssistLevels[i].Type = (AssistFlagsType)br.ReadByte(); + SportAssistLevels[i].MaxCurrentPercent = br.ReadByte(); + SportAssistLevels[i].MaxThrottlePercent = br.ReadByte(); + SportAssistLevels[i].MaxCadencePercent = br.ReadByte(); + SportAssistLevels[i].MaxSpeedPercent = br.ReadByte(); + SportAssistLevels[i].TorqueAmplificationFactor = br.ReadByte() / 10f; + } } - // apply same default settings for non existing options in version + // apply default settings for non existing options in version LightsAlwaysOn = false; + ThrottleGlobalSpeedLimitOpt = ThrottleGlobalSpeedLimit.Disabled; + ThrottleGlobalSpeedLimitPercent = 100; + + return true; + } + + public bool ParseFromBufferV4(byte[] buffer) + { + if (buffer.Length != ByteSizeV4) + { + return false; + } + + using (var s = new MemoryStream(buffer)) + { + var br = new BinaryReader(s); + + UseFreedomUnits = br.ReadBoolean(); + + MaxCurrentAmps = br.ReadByte(); + CurrentRampAmpsSecond = br.ReadByte(); + MaxBatteryVolts = br.ReadUInt16() / 100f; + LowCutoffVolts = br.ReadByte(); + MaxSpeedKph = br.ReadByte(); + + UseSpeedSensor = br.ReadBoolean(); + UseShiftSensor = br.ReadBoolean(); + UsePushWalk = br.ReadBoolean(); + UseTemperatureSensor = (TemperatureSensor)br.ReadByte(); + + LightsAlwaysOn = br.ReadBoolean(); + + WheelSizeInch = br.ReadUInt16() / 10f; + NumWheelSensorSignals = br.ReadByte(); + + PasStartDelayPulses = br.ReadByte(); + PasStopDelayMilliseconds = br.ReadByte() * 10u; + PasKeepCurrentPercent = br.ReadByte(); + PasKeepCurrentCadenceRpm = br.ReadByte(); + + ThrottleStartMillivolts = br.ReadUInt16(); + ThrottleEndMillivolts = br.ReadUInt16(); + ThrottleStartPercent = br.ReadByte(); + ThrottleGlobalSpeedLimitOpt = (ThrottleGlobalSpeedLimit)br.ReadByte(); + ThrottleGlobalSpeedLimitPercent = br.ReadByte(); + + ShiftInterruptDuration = br.ReadUInt16(); + ShiftInterruptCurrentThresholdPercent = br.ReadByte(); + + WalkModeDataDisplay = (WalkModeData)br.ReadByte(); + + AssistModeSelection = (AssistModeSelect)br.ReadByte(); + AssistStartupLevel = br.ReadByte(); + + for (int i = 0; i < StandardAssistLevels.Length; ++i) + { + StandardAssistLevels[i].Type = (AssistFlagsType)br.ReadByte(); + StandardAssistLevels[i].MaxCurrentPercent = br.ReadByte(); + StandardAssistLevels[i].MaxThrottlePercent = br.ReadByte(); + StandardAssistLevels[i].MaxCadencePercent = br.ReadByte(); + StandardAssistLevels[i].MaxSpeedPercent = br.ReadByte(); + StandardAssistLevels[i].TorqueAmplificationFactor = br.ReadByte() / 10f; + } + + for (int i = 0; i < SportAssistLevels.Length; ++i) + { + SportAssistLevels[i].Type = (AssistFlagsType)br.ReadByte(); + SportAssistLevels[i].MaxCurrentPercent = br.ReadByte(); + SportAssistLevels[i].MaxThrottlePercent = br.ReadByte(); + SportAssistLevels[i].MaxCadencePercent = br.ReadByte(); + SportAssistLevels[i].MaxSpeedPercent = br.ReadByte(); + SportAssistLevels[i].TorqueAmplificationFactor = br.ReadByte() / 10f; + } + } + + return true; + } + + + public byte[] WriteToBuffer() + { + using (var s = new MemoryStream()) + { + var bw = new BinaryWriter(s); + + bw.Write(UseFreedomUnits); + + bw.Write((byte)MaxCurrentAmps); + bw.Write((byte)CurrentRampAmpsSecond); + bw.Write((UInt16)(MaxBatteryVolts * 100)); + bw.Write((byte)LowCutoffVolts); + bw.Write((byte)MaxSpeedKph); + + bw.Write(UseSpeedSensor); + bw.Write(UseShiftSensor); + bw.Write(UsePushWalk); + bw.Write((byte)UseTemperatureSensor); + + bw.Write(LightsAlwaysOn); + + bw.Write((UInt16)(WheelSizeInch * 10)); + bw.Write((byte)NumWheelSensorSignals); + + bw.Write((byte)PasStartDelayPulses); + bw.Write((byte)(PasStopDelayMilliseconds / 10u)); + bw.Write((byte)PasKeepCurrentPercent); + bw.Write((byte)PasKeepCurrentCadenceRpm); + + bw.Write((UInt16)ThrottleStartMillivolts); + bw.Write((UInt16)ThrottleEndMillivolts); + bw.Write((byte)ThrottleStartPercent); + bw.Write((byte)ThrottleGlobalSpeedLimitOpt); + bw.Write((byte)ThrottleGlobalSpeedLimitPercent); + + bw.Write((UInt16)ShiftInterruptDuration); + bw.Write((byte)ShiftInterruptCurrentThresholdPercent); + + bw.Write((byte)WalkModeDataDisplay); + + bw.Write((byte)AssistModeSelection); + bw.Write((byte)AssistStartupLevel); + + for (int i = 0; i < StandardAssistLevels.Length; ++i) + { + bw.Write((byte)StandardAssistLevels[i].Type); + bw.Write((byte)StandardAssistLevels[i].MaxCurrentPercent); + bw.Write((byte)StandardAssistLevels[i].MaxThrottlePercent); + bw.Write((byte)StandardAssistLevels[i].MaxCadencePercent); + bw.Write((byte)StandardAssistLevels[i].MaxSpeedPercent); + bw.Write((byte)Math.Round(StandardAssistLevels[i].TorqueAmplificationFactor * 10)); + } + + for (int i = 0; i < SportAssistLevels.Length; ++i) + { + bw.Write((byte)SportAssistLevels[i].Type); + bw.Write((byte)SportAssistLevels[i].MaxCurrentPercent); + bw.Write((byte)SportAssistLevels[i].MaxThrottlePercent); + bw.Write((byte)SportAssistLevels[i].MaxCadencePercent); + bw.Write((byte)SportAssistLevels[i].MaxSpeedPercent); + bw.Write((byte)Math.Round(SportAssistLevels[i].TorqueAmplificationFactor * 10)); + } + + return s.ToArray(); + } + } + + public void CopyFrom(Configuration cfg) + { + Target = cfg.Target; + + UseFreedomUnits = cfg.UseFreedomUnits; + MaxCurrentAmps = cfg.MaxCurrentAmps; + CurrentRampAmpsSecond = cfg.CurrentRampAmpsSecond; + MaxBatteryVolts = cfg.MaxBatteryVolts; + LowCutoffVolts = cfg.LowCutoffVolts; + UseSpeedSensor = cfg.UseSpeedSensor; + UseShiftSensor = cfg.UseShiftSensor; + UsePushWalk = cfg.UsePushWalk; + UseTemperatureSensor = cfg.UseTemperatureSensor; + LightsAlwaysOn = cfg.LightsAlwaysOn; + WheelSizeInch = cfg.WheelSizeInch; + NumWheelSensorSignals = cfg.NumWheelSensorSignals; + MaxSpeedKph = cfg.MaxSpeedKph; + PasStartDelayPulses = cfg.PasStartDelayPulses; + PasStopDelayMilliseconds = cfg.PasStopDelayMilliseconds; + PasKeepCurrentPercent = cfg.PasKeepCurrentPercent; + PasKeepCurrentCadenceRpm = cfg.PasKeepCurrentCadenceRpm; + ThrottleStartMillivolts = cfg.ThrottleStartMillivolts; + ThrottleEndMillivolts = cfg.ThrottleEndMillivolts; + ThrottleStartPercent = cfg.ThrottleStartPercent; + ThrottleGlobalSpeedLimitOpt = cfg.ThrottleGlobalSpeedLimitOpt; + ThrottleGlobalSpeedLimitPercent = cfg.ThrottleGlobalSpeedLimitPercent; + ShiftInterruptDuration = cfg.ShiftInterruptDuration; + ShiftInterruptCurrentThresholdPercent = cfg.ShiftInterruptCurrentThresholdPercent; + WalkModeDataDisplay = cfg.WalkModeDataDisplay; + AssistModeSelection = cfg.AssistModeSelection; + AssistStartupLevel = cfg.AssistStartupLevel; + + for (int i = 0; i < Math.Min(cfg.StandardAssistLevels.Length, StandardAssistLevels.Length); ++i) + { + StandardAssistLevels[i].Type = cfg.StandardAssistLevels[i].Type; + StandardAssistLevels[i].MaxCurrentPercent = cfg.StandardAssistLevels[i].MaxCurrentPercent; + StandardAssistLevels[i].MaxThrottlePercent = cfg.StandardAssistLevels[i].MaxThrottlePercent; + StandardAssistLevels[i].MaxCadencePercent = cfg.StandardAssistLevels[i].MaxCadencePercent; + StandardAssistLevels[i].MaxSpeedPercent = cfg.StandardAssistLevels[i].MaxSpeedPercent; + StandardAssistLevels[i].TorqueAmplificationFactor = cfg.StandardAssistLevels[i].TorqueAmplificationFactor; + } + + for (int i = 0; i < Math.Min(cfg.SportAssistLevels.Length, SportAssistLevels.Length); ++i) + { + SportAssistLevels[i].Type = cfg.SportAssistLevels[i].Type; + SportAssistLevels[i].MaxCurrentPercent = cfg.SportAssistLevels[i].MaxCurrentPercent; + SportAssistLevels[i].MaxThrottlePercent = cfg.SportAssistLevels[i].MaxThrottlePercent; + SportAssistLevels[i].MaxCadencePercent = cfg.SportAssistLevels[i].MaxCadencePercent; + SportAssistLevels[i].MaxSpeedPercent = cfg.SportAssistLevels[i].MaxSpeedPercent; + SportAssistLevels[i].TorqueAmplificationFactor = cfg.SportAssistLevels[i].TorqueAmplificationFactor; + } + } + + public void ReadFromFile(string filepath) + { + var serializer = new XmlSerializer(typeof(Configuration)); + + using (var reader = new FileStream(filepath, FileMode.Open)) + { + var obj = serializer.Deserialize(reader) as Configuration; + CopyFrom(obj); + } + } + + public void WriteToFile(string filepath) + { + var serializer = new XmlSerializer(typeof(Configuration)); + var settings = new XmlWriterSettings { Encoding = Encoding.UTF8, Indent = true }; + using (var xmlWriter = XmlWriter.Create(new StreamWriter(filepath), settings)) + { + serializer.Serialize(xmlWriter, this); + } + } + + public void Validate() + { + ValidateLimits(MaxCurrentAmps, 5, MaxCurrentLimitAmps, "Max Current (A)"); + ValidateLimits(CurrentRampAmpsSecond, 1, 255, "Current Ramp (A/s)"); + ValidateLimits((uint)MaxBatteryVolts, 1, 100, "Max Battery Voltage (V)"); + ValidateLimits(LowCutoffVolts, 1, 100, "Low Voltage Cut Off (V)"); + + ValidateLimits((uint)WheelSizeInch, 10, 40, "Wheel Size (inch)"); + ValidateLimits(NumWheelSensorSignals, 1, 10, "Wheel Sensor Signals"); + ValidateLimits(MaxSpeedKph, 0, 100, "Max Speed (km/h)"); + + ValidateLimits(PasStartDelayPulses, 0, 24, "Pas Delay (pulses)"); + ValidateLimits(PasStopDelayMilliseconds, 50, 1000, "Pas Stop Delay (ms)"); + ValidateLimits(PasKeepCurrentPercent, 10, 100, "Pas Keep Current (%)"); + ValidateLimits(PasKeepCurrentCadenceRpm, 0, 255, "Pas Keep Current Cadence (rpm)"); + + ValidateLimits(ThrottleStartMillivolts, 200, 2500, "Throttle Start (mV)"); + ValidateLimits(ThrottleEndMillivolts, 2500, 5000, "Throttle End (mV)"); + ValidateLimits(ThrottleStartPercent, 0, 100, "Throttle Start (%)"); + ValidateLimits(ThrottleGlobalSpeedLimitPercent, 0, 100, "Throttle Global Speed Limit (%)"); + + ValidateLimits(ShiftInterruptDuration, 50, 2000, "Shift Interrupt Duration (ms)"); + ValidateLimits(ShiftInterruptCurrentThresholdPercent, 0, 100, "Shift Interrupt Current Threshold (%)"); + + ValidateLimits(AssistStartupLevel, 0, 9, "Assist Startup Level"); + + for (int i = 0; i < StandardAssistLevels.Length; ++i) + { + ValidateLimits(StandardAssistLevels[i].MaxCurrentPercent, 0, 100, $"Standard (Level {i}): Target Power (%)"); + ValidateLimits(StandardAssistLevels[i].MaxThrottlePercent, 0, 100, $"Standard (Level {i}): Max Throttle (%)"); + ValidateLimits(StandardAssistLevels[i].MaxCadencePercent, 0, 100, $"Standard (Level {i}): Max Cadence (%)"); + ValidateLimits(StandardAssistLevels[i].MaxSpeedPercent, 0, 100, $"Standard (Level {i}): Max Speed (%)"); + ValidateLimits((uint)StandardAssistLevels[i].TorqueAmplificationFactor, 0, 25, $"Standard (Level {i}): Torque Amplification"); + } + + for (int i = 0; i < SportAssistLevels.Length; ++i) + { + ValidateLimits(SportAssistLevels[i].MaxCurrentPercent, 0, 100, $"Sport (Level {i}): Target Power (%)"); + ValidateLimits(SportAssistLevels[i].MaxThrottlePercent, 0, 100, $"Sport (Level {i}): Max Throttle (%)"); + ValidateLimits(SportAssistLevels[i].MaxCadencePercent, 0, 100, $"Sport (Level {i}): Max Cadence (%)"); + ValidateLimits(SportAssistLevels[i].MaxSpeedPercent, 0, 100, $"Sport (Level {i}): Max Speed (%)"); + ValidateLimits((uint)SportAssistLevels[i].TorqueAmplificationFactor, 0, 25, $"Sport (Level {i}): Torque Amplification"); + } + } + + + private void ValidateLimits(uint value, uint min, uint max, string name) + { + if (value < min || value > max) + { + throw new Exception(name + " must be in interval " + min + "-" + max + "."); + } + } - return true; - } - - public bool ParseFromBufferV4(byte[] buffer) - { - if (buffer.Length != ByteSizeV4) - { - return false; - } - - using (var s = new MemoryStream(buffer)) - { - var br = new BinaryReader(s); - - UseFreedomUnits = br.ReadBoolean(); - - MaxCurrentAmps = br.ReadByte(); - CurrentRampAmpsSecond = br.ReadByte(); - MaxBatteryVolts = br.ReadUInt16() / 100f; - LowCutoffVolts = br.ReadByte(); - MaxSpeedKph = br.ReadByte(); - - UseSpeedSensor = br.ReadBoolean(); - UseShiftSensor = br.ReadBoolean(); - UsePushWalk = br.ReadBoolean(); - UseTemperatureSensor = (TemperatureSensor)br.ReadByte(); - - LightsAlwaysOn = br.ReadBoolean(); - - WheelSizeInch = br.ReadUInt16() / 10f; - NumWheelSensorSignals = br.ReadByte(); - - PasStartDelayPulses = br.ReadByte(); - PasStopDelayMilliseconds = br.ReadByte() * 10u; - PasKeepCurrentPercent = br.ReadByte(); - PasKeepCurrentCadenceRpm = br.ReadByte(); - - ThrottleStartMillivolts = br.ReadUInt16(); - ThrottleEndMillivolts = br.ReadUInt16(); - ThrottleStartPercent = br.ReadByte(); - - ShiftInterruptDuration = br.ReadUInt16(); - ShiftInterruptCurrentThresholdPercent = br.ReadByte(); - - WalkModeDataDisplay = (WalkModeData)br.ReadByte(); - - AssistModeSelection = (AssistModeSelect)br.ReadByte(); - AssistStartupLevel = br.ReadByte(); - - for (int i = 0; i < StandardAssistLevels.Length; ++i) - { - StandardAssistLevels[i].Type = (AssistFlagsType)br.ReadByte(); - StandardAssistLevels[i].MaxCurrentPercent = br.ReadByte(); - StandardAssistLevels[i].MaxThrottlePercent = br.ReadByte(); - StandardAssistLevels[i].MaxCadencePercent = br.ReadByte(); - StandardAssistLevels[i].MaxSpeedPercent = br.ReadByte(); - StandardAssistLevels[i].TorqueAmplificationFactor = br.ReadByte() / 10f; - } - - for (int i = 0; i < SportAssistLevels.Length; ++i) - { - SportAssistLevels[i].Type = (AssistFlagsType)br.ReadByte(); - SportAssistLevels[i].MaxCurrentPercent = br.ReadByte(); - SportAssistLevels[i].MaxThrottlePercent = br.ReadByte(); - SportAssistLevels[i].MaxCadencePercent = br.ReadByte(); - SportAssistLevels[i].MaxSpeedPercent = br.ReadByte(); - SportAssistLevels[i].TorqueAmplificationFactor = br.ReadByte() / 10f; - } - } - - return true; - } - - public byte[] WriteToBuffer() - { - using (var s = new MemoryStream()) - { - var bw = new BinaryWriter(s); - - bw.Write(UseFreedomUnits); - - bw.Write((byte)MaxCurrentAmps); - bw.Write((byte)CurrentRampAmpsSecond); - bw.Write((UInt16)(MaxBatteryVolts * 100)); - bw.Write((byte)LowCutoffVolts); - bw.Write((byte)MaxSpeedKph); - - bw.Write(UseSpeedSensor); - bw.Write(UseShiftSensor); - bw.Write(UsePushWalk); - bw.Write((byte)UseTemperatureSensor); - - bw.Write(LightsAlwaysOn); - - bw.Write((UInt16)(WheelSizeInch * 10)); - bw.Write((byte)NumWheelSensorSignals); - - bw.Write((byte)PasStartDelayPulses); - bw.Write((byte)(PasStopDelayMilliseconds / 10u)); - bw.Write((byte)PasKeepCurrentPercent); - bw.Write((byte)PasKeepCurrentCadenceRpm); - - bw.Write((UInt16)ThrottleStartMillivolts); - bw.Write((UInt16)ThrottleEndMillivolts); - bw.Write((byte)ThrottleStartPercent); - - bw.Write((UInt16)ShiftInterruptDuration); - bw.Write((byte)ShiftInterruptCurrentThresholdPercent); - - bw.Write((byte)WalkModeDataDisplay); - - bw.Write((byte)AssistModeSelection); - bw.Write((byte)AssistStartupLevel); - - for (int i = 0; i < StandardAssistLevels.Length; ++i) - { - bw.Write((byte)StandardAssistLevels[i].Type); - bw.Write((byte)StandardAssistLevels[i].MaxCurrentPercent); - bw.Write((byte)StandardAssistLevels[i].MaxThrottlePercent); - bw.Write((byte)StandardAssistLevels[i].MaxCadencePercent); - bw.Write((byte)StandardAssistLevels[i].MaxSpeedPercent); - bw.Write((byte)Math.Round(StandardAssistLevels[i].TorqueAmplificationFactor * 10)); - } - - for (int i = 0; i < SportAssistLevels.Length; ++i) - { - bw.Write((byte)SportAssistLevels[i].Type); - bw.Write((byte)SportAssistLevels[i].MaxCurrentPercent); - bw.Write((byte)SportAssistLevels[i].MaxThrottlePercent); - bw.Write((byte)SportAssistLevels[i].MaxCadencePercent); - bw.Write((byte)SportAssistLevels[i].MaxSpeedPercent); - bw.Write((byte)Math.Round(SportAssistLevels[i].TorqueAmplificationFactor * 10)); - } - - return s.ToArray(); - } - } - - public void CopyFrom(Configuration cfg) - { - Target = cfg.Target; - - UseFreedomUnits = cfg.UseFreedomUnits; - MaxCurrentAmps = cfg.MaxCurrentAmps; - CurrentRampAmpsSecond = cfg.CurrentRampAmpsSecond; - MaxBatteryVolts = cfg.MaxBatteryVolts; - LowCutoffVolts = cfg.LowCutoffVolts; - UseSpeedSensor = cfg.UseSpeedSensor; - UseShiftSensor = cfg.UseShiftSensor; - UsePushWalk = cfg.UsePushWalk; - UseTemperatureSensor = cfg.UseTemperatureSensor; - LightsAlwaysOn = cfg.LightsAlwaysOn; - WheelSizeInch = cfg.WheelSizeInch; - NumWheelSensorSignals = cfg.NumWheelSensorSignals; - MaxSpeedKph = cfg.MaxSpeedKph; - PasStartDelayPulses = cfg.PasStartDelayPulses; - PasStopDelayMilliseconds = cfg.PasStopDelayMilliseconds; - PasKeepCurrentPercent = cfg.PasKeepCurrentPercent; - PasKeepCurrentCadenceRpm = cfg.PasKeepCurrentCadenceRpm; - ThrottleStartMillivolts = cfg.ThrottleStartMillivolts; - ThrottleEndMillivolts = cfg.ThrottleEndMillivolts; - ThrottleStartPercent = cfg.ThrottleStartPercent; - ShiftInterruptDuration = cfg.ShiftInterruptDuration; - ShiftInterruptCurrentThresholdPercent = cfg.ShiftInterruptCurrentThresholdPercent; - WalkModeDataDisplay = cfg.WalkModeDataDisplay; - AssistModeSelection = cfg.AssistModeSelection; - AssistStartupLevel = cfg.AssistStartupLevel; - - for (int i = 0; i < Math.Min(cfg.StandardAssistLevels.Length, StandardAssistLevels.Length); ++i) - { - StandardAssistLevels[i].Type = cfg.StandardAssistLevels[i].Type; - StandardAssistLevels[i].MaxCurrentPercent = cfg.StandardAssistLevels[i].MaxCurrentPercent; - StandardAssistLevels[i].MaxThrottlePercent = cfg.StandardAssistLevels[i].MaxThrottlePercent; - StandardAssistLevels[i].MaxCadencePercent = cfg.StandardAssistLevels[i].MaxCadencePercent; - StandardAssistLevels[i].MaxSpeedPercent = cfg.StandardAssistLevels[i].MaxSpeedPercent; - StandardAssistLevels[i].TorqueAmplificationFactor = cfg.StandardAssistLevels[i].TorqueAmplificationFactor; - } - - for (int i = 0; i < Math.Min(cfg.SportAssistLevels.Length, SportAssistLevels.Length); ++i) - { - SportAssistLevels[i].Type = cfg.SportAssistLevels[i].Type; - SportAssistLevels[i].MaxCurrentPercent = cfg.SportAssistLevels[i].MaxCurrentPercent; - SportAssistLevels[i].MaxThrottlePercent = cfg.SportAssistLevels[i].MaxThrottlePercent; - SportAssistLevels[i].MaxCadencePercent = cfg.SportAssistLevels[i].MaxCadencePercent; - SportAssistLevels[i].MaxSpeedPercent = cfg.SportAssistLevels[i].MaxSpeedPercent; - SportAssistLevels[i].TorqueAmplificationFactor = cfg.SportAssistLevels[i].TorqueAmplificationFactor; - } - } - - public void ReadFromFile(string filepath) - { - var serializer = new XmlSerializer(typeof(Configuration)); - - using (var reader = new FileStream(filepath, FileMode.Open)) - { - var obj = serializer.Deserialize(reader) as Configuration; - CopyFrom(obj); - } - } - - public void WriteToFile(string filepath) - { - var serializer = new XmlSerializer(typeof(Configuration)); - var settings = new XmlWriterSettings { Encoding = Encoding.UTF8, Indent = true }; - using (var xmlWriter = XmlWriter.Create(new StreamWriter(filepath), settings)) - { - serializer.Serialize(xmlWriter, this); - } - } - - public void Validate() - { - ValidateLimits(MaxCurrentAmps, 5, MaxCurrentLimitAmps, "Max Current (A)"); - ValidateLimits(CurrentRampAmpsSecond, 1, 255, "Current Ramp (A/s)"); - ValidateLimits((uint)MaxBatteryVolts, 1, 100, "Max Battery Voltage (V)"); - ValidateLimits(LowCutoffVolts, 1, 100, "Low Voltage Cut Off (V)"); - - ValidateLimits((uint)WheelSizeInch, 10, 40, "Wheel Size (inch)"); - ValidateLimits(NumWheelSensorSignals, 1, 10, "Wheel Sensor Signals"); - ValidateLimits(MaxSpeedKph, 0, 100, "Max Speed (km/h)"); - - ValidateLimits(PasStartDelayPulses, 0, 24, "Pas Delay (pulses)"); - ValidateLimits(PasStopDelayMilliseconds, 50, 1000, "Pas Stop Delay (ms)"); - ValidateLimits(PasKeepCurrentPercent, 10, 100, "Pas Keep Current (%)"); - ValidateLimits(PasKeepCurrentCadenceRpm, 0, 255, "Pas Keep Current Cadence (rpm)"); - - ValidateLimits(ThrottleStartMillivolts, 200, 2500, "Throttle Start (mV)"); - ValidateLimits(ThrottleEndMillivolts, 2500, 5000, "Throttle End (mV)"); - ValidateLimits(ThrottleStartPercent, 0, 100, "Throttle Start (%)"); - - ValidateLimits(ShiftInterruptDuration, 50, 2000, "Shift Interrupt Duration (ms)"); - ValidateLimits(ShiftInterruptCurrentThresholdPercent, 0, 100, "Shift Interrupt Current Threshold (%)"); - - ValidateLimits(AssistStartupLevel, 0, 9, "Assist Startup Level"); - - for (int i = 0; i < StandardAssistLevels.Length; ++i) - { - ValidateLimits(StandardAssistLevels[i].MaxCurrentPercent, 0, 100, $"Standard (Level {i}): Target Power (%)"); - ValidateLimits(StandardAssistLevels[i].MaxThrottlePercent, 0, 100, $"Standard (Level {i}): Max Throttle (%)"); - ValidateLimits(StandardAssistLevels[i].MaxCadencePercent, 0, 100, $"Standard (Level {i}): Max Cadence (%)"); - ValidateLimits(StandardAssistLevels[i].MaxSpeedPercent, 0, 100, $"Standard (Level {i}): Max Speed (%)"); - ValidateLimits((uint)StandardAssistLevels[i].TorqueAmplificationFactor, 0, 25, $"Standard (Level {i}): Torque Amplification"); - } - - for (int i = 0; i < SportAssistLevels.Length; ++i) - { - ValidateLimits(SportAssistLevels[i].MaxCurrentPercent, 0, 100, $"Sport (Level {i}): Target Power (%)"); - ValidateLimits(SportAssistLevels[i].MaxThrottlePercent, 0, 100, $"Sport (Level {i}): Max Throttle (%)"); - ValidateLimits(SportAssistLevels[i].MaxCadencePercent, 0, 100, $"Sport (Level {i}): Max Cadence (%)"); - ValidateLimits(SportAssistLevels[i].MaxSpeedPercent, 0, 100, $"Sport (Level {i}): Max Speed (%)"); - ValidateLimits((uint)SportAssistLevels[i].TorqueAmplificationFactor, 0, 25, $"Sport (Level {i}): Torque Amplification"); - } - } - - - private void ValidateLimits(uint value, uint min, uint max, string name) - { - if (value < min || value > max) - { - throw new Exception(name + " must be in interval " + min + "-" + max + "."); - } - } - - } -} + } +} diff --git a/src/tool/View/MainWindow.xaml b/src/tool/View/MainWindow.xaml index 2bd41d5..8d9d0e5 100644 --- a/src/tool/View/MainWindow.xaml +++ b/src/tool/View/MainWindow.xaml @@ -7,7 +7,7 @@ xmlns:vm="clr-namespace:BBSFW.ViewModel" xmlns:vw="clr-namespace:BBSFW.View" mc:Ignorable="d" - Title="BBS-FW Tool" Height="580" Width="800" + Title="BBS-FW Tool" Height="640" Width="860" Background="#FFE8E8E8"> diff --git a/src/tool/View/SystemView.xaml b/src/tool/View/SystemView.xaml index 8561ec9..09a52ee 100644 --- a/src/tool/View/SystemView.xaml +++ b/src/tool/View/SystemView.xaml @@ -5,7 +5,7 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:local="clr-namespace:BBSFW.View" mc:Ignorable="d" - d:DesignHeight="450" d:DesignWidth="800"> + d:DesignHeight="500" d:DesignWidth="850"> @@ -75,6 +75,8 @@ + + @@ -88,6 +90,46 @@ + + + + Enable global speed limit when throttle is used. Some countries have laws + that only permits use of throttle up to a specific speed. This option can + be used in order to comply with such laws. + + + When this option is enabled the configured global speed limit will override the + assist level speed limit when throttle is used. + + + Disabled - No global speed limit. + + Enabled - Global speed limit applies to all assist levels. + + Standard Levels - Global speed limit applies to standard assist levels only. + + + + + + + + Set speed limit in % of configured max speed when global throttle speed limit is enabled. + + + + + + + + @@ -263,6 +305,7 @@ + @@ -279,6 +322,8 @@ Temperature - Display max value of motor/controller temperature in °C. Requested Power - Display requested motor power in percent. + + Battery Level - Display computed battery level in percent. diff --git a/src/tool/ViewModel/ConfigurationViewModel.cs b/src/tool/ViewModel/ConfigurationViewModel.cs index 29d388e..736be77 100644 --- a/src/tool/ViewModel/ConfigurationViewModel.cs +++ b/src/tool/ViewModel/ConfigurationViewModel.cs @@ -48,7 +48,6 @@ public static List TemperatureSensorOptions new ValueItemViewModel(Configuration.AssistModeSelect.Pas9AndLights, "PAS 9 + Lights Button"), }; - public static List> WalkModeDataDisplayOptions { get; } = new List> { @@ -58,6 +57,14 @@ public static List TemperatureSensorOptions new ValueItemViewModel(Configuration.WalkModeData.BatteryPercent, "Battery Level (%)") }; + public static List> ThrottleGlobalSpeedLimitOptions { get; } = + new List> + { + new ValueItemViewModel(Configuration.ThrottleGlobalSpeedLimit.Disabled, "Disabled"), + new ValueItemViewModel(Configuration.ThrottleGlobalSpeedLimit.Enabled, "Enabled"), + new ValueItemViewModel(Configuration.ThrottleGlobalSpeedLimit.StandardLevels, "Standard Levels"), + }; + // support @@ -302,6 +309,37 @@ public uint ThrottleStartCurrentPercent } } + public ValueItemViewModel ThrottleGlobalSpeedLimitOpt + { + get + { + return ThrottleGlobalSpeedLimitOptions.FirstOrDefault((e) => e.Value == _config.ThrottleGlobalSpeedLimitOpt); + } + set + { + if (_config.ThrottleGlobalSpeedLimitOpt != value.Value) + { + _config.ThrottleGlobalSpeedLimitOpt = value.Value; + OnPropertyChanged(nameof(ThrottleGlobalSpeedLimitOpt)); + } + } + } + + public uint ThrottleGlobalSpeedLimitPercent + { + get { return _config.ThrottleGlobalSpeedLimitPercent; } + set + { + if (_config.ThrottleGlobalSpeedLimitPercent != value) + { + _config.ThrottleGlobalSpeedLimitPercent = value; + OnPropertyChanged(nameof(ThrottleGlobalSpeedLimitPercent)); + } + } + } + + + public uint PasStartDelayDegrees { get { return _config.PasStartDelayPulses * 15; }