From a8e511ebd4a3f434000d600b8b43f3d3bf67c152 Mon Sep 17 00:00:00 2001 From: casainho Date: Fri, 5 Apr 2024 17:08:08 +0100 Subject: [PATCH] Improved ESPNow boards IDs structure. Extra validation to ESPNow data sent to display --- __init__.py | 0 diy_display/__init__.py | 0 diy_display/firmware/front_lights_espnow.py | 5 +- diy_display/firmware/main copy.py | 435 ++++++++++++++++++ diy_display/firmware/main.py | 48 +- diy_display/firmware/motor_board_espnow.py | 31 +- diy_display/firmware/power_switch_espnow.py | 5 +- diy_display/firmware/rear_lights_espnow.py | 4 +- diy_display/firmware/settings.toml | 10 + diy_display/firmware/system_data.py | 3 +- diy_lights_board/firmware/espnow_comms.py | 22 +- diy_lights_board/firmware/main.py | 7 +- diy_main_board/firmware/boot.py | 8 +- diy_main_board/firmware/boot_out.txt | 4 - .../escooter_fiido_q1_s/display_espnow.py | 21 +- .../firmware/escooter_fiido_q1_s/main.py | 116 ++--- diy_main_board/firmware/settings.toml | 10 + diy_main_board/firmware/vesc.py | 4 +- firmware_common/__init__.py | 0 firmware_common/boards_ids.py | 7 + 20 files changed, 588 insertions(+), 152 deletions(-) create mode 100644 __init__.py create mode 100644 diy_display/__init__.py create mode 100644 diy_display/firmware/main copy.py create mode 100644 diy_display/firmware/settings.toml delete mode 100644 diy_main_board/firmware/boot_out.txt create mode 100644 firmware_common/__init__.py create mode 100644 firmware_common/boards_ids.py diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/diy_display/__init__.py b/diy_display/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/diy_display/firmware/front_lights_espnow.py b/diy_display/firmware/front_lights_espnow.py index 0727709..df38ce9 100644 --- a/diy_display/firmware/front_lights_espnow.py +++ b/diy_display/firmware/front_lights_espnow.py @@ -1,11 +1,10 @@ import espnow as ESPNow +from ...firmware_common.boards_ids import BoardsIds class FrontLights(object): def __init__(self, _espnow, mac_address, system_data): self._system_data = system_data - self.message_id = 16 # front lights ESPNow messages ID - self._espnow = _espnow self._peer = ESPNow.Peer(mac=bytes(mac_address), channel=1) @@ -15,7 +14,7 @@ def update(self): # add peer before sending the message self._espnow.peers.append(self._peer) - self._espnow.send(f"{self.message_id} {int(self._system_data.front_lights_board_pins_state)}") + self._espnow.send(f"{int(BoardsIds.FRONT_LIGHTS)} {int(self._system_data.front_lights_board_pins_state)}") # now remove the peer self._espnow.peers.remove(self._peer) diff --git a/diy_display/firmware/main copy.py b/diy_display/firmware/main copy.py new file mode 100644 index 0000000..874c0fc --- /dev/null +++ b/diy_display/firmware/main copy.py @@ -0,0 +1,435 @@ +import board +import display as Display +import motor_board_espnow +import system_data as _SystemData +import time +import displayio +from adafruit_display_text import label +import terminalio +import thisbutton as tb +import power_switch_espnow +import rear_lights_espnow +import front_lights_espnow +import wifi +import espnow as _ESPNow + +import supervisor +supervisor.runtime.autoreload = False + +print("Starting Display") + +######################################## +# CONFIGURATIONS + +# MAC Address value needed for the wireless communication +my_mac_address = [0x68, 0xb6, 0xb3, 0x01, 0xf7, 0xf3] +mac_address_power_switch_board = [0x68, 0xb6, 0xb3, 0x01, 0xf7, 0xf1] +mac_address_motor_board = [0x68, 0xb6, 0xb3, 0x01, 0xf7, 0xf2] +mac_address_rear_lights_board = [0x68, 0xb6, 0xb3, 0x01, 0xf7, 0xf4] +mac_address_front_lights_board = [0x68, 0xb6, 0xb3, 0x01, 0xf7, 0xf5] +######################################## + +system_data = _SystemData.SystemData() + +wifi.radio.enabled = True +wifi.radio.mac_address = bytearray(my_mac_address) +wifi.radio.start_ap(ssid="NO_SSID", channel=1) +wifi.radio.stop_ap() + +_espnow = _ESPNow.ESPNow() +motor = motor_board_espnow.MotorBoard(_espnow, mac_address_motor_board, system_data) # System data object to hold the EBike data +power_switch = power_switch_espnow.PowerSwitch(_espnow, mac_address_power_switch_board, system_data) +front_lights = front_lights_espnow.FrontLights(_espnow, mac_address_front_lights_board, system_data) +rear_lights = rear_lights_espnow.RearLights(_espnow, mac_address_rear_lights_board, system_data) + +# this will try send data over ESPNow and if there is an error, will restart +system_data.display_communication_counter = (system_data.display_communication_counter + 1) % 1024 +# just to check if is possible to send data to motor + +displayObject = Display.Display( + board.IO7, # CLK pin + board.IO9, # MOSI pin + board.IO5, # chip select pin, not used but for some reason there is an error if chip_select is None + board.IO12, # command pin + board.IO11, # reset pin + 1000000) # spi clock frequency +display = displayObject.display + +DISPLAY_WIDTH = 64 +DISPLAY_HEIGHT = 128 +TEXT = "0" + +def filter_motor_power(motor_power): + + if motor_power < 0: + if motor_power > -10: + motor_power = 0 + elif motor_power > -25: + pass + elif motor_power > -50: + motor_power = round(motor_power / 2) * 2 + elif motor_power > -100: + motor_power = round(motor_power / 5) * 5 + else: + motor_power = round(motor_power / 10) * 10 + else: + if motor_power < 10: + motor_power = 0 + elif motor_power < 25: + pass + elif motor_power < 50: + motor_power = round(motor_power / 2) * 2 + elif motor_power < 100: + motor_power = round(motor_power / 5) * 5 + else: + motor_power = round(motor_power / 10) * 10 + + return motor_power + +assist_level_area = label.Label(terminalio.FONT, text=TEXT) +assist_level_area.anchor_point = (0.0, 0.0) +assist_level_area.anchored_position = (4, 0) +assist_level_area.scale = 2 + +battery_voltage_area = label.Label(terminalio.FONT, text=TEXT) +battery_voltage_area.anchor_point = (1.0, 0.0) +battery_voltage_area.anchored_position = (129, 0) +battery_voltage_area.scale = 1 + +label_x = 61 +label_y = 10 + 24 +label_1 = label.Label(terminalio.FONT, text=TEXT) +label_1.anchor_point = (1.0, 0.0) +label_1.anchored_position = (label_x, label_y) +label_1.scale = 2 + +label_x = 61 +label_y = 0 +label_2 = label.Label(terminalio.FONT, text=TEXT) +label_2.anchor_point = (1.0, 0.0) +label_2.anchored_position = (label_x, label_y) +label_2.scale = 2 + +label_x = 120 +label_y = 10 + 24 +label_3 = label.Label(terminalio.FONT, text=TEXT) +label_3.anchor_point = (1.0, 0.0) +label_3.anchored_position = (label_x, label_y) +label_3.scale = 2 + +warning_area = label.Label(terminalio.FONT, text=TEXT) +warning_area.anchor_point = (0.0, 0.0) +warning_area.anchored_position = (2, 48) +warning_area.scale = 1 + +assist_level = 0 +assist_level_state = 0 +now = time.monotonic() +buttons_time_previous = now +display_time_previous = now +ebike_rx_tx_data_time_previous = now +lights_send_data_time_previous = now +power_switch_send_data_time_previous = now + +battery_voltage_previous_x10 = 9999 +battery_current_previous_x100 = 9999 +motor_current_previous_x100 = 9999 +motor_power_previous = 9999 +motor_temperature_x10_previous = 9999 +vesc_temperature_x10_previous = 9999 +motor_speed_erpm_previous = 9999 +brakes_are_active_previous = False +vesc_fault_code_previous = 9999 + +rear_light_pin_tail_bit = 1 +rear_light_pin_stop_bit = 2 +rear_light_pin_turn_left_bit = 4 +rear_light_pin_turn_right_bit = 8 + +front_light_pin_head_bit = 1 + +# keep tail light always on +system_data.rear_lights_board_pins_state = 0 + +def turn_off_execute(): + + motor.send_data() + + system_data.display_communication_counter = (system_data.display_communication_counter + 1) % 1024 + power_switch.update() + + front_lights.update() + rear_lights.update() + +def turn_off(): + + # new values when turn off the system + system_data.motor_enable_state = False + system_data.turn_off_relay = True + system_data.front_lights_board_pins_state = 0 + system_data.rear_lights_board_pins_state = 0 + + label_x = 10 + label_y = 18 + label_1 = label.Label(terminalio.FONT, text="Shutting down") + label_1.anchor_point = (0.0, 0.0) + label_1.anchored_position = (label_x, label_y) + label_1.scale = 1 + + g = displayio.Group() + g.append(label_1) + display.root_group = g + + # wait for button long press release + while buttons[button_POWER].isHeld: + buttons[button_POWER].tick() + turn_off_execute() + time.sleep(0.05) + + # keep sending the data to the various boards until the system turns off (battery power off), + # or reset the display if button_POWER is clicked + while not buttons[button_POWER].buttonActive: + buttons[button_POWER].tick() + turn_off_execute() + time.sleep(0.05) + + # let's reset the display + import supervisor + supervisor.reload() + while True: + pass + +def increase_assist_level(): + if assist_level < 20: + assist_level += 1 + +def decrease_assist_level(): + if assist_level > 0: + assist_level -= 1 + +def button_power_click_start_cb(): + system_data.button_power_state |= 1 + + # flip bit state + if system_data.button_power_state & 0x0100: + system_data.button_power_state &= ~0x0100 + else: + system_data.button_power_state |= 0x0100 + +def button_power_click_release_cb(): + system_data.button_power_state &= ~1 + +def button_power_long_click_start_cb(): + # only turn off after initial motor enable + if system_data.motor_enable_state and system_data.wheel_speed_x10 < 20: + turn_off() + else: + system_data.button_power_state |= 2 + + # flip bit state + if system_data.button_power_state & 0x0200: + system_data.button_power_state &= ~0x0200 + else: + system_data.button_power_state |= 0x0200 + +def button_power_long_click_release_cb(): + system_data.button_power_state &= ~2 + +def button_left_click_start_cb(): + system_data.rear_lights_board_pins_state |= rear_light_pin_turn_left_bit + +def button_left_click_release_cb(): + system_data.rear_lights_board_pins_state &= ~rear_light_pin_turn_left_bit + +def button_right_click_start_cb(): + system_data.rear_lights_board_pins_state |= rear_light_pin_turn_right_bit + +def button_right_click_release_cb(): + system_data.rear_lights_board_pins_state &= ~rear_light_pin_turn_right_bit + +def button_lights_click_start_cb(): + system_data.lights_state = True + +def button_lights_click_release_cb(): + system_data.lights_state = False + +def button_switch_click_start_cb(): + pass + +def button_switch_click_release_cb(): + pass + +### Setup buttons ### +button_POWER, button_LEFT, button_RIGHT, button_LIGHTS, button_SWITCH = range(5) +buttons_pins = [ + board.IO39, # button_POWER + board.IO37, # button_LEFT + board.IO35, # button_RIGHT + board.IO33, # button_LIGHTS + board.IO18 # button_SWITCH +] + +buttons_callbacks = { + button_POWER: { + 'click_start': button_power_click_start_cb, + 'click_release': button_power_click_release_cb, + 'long_click_start': button_power_long_click_start_cb, + 'long_click_release': button_power_long_click_release_cb}, + button_LEFT: { + 'click_start': button_left_click_start_cb, + 'click_release': button_left_click_release_cb}, + button_RIGHT: { + 'click_start': button_right_click_start_cb, + 'click_release': button_right_click_release_cb}, + button_LIGHTS: { + 'click_start': button_lights_click_start_cb, + 'click_release': button_lights_click_release_cb}, + button_SWITCH: { + 'click_start': button_switch_click_start_cb, + 'click_release': button_switch_click_release_cb} +} + +nr_buttons = len(buttons_pins) +buttons = [0] * nr_buttons +for index in range(nr_buttons): + buttons[index] = tb.thisButton(buttons_pins[index], True) + buttons[index].setDebounceThreshold(50) + buttons[index].setLongPressThreshold(1500) + # check if each callback is defined, and if so, register it + if 'click_start' in buttons_callbacks[index]: buttons[index].assignClickStart(buttons_callbacks[index]['click_start']) + if 'click_release' in buttons_callbacks[index]: buttons[index].assignClickRelease(buttons_callbacks[index]['click_release']) + if 'long_click_start' in buttons_callbacks[index]: buttons[index].assignLongClickStart(buttons_callbacks[index]['long_click_start']) + if 'long_click_release' in buttons_callbacks[index]: buttons[index].assignLongClickRelease(buttons_callbacks[index]['long_click_release']) + +# show init screen +label_x = 10 +label_y = 18 +label_init_screen = label.Label(terminalio.FONT, text=TEXT) +label_init_screen.anchor_point = (0.0, 0.0) +label_init_screen.anchored_position = (label_x, label_y) +label_init_screen.scale = 1 +label_init_screen.text = "Ready to power on" +screen1_group = displayio.Group() +screen1_group.append(label_init_screen) +display.root_group = screen1_group + +# let's wait for a first click on power button +time_previous = time.monotonic() +while True: + now = time.monotonic() + if (now - time_previous) > 0.05: + time_previous = now + + buttons[button_POWER].tick() + + if buttons[button_POWER].buttonActive: + + # wait for user to release the button + while buttons[button_POWER].buttonActive: + buttons[button_POWER].tick() + + # motor board will now enable the motor + system_data.motor_enable_state = True + break + +# show main screen +text_group = displayio.Group() +# text_group.append(assist_level_area) +text_group.append(battery_voltage_area) +text_group.append(label_1) +text_group.append(label_2) +text_group.append(label_3) +text_group.append(warning_area) +display.root_group = text_group + +while True: + now = time.monotonic() + if (now - display_time_previous) > 0.1: + display_time_previous = now + + if battery_voltage_previous_x10 != system_data.battery_voltage_x10: + battery_voltage_previous_x10 = system_data.battery_voltage_x10 + battery_voltage = system_data.battery_voltage_x10 / 10.0 + battery_voltage_area.text = f"{battery_voltage:2.1f}v" + + # calculate the motor power + if system_data.motor_speed_erpm < 100: + system_data.battery_current_x100 = 0 + + system_data.motor_power = int((system_data.battery_voltage_x10 * system_data.battery_current_x100) / 1000.0) + if motor_power_previous != system_data.motor_power: + motor_power_previous = system_data.motor_power + motor_power = filter_motor_power(system_data.motor_power) + label_1.text = f"{motor_power:5}" + + # if motor_current_previous_x100 != system_data.motor_current_x100: + # motor_current_previous_x100 = s ystem_data.motor_current_x100 + + # motor_current = int(system_data.motor_current_x100 / 100.0) + # label_1.text = f"{motor_current:5}" + + if motor_temperature_x10_previous != system_data.motor_temperature_x10: + motor_temperature_x10_previous = system_data.motor_temperature_x10 + label_2.text = f"{int(system_data.motor_temperature_x10 / 10.0): 2}" + + label_3.text = f"{float(system_data.wheel_speed_x10 / 10.0)}" + + now = time.monotonic() + if (now - ebike_rx_tx_data_time_previous) > 0.05: + ebike_rx_tx_data_time_previous = now + motor.send_data() + motor.process_data() + + if brakes_are_active_previous != system_data.brakes_are_active: + brakes_are_active_previous = system_data.brakes_are_active + if system_data.brakes_are_active: + warning_area.text = str("brakes") + else: + warning_area.text = str("") + elif vesc_fault_code_previous != system_data.vesc_fault_code: + vesc_fault_code_previous = system_data.vesc_fault_code + if system_data.vesc_fault_code: + warning_area.text = str(f"mot e: {system_data.vesc_fault_code}") + else: + warning_area.text = str("") + + now = time.monotonic() + if (now - lights_send_data_time_previous) > 0.05: + lights_send_data_time_previous = now + + # if we are braking, enable brake light + # braking current < 20A + if system_data.brakes_are_active or system_data.motor_current_x100 < -2000.0: + system_data.rear_lights_board_pins_state |= rear_light_pin_stop_bit + else: + system_data.rear_lights_board_pins_state &= ~rear_light_pin_stop_bit + + # if lights are enable, enable the tail light + if system_data.lights_state: + system_data.front_lights_board_pins_state |= front_light_pin_head_bit + system_data.rear_lights_board_pins_state |= rear_light_pin_tail_bit + else: + system_data.front_lights_board_pins_state &= ~front_light_pin_head_bit + system_data.rear_lights_board_pins_state &= ~rear_light_pin_tail_bit + + front_lights.update() + rear_lights.update() + + now = time.monotonic() + if (now - power_switch_send_data_time_previous) > 0.5: + power_switch_send_data_time_previous = now + + system_data.display_communication_counter = (system_data.display_communication_counter + 1) % 1024 + power_switch.update() + + now = time.monotonic() + if (now - buttons_time_previous) > 0.05: + buttons_time_previous = now + + for index in range(nr_buttons): + buttons[index].tick() + + # system_data.assist_level = assist_level + # assist_level_area.text = str(assist_level) + + diff --git a/diy_display/firmware/main.py b/diy_display/firmware/main.py index d5d61de..874c0fc 100644 --- a/diy_display/firmware/main.py +++ b/diy_display/firmware/main.py @@ -1,5 +1,5 @@ import board -import display +import display as Display import motor_board_espnow import system_data as _SystemData import time @@ -46,7 +46,7 @@ system_data.display_communication_counter = (system_data.display_communication_counter + 1) % 1024 # just to check if is possible to send data to motor -displayObject = display.Display( +displayObject = Display.Display( board.IO7, # CLK pin board.IO9, # MOSI pin board.IO5, # chip select pin, not used but for some reason there is an error if chip_select is None @@ -97,13 +97,21 @@ def filter_motor_power(motor_power): battery_voltage_area.scale = 1 label_x = 61 -label_y = 10 + 16 +label_y = 10 + 24 label_1 = label.Label(terminalio.FONT, text=TEXT) label_1.anchor_point = (1.0, 0.0) label_1.anchored_position = (label_x, label_y) label_1.scale = 2 -label_x = 129 +label_x = 61 +label_y = 0 +label_2 = label.Label(terminalio.FONT, text=TEXT) +label_2.anchor_point = (1.0, 0.0) +label_2.anchored_position = (label_x, label_y) +label_2.scale = 2 + +label_x = 120 +label_y = 10 + 24 label_3 = label.Label(terminalio.FONT, text=TEXT) label_3.anchor_point = (1.0, 0.0) label_3.anchored_position = (label_x, label_y) @@ -144,7 +152,6 @@ def filter_motor_power(motor_power): system_data.rear_lights_board_pins_state = 0 def turn_off_execute(): - global motor, power_switch, front_lights, rear_lights, buttons motor.send_data() @@ -155,7 +162,6 @@ def turn_off_execute(): rear_lights.update() def turn_off(): - global system_data, buttons, motor, front_lights, back_lights, power_switch, label_x, label_y, label_1, display # new values when turn off the system system_data.motor_enable_state = False @@ -194,12 +200,10 @@ def turn_off(): pass def increase_assist_level(): - global assist_level if assist_level < 20: assist_level += 1 def decrease_assist_level(): - global assist_level if assist_level > 0: assist_level -= 1 @@ -217,7 +221,7 @@ def button_power_click_release_cb(): def button_power_long_click_start_cb(): # only turn off after initial motor enable - if system_data.motor_enable_state and system_data.wheel_speed < 2.0: + if system_data.motor_enable_state and system_data.wheel_speed_x10 < 20: turn_off() else: system_data.button_power_state |= 2 @@ -333,7 +337,7 @@ def button_switch_click_release_cb(): # text_group.append(assist_level_area) text_group.append(battery_voltage_area) text_group.append(label_1) -# text_group.append(label_2) +text_group.append(label_2) text_group.append(label_3) text_group.append(warning_area) display.root_group = text_group @@ -359,30 +363,16 @@ def button_switch_click_release_cb(): label_1.text = f"{motor_power:5}" # if motor_current_previous_x100 != system_data.motor_current_x100: - # motor_current_previous_x100 = system_data.motor_current_x100 + # motor_current_previous_x100 = s ystem_data.motor_current_x100 # motor_current = int(system_data.motor_current_x100 / 100.0) # label_1.text = f"{motor_current:5}" - # if motor_temperature_sensor_x10_previous != ebike_data.motor_temperature_sensor_x10: - # motor_temperature_sensor_x10_previous = ebike_data.motor_temperature_sensor_x10 - # label_2.text = str(f"{(ebike_data.motor_temperature_sensor_x10 / 10.0): 2.1f}") - - if motor_speed_erpm_previous != system_data.motor_speed_erpm: - motor_speed_erpm_previous = system_data.motor_speed_erpm - - if system_data.motor_speed_erpm > 150: - # Fiido Q1S with installed Luneye motor 2000W - # calculate the wheel speed - wheel_radius = 0.165 # measured as 16.5cms - perimeter = 6.28 * wheel_radius # 2*pi = 6.28 - motor_rpm = system_data.motor_speed_erpm / 15.0 # motor with 15 poles pair - system_data.wheel_speed = ((perimeter / 1000.0) * motor_rpm * 60) - - else: - system_data.wheel_speed = 0 + if motor_temperature_x10_previous != system_data.motor_temperature_x10: + motor_temperature_x10_previous = system_data.motor_temperature_x10 + label_2.text = f"{int(system_data.motor_temperature_x10 / 10.0): 2}" - label_3.text = f"{system_data.wheel_speed:2.1f}" + label_3.text = f"{float(system_data.wheel_speed_x10 / 10.0)}" now = time.monotonic() if (now - ebike_rx_tx_data_time_previous) > 0.05: diff --git a/diy_display/firmware/motor_board_espnow.py b/diy_display/firmware/motor_board_espnow.py index a2a1760..f3ccb68 100644 --- a/diy_display/firmware/motor_board_espnow.py +++ b/diy_display/firmware/motor_board_espnow.py @@ -1,12 +1,11 @@ import espnow as ESPNow +from ...firmware_common.boards_ids import BoardsIds class MotorBoard(object): def __init__(self, _espnow, mac_address, system_data): self._espnow = _espnow self._peer = ESPNow.Peer(mac=bytes(mac_address), channel=1) - self._system_data = system_data - self.message_id = 1 # motor board ESPNow messages ID def process_data(self): try: @@ -14,22 +13,26 @@ def process_data(self): # read a package and discard others available while self._espnow is not None: - rx_data = self._espnow.read() - if rx_data is None: + rx_data_string = self._espnow.read() + if rx_data_string is None: break else: - data = rx_data + data = rx_data_string # process the package, if available if data is not None: - data = [n for n in data.msg.split()] - self._system_data.battery_voltage_x10 = int(data[0]) - self._system_data.battery_current_x100 = int(data[1]) * -1.0 - self._system_data.motor_current_x100 = int(data[2]) * -1.0 - self._system_data.motor_speed_erpm = int(data[3]) - self._system_data.brakes_are_active = True if int(data[4]) == 1 else False - self._system_data.vesc_temperature_x10 = int(data[5]) - self._system_data.motor_temperature_x10 = int(data[6]) + data_list = [int(n) for n in data.msg.split()] + + # only process packages for us + # must have 8 elements: message_id + 7 variables + if int(data_list[0]) == int(BoardsIds.DISPLAY) and len(data_list) == 8: + self._system_data.battery_voltage_x10 = data_list[1] + self._system_data.battery_current_x100 = data_list[2] + self._system_data.motor_current_x100 = data_list[3] + self._system_data.wheel_speed_x10 = data_list[4] + self._system_data.brakes_are_active = True if data_list[5] == 1 else False + self._system_data.vesc_temperature_x10 = data_list[6] + self._system_data.motor_temperature_x10 = data_list[7] except: pass @@ -40,7 +43,7 @@ def send_data(self): self._espnow.peers.append(self._peer) motor_enable_state = 1 if self._system_data.motor_enable_state else 0 - self._espnow.send(f"{int(self.message_id)} {motor_enable_state} {self._system_data.button_power_state}") + self._espnow.send(f"{int(BoardsIds.MAIN_BOARD)} {motor_enable_state} {self._system_data.button_power_state}") # now remove the peer self._espnow.peers.remove(self._peer) diff --git a/diy_display/firmware/power_switch_espnow.py b/diy_display/firmware/power_switch_espnow.py index 562ce8a..6129b1f 100644 --- a/diy_display/firmware/power_switch_espnow.py +++ b/diy_display/firmware/power_switch_espnow.py @@ -1,11 +1,10 @@ import espnow as ESPNow +from ...firmware_common.boards_ids import BoardsIds class PowerSwitch(object): def __init__(self, _espnow, mac_address, system_data): self._system_data = system_data - self.message_id = 4 # power switch ESPNow messages ID - self._espnow = _espnow self._peer = ESPNow.Peer(mac=bytes(mac_address), channel=1) @@ -15,7 +14,7 @@ def update(self): # add peer before sending the message self._espnow.peers.append(self._peer) - self._espnow.send(f"{self.message_id} {int(self._system_data.display_communication_counter)} {int(self._system_data.turn_off_relay)}") + self._espnow.send(f"{int(BoardsIds.POWER_SWITCH)} {int(self._system_data.display_communication_counter)} {int(self._system_data.turn_off_relay)}") # now remove the peer self._espnow.peers.remove(self._peer) diff --git a/diy_display/firmware/rear_lights_espnow.py b/diy_display/firmware/rear_lights_espnow.py index bcba0f0..2a8716c 100644 --- a/diy_display/firmware/rear_lights_espnow.py +++ b/diy_display/firmware/rear_lights_espnow.py @@ -1,10 +1,10 @@ import espnow as ESPNow +from ...firmware_common.boards_ids import BoardsIds class RearLights(object): def __init__(self, _espnow, mac_address, system_data): self._system_data = system_data - self.message_id = 8 # rear lights ESPNow messages ID self._espnow = _espnow self._peer = ESPNow.Peer(mac=bytes(mac_address), channel=1) @@ -15,7 +15,7 @@ def update(self): # add peer before sending the message self._espnow.peers.append(self._peer) - self._espnow.send(f"{self.message_id} {int(self._system_data.rear_lights_board_pins_state)}") + self._espnow.send(f"{int(BoardsIds.REAR_LIGHTS)} {int(self._system_data.rear_lights_board_pins_state)}") # now remove the peer self._espnow.peers.remove(self._peer) diff --git a/diy_display/firmware/settings.toml b/diy_display/firmware/settings.toml new file mode 100644 index 0000000..f97d9ac --- /dev/null +++ b/diy_display/firmware/settings.toml @@ -0,0 +1,10 @@ +# To auto-connect to Wi-Fi +CIRCUITPY_WIFI_SSID="egg" +CIRCUITPY_WIFI_PASSWORD="verygood" + +# To enable the web workflow. Change this too! +# Leave the User field blank in the browser. +CIRCUITPY_WEB_API_PASSWORD="verygood" + +CIRCUITPY_WEB_API_PORT=80 +CIRCUITPY_WEB_INSTANCE_NAME="EScooter Display" \ No newline at end of file diff --git a/diy_display/firmware/system_data.py b/diy_display/firmware/system_data.py index aa59be3..c95c4e1 100644 --- a/diy_display/firmware/system_data.py +++ b/diy_display/firmware/system_data.py @@ -8,8 +8,7 @@ def __init__(self): self.battery_current_x100 = 0 self.motor_power = 0 self.motor_current_x100 = 0 - self.motor_speed_erpm = 0 - self.wheel_speed = 0.0 + self.wheel_speed_x10 = 0 self.previous_motor_current_target = True self.brakes_are_active = False self.torque_weight = 0 diff --git a/diy_lights_board/firmware/espnow_comms.py b/diy_lights_board/firmware/espnow_comms.py index 9cc8e44..2468342 100644 --- a/diy_lights_board/firmware/espnow_comms.py +++ b/diy_lights_board/firmware/espnow_comms.py @@ -1,13 +1,21 @@ import wifi import espnow +from .main import lights_board, FRONT_VERSION, REAR_VERSION +from ...firmware_common.boards_ids import BoardsIds class ESPNowComms(object): - def __init__(self, mac_address, message_id): + def __init__(self, mac_address): wifi.radio.enabled = True wifi.radio.mac_address = bytearray(mac_address) - self._message_id = message_id self._espnow = espnow.ESPNow() + self._board_id = 0 + + if lights_board == FRONT_VERSION: + self._board_id = BoardsIds.FRONT_LIGHTS + elif lights_board == REAR_VERSION: + self._board_id = BoardsIds.REAR_LIGHTS + def get_data(self): received_data = None @@ -23,10 +31,12 @@ def get_data(self): # process the package, if available if data is not None: - data = [n for n in data.msg.split()] - # only process packages for us - if int(data[0]) == self._message_id: - received_data = int(data[1]) + data_list = [int(n) for n in data.msg.split()] + + # only process packages for us + # must have 2 elements: message_id + 1 variables + if data_list[0] == int(self._board_id) and len(data_list) == 2: + received_data = data[1] except Exception as ex: print(ex) diff --git a/diy_lights_board/firmware/main.py b/diy_lights_board/firmware/main.py index 42aff9f..b3526c1 100644 --- a/diy_lights_board/firmware/main.py +++ b/diy_lights_board/firmware/main.py @@ -19,15 +19,10 @@ # front lights board ESPNow MAC Address my_mac_address = [0x68, 0xb6, 0xb3, 0x01, 0xf7, 0xf5] - # front lights board ESPNow messages ID = 8 - message_id = 16 elif lights_board is REAR_VERSION: # rear lights board ESPNow MAC Address my_mac_address = [0x68, 0xb6, 0xb3, 0x01, 0xf7, 0xf4] - # rear lights board ESPNow messages ID = 8 - message_id = 8 - ################################################################ # print versions @@ -47,7 +42,7 @@ switch_pins[index].direction = digitalio.Direction.OUTPUT switch_pins[index].value = False -espnow_comms = espnow_comms.ESPNowComms(my_mac_address, message_id) +espnow_comms = espnow_comms.ESPNowComms(my_mac_address) io_pins_target = 0 io_pins_target_previous = 0 diff --git a/diy_main_board/firmware/boot.py b/diy_main_board/firmware/boot.py index 250c613..a81a080 100644 --- a/diy_main_board/firmware/boot.py +++ b/diy_main_board/firmware/boot.py @@ -1,4 +1,4 @@ -import storage -storage.disable_usb_drive() -storage.remount("/", readonly=False) -storage.enable_usb_drive() +# import storage +# storage.disable_usb_drive() +# storage.remount("/", readonly=False) +# storage.enable_usb_drive() diff --git a/diy_main_board/firmware/boot_out.txt b/diy_main_board/firmware/boot_out.txt deleted file mode 100644 index a3c4dbe..0000000 --- a/diy_main_board/firmware/boot_out.txt +++ /dev/null @@ -1,4 +0,0 @@ -Adafruit CircuitPython 8.2.9 on 2023-12-06; ESP32-S3-DevKitC-1-N8R2 with ESP32S3 -Board ID:espressif_esp32s3_devkitc_1_n8r2 -UID:866B3BB2E444 -boot.py output: diff --git a/diy_main_board/firmware/escooter_fiido_q1_s/display_espnow.py b/diy_main_board/firmware/escooter_fiido_q1_s/display_espnow.py index 922d246..06dd0f4 100644 --- a/diy_main_board/firmware/escooter_fiido_q1_s/display_espnow.py +++ b/diy_main_board/firmware/escooter_fiido_q1_s/display_espnow.py @@ -1,12 +1,11 @@ import espnow as ESPNow +from ....firmware_common.boards_ids import BoardsIds class Display(object): """Display""" def __init__(self, display_mac_address, system_data): self._system_data = system_data - self.message_id = 1 # motor board ESPNow message ID - self._espnow = ESPNow.ESPNow() peer = ESPNow.Peer(mac=bytes(display_mac_address), channel=1) self._espnow.peers.append(peer) @@ -17,19 +16,21 @@ def process_data(self): # read a package and discard others available while self._espnow is not None: - rx_data = self._espnow.read() - if rx_data is None: + rx_data_string = self._espnow.read() + if rx_data_string is None: break else: - data = rx_data + data = rx_data_string # process the package, if available if data is not None: - data = [n for n in data.msg.split()] + data_list = [int(n) for n in data.msg.split()] + # only process packages for us - if int(data[0]) == self.message_id: - self._system_data.motor_enable_state = True if int(data[1]) != 0 else False - self._system_data.button_power_state = int(data[2]) + # must have 3 elements: message_id + 2 variables + if int(data_list[0]) == int(BoardsIds.MAIN_BOARD) and len(data_list) == 3: + self._system_data.motor_enable_state = True if data_list[1] != 0 else False + self._system_data.button_power_state = data_list[2] except: pass @@ -37,6 +38,6 @@ def update(self): if self._espnow is not None: try: brakes_are_active = 1 if self._system_data.brakes_are_active else 0 - self._espnow.send(f"{int(self._system_data.battery_voltage_x10)} {int(self._system_data.battery_current_x100)} {int(self._system_data.motor_current_x100)} {self._system_data.motor_speed_erpm} {brakes_are_active} {int(self._system_data.vesc_temperature_x10)} {int(self._system_data.motor_temperature_x10)}") + self._espnow.send(f"{int(BoardsIds.DISPLAY)} {int(self._system_data.battery_voltage_x10)} {int(self._system_data.battery_current_x100)} {int(self._system_data.motor_current_x100)} {int(self._system_data.wheel_speed * 10)} {int(brakes_are_active)} {int(self._system_data.vesc_temperature_x10)} {int(self._system_data.motor_temperature_x10)}") except: pass diff --git a/diy_main_board/firmware/escooter_fiido_q1_s/main.py b/diy_main_board/firmware/escooter_fiido_q1_s/main.py index 5bbdc1e..f5f4869 100644 --- a/diy_main_board/firmware/escooter_fiido_q1_s/main.py +++ b/diy_main_board/firmware/escooter_fiido_q1_s/main.py @@ -16,17 +16,14 @@ import supervisor supervisor.runtime.autoreload = False -wifi.radio.enabled = True my_mac_address = [0x68, 0xb6, 0xb3, 0x01, 0xf7, 0xf2] wifi.radio.mac_address = bytearray(my_mac_address) +wifi.radio.enabled = True class MotorControlScheme: CURRENT = 0 SPEED_CURRENT = 1 -# while True: -# pass - # Tested on a ESP32-S3-DevKitC-1-N8R2 ############################################### @@ -47,18 +44,26 @@ class MotorControlScheme: throttle_adc_max = 50540 # this is a value that should be a bit lower than the max value, so if throttle is at max position, the calculated value of throttle will be the max throttle_adc_over_max_error = 51500 # this is a value that should be a bit superior than the max value, just to protect is the case there is some issue with the signal and then motor can keep run at max speed!! -motor_min_current_start = 10.0 # to much lower value will make the motor vibrate and not run, so, impose a min limit (??) -motor_max_current_limit = 150.0 # max value, be careful to not burn your motor +motor_min_current_start = 5.0 # to much lower value will make the motor vibrate and not run, so, impose a min limit (??) +motor_max_current_limit = 135.0 # max value, be careful to not burn your motor + +# To reduce motor temperature, motor current limits are higher at startup and low at higer speeds +# motor current limits will be adjusted on this values, depending on the speed +# like at startup will have 'motor_current_limit_max_max' and then will reduce linearly +# up to the 'motor_current_limit_max_min', when wheel speed is +# 'motor_current_limit_max_min_speed' +motor_current_limit_max_max = 135.0 +motor_current_limit_max_min = 50.0 +motor_current_limit_max_min_speed = 35.0 + +# this are the values for regen +motor_current_limit_min_min = -70.0 +motor_current_limit_min_max = -50.0 +motor_current_limit_min_max_speed = 35.0 # motor_control_scheme = MotorControlScheme.CURRENT motor_control_scheme = MotorControlScheme.SPEED_CURRENT -# ramp up and down constants -current_ramp_up_time = 0.00001 # ram up time for each 1A -current_ramp_down_time = 0.00001 # ram down time for each 1A -speed_ramp_up_time = 0.00001 # ram up time for each 1 erpm -speed_ramp_down_time = 0.00001 # ram down time for each 1 erpm - # MAC Address value needed for the wireless communication with the display display_mac_address = [0x68, 0xb6, 0xb3, 0x01, 0xf7, 0xf3] @@ -69,11 +74,9 @@ class MotorControlScheme: # if brakes are active at startup, block here # this is needed for development, to help keep the motor and the UART disable -# if brake_sensor.value: -# while True: -# print('brake at start') -# time.sleep(1) -# pass +while brake_sensor.value: + print('brake at start') + time.sleep(1) throttle = Throttle.Throttle( board.IO11, # ADC pin for throttle @@ -202,11 +205,11 @@ async def task_control_motor(): # low pass filter torque sensor value to smooth it, # because the easy DIY hardware lacks such low pass filter on purpose - #throttle_value_filtered = lowpass_filter(throttle.value, 0.25) - #if throttle_value_filtered < 1.0: + # throttle_value_filtered = lowpass_filter(throttle.value, 0.02) + # if throttle_value_filtered < 0.01: # throttle_value_filtered = 0 throttle_value_filtered = throttle.value - + # check to see if throttle is over the suposed max error value, # if this happens, that probably means there is an issue with ADC and this can be dangerous, # as this did happen a few times during development and motor keeps running at max target / current / speed!! @@ -217,20 +220,19 @@ async def task_control_motor(): vesc.set_motor_current_amps(0) vesc.set_motor_current_amps(0) vesc.set_motor_current_amps(0) - print(f"throttle_adc_previous_value: {throttle_adc_previous_value}") - raise Exception("throttle value is over max, this can be dangerous!") + raise Exception(f'throttle value: {throttle_adc_previous_value} -- is over max, this can be dangerous!') # Apply cruise control throttle_value_filtered = cruise_control(throttle_value_filtered) - motor_target_current = simpleio.map_range( + system_data.motor_target_current = simpleio.map_range( throttle_value_filtered, 0.0, # min input 1000.0, # max input motor_min_current_start, # min output motor_max_current_target) # max output - motor_target_speed = simpleio.map_range( + system_data.motor_target_speed = simpleio.map_range( throttle_value_filtered, 0.0, # min input 1000.0, # max input @@ -239,33 +241,12 @@ async def task_control_motor(): ########################################################################################## # impose a min motor target value, as to much lower value will make the motor vibrate and not run (??) - if motor_target_current < (motor_min_current_start + 1): - motor_target_current = 0.0 + if system_data.motor_target_current < (motor_min_current_start + 1): + system_data.motor_target_current = 0.0 - if motor_target_speed < 500.0: - motor_target_speed = 0.0 + if system_data.motor_target_speed < 500.0: + system_data.motor_target_speed = 0.0 - # apply ramp up / down factor: faster when ramp down - if motor_target_current > system_data.motor_target_current: - ramp_time_current = current_ramp_up_time - else: - ramp_time_current = current_ramp_down_time - - if motor_target_speed > system_data.motor_target_speed: - ramp_time_speed = speed_ramp_up_time - else: - ramp_time_speed = speed_ramp_down_time - - time_now = time.monotonic_ns() - ramp_step = (time_now - system_data.ramp_current_last_time) / (ramp_time_current * 40_000_000) - system_data.ramp_current_last_time = time_now - system_data.motor_target_current = utils_step_towards(system_data.motor_target_current, motor_target_current, ramp_step) - - time_now = time.monotonic_ns() - ramp_step = (time_now - system_data.ramp_speed_last_time) / (ramp_time_speed * 40_000_000) - system_data.ramp_speed_last_time = time_now - system_data.motor_target_speed = utils_step_towards(system_data.motor_target_speed, motor_target_speed, ramp_step) - # let's limit the value if system_data.motor_target_current > motor_max_current_limit: system_data.motor_target_current = motor_max_current_limit @@ -307,21 +288,23 @@ async def task_control_motor(): await asyncio.sleep(0.02) async def task_control_motor_limit_current(): + while True: ########################################################################################## motor_target_current_limit_max = simpleio.map_range( system_data.wheel_speed, - 0.0, # min input - 35.0, # max input - 135, # min output - 50) # max output + 0.0, + motor_current_limit_max_min_speed, + motor_current_limit_max_max, + motor_current_limit_max_min) - motor_target_current_limit_min = -1.0 * simpleio.map_range( + + motor_target_current_limit_min = simpleio.map_range( system_data.wheel_speed, - 0.0, # min input - 35.0, # max input - 70, # min output - 50) # max output + 0.0, + motor_current_limit_min_max_speed, + motor_current_limit_min_min, + motor_current_limit_min_max) vesc.set_motor_current_limit_max(motor_target_current_limit_max) vesc.set_motor_current_limit_min(motor_target_current_limit_min) @@ -339,15 +322,14 @@ async def task_various_0_5s(): if system_data.motor_speed_erpm != wheel_speed_previous_motor_speed_erpm: wheel_speed_previous_motor_speed_erpm = system_data.motor_speed_erpm - if system_data.motor_speed_erpm > 150: - # Fiido Q1S with installed Luneye motor 2000W - # calculate the wheel speed - wheel_radius = 0.165 # measured as 16.5cms - perimeter = 6.28 * wheel_radius # 2*pi = 6.28 - motor_rpm = system_data.motor_speed_erpm / motor_poles_pair - system_data.wheel_speed = ((perimeter / 1000.0) * motor_rpm * 60) - - else: + # Fiido Q1S with installed Luneye motor 2000W + # calculate the wheel speed + wheel_radius = 0.165 # measured as 16.5cms + perimeter = 6.28 * wheel_radius # 2*pi = 6.28 + motor_rpm = system_data.motor_speed_erpm / motor_poles_pair + system_data.wheel_speed = ((perimeter / 1000.0) * motor_rpm * 60) + + if abs(system_data.wheel_speed < 3.5): system_data.wheel_speed = 0.0 await asyncio.sleep(0.5) diff --git a/diy_main_board/firmware/settings.toml b/diy_main_board/firmware/settings.toml index e69de29..e2ab4c3 100644 --- a/diy_main_board/firmware/settings.toml +++ b/diy_main_board/firmware/settings.toml @@ -0,0 +1,10 @@ +# To auto-connect to Wi-Fi +CIRCUITPY_WIFI_SSID="egg" +CIRCUITPY_WIFI_PASSWORD="verygood" + +# To enable the web workflow. Change this too! +# Leave the User field blank in the browser. +CIRCUITPY_WEB_API_PASSWORD="verygood" + +CIRCUITPY_WEB_API_PORT=80 +CIRCUITPY_WEB_INSTANCE_NAME="EScooter Main Board" diff --git a/diy_main_board/firmware/vesc.py b/diy_main_board/firmware/vesc.py index e68f97f..95661dd 100644 --- a/diy_main_board/firmware/vesc.py +++ b/diy_main_board/firmware/vesc.py @@ -128,8 +128,8 @@ def refresh_data(self): # store the motor controller data self._app_data.vesc_temperature_x10 = struct.unpack_from('>h', response, 3)[0] self._app_data.motor_temperature_x10 = struct.unpack_from('>h', response, 5)[0] - self._app_data.motor_current_x100 = (struct.unpack_from('>l', response, 7)[0]) * -1.0 # for some reason, the current is inverted - self._app_data.battery_current_x100 = (struct.unpack_from('>l', response, 11)[0]) * -1.0 # for some reason, the current is inverted + self._app_data.motor_current_x100 = (struct.unpack_from('>l', response, 7)[0]) + self._app_data.battery_current_x100 = (struct.unpack_from('>l', response, 11)[0]) self._app_data.motor_speed_erpm = struct.unpack_from('>l', response, 25)[0] self._app_data.battery_voltage_x10 = struct.unpack_from('>h', response, 29)[0] self._app_data.vesc_fault_code = response[55] diff --git a/firmware_common/__init__.py b/firmware_common/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/firmware_common/boards_ids.py b/firmware_common/boards_ids.py new file mode 100644 index 0000000..a1385a3 --- /dev/null +++ b/firmware_common/boards_ids.py @@ -0,0 +1,7 @@ +class BoardsIds: + MAIN_BOARD = 1 + DISPLAY = 2 + POWER_SWITCH = 4 + REAR_LIGHTS = 8 + FRONT_LIGHTS = 16 + \ No newline at end of file