From 96f9228adafe546fed8e213b76f57ae3cc117c13 Mon Sep 17 00:00:00 2001 From: Dan Mezhiborsky <54985569+dmezh@users.noreply.github.com> Date: Tue, 6 Jun 2023 21:34:45 -0400 Subject: [PATCH 1/9] Add ember platformio scripts and partially decouple ember_bl from selfdrive --- ember-bltools/ember_app_desc.c | 16 +++ ember-pio-scripts/.gitignore | 1 + ember-pio-scripts/ember_identity.py | 27 ++++ ember-pio-scripts/ember_opencan.py | 26 ++++ ember-pio-scripts/ember_ota_target.py | 11 ++ ember_bl_updater.py | 171 ++++++++++++++++++++++++++ 6 files changed, 252 insertions(+) create mode 100644 ember-bltools/ember_app_desc.c create mode 100644 ember-pio-scripts/.gitignore create mode 100644 ember-pio-scripts/ember_identity.py create mode 100644 ember-pio-scripts/ember_opencan.py create mode 100644 ember-pio-scripts/ember_ota_target.py create mode 100644 ember_bl_updater.py diff --git a/ember-bltools/ember_app_desc.c b/ember-bltools/ember_app_desc.c new file mode 100644 index 0000000..0d2bbfe --- /dev/null +++ b/ember-bltools/ember_app_desc.c @@ -0,0 +1,16 @@ +#include "ember_app_desc.h" + +#ifndef EMBER_NODE_IDENTITY + #error "EMBER_NODE_IDENTITY must be defined" +#endif + +// stringification macros +#define XSTR(s) STR(s) +#define STR(s) #s + +const IN_DESC_SECTION ember_app_desc_v1_t ember_app_description = { + .ember_magic = EMBER_MAGIC, + .app_desc_version = EMBER_APP_DESC_VERSION, + + .node_identity = XSTR(EMBER_NODE_IDENTITY), +}; diff --git a/ember-pio-scripts/.gitignore b/ember-pio-scripts/.gitignore new file mode 100644 index 0000000..bee8a64 --- /dev/null +++ b/ember-pio-scripts/.gitignore @@ -0,0 +1 @@ +__pycache__ diff --git a/ember-pio-scripts/ember_identity.py b/ember-pio-scripts/ember_identity.py new file mode 100644 index 0000000..cb3c2e6 --- /dev/null +++ b/ember-pio-scripts/ember_identity.py @@ -0,0 +1,27 @@ +# Simple script to add a #define for EMBER_NODE_IDENTITY. + +from SCons.Script import ARGUMENTS + +def get_ember_identity(env): + identity = env.GetProjectOption('board_node_identity', default=None) + if identity is None: + if env.GetProjectOption('custom_ember_do_not_use_can_node_as_identity', default=0) != 0: + print("Error: board_node_identity must be set in platformio.ini - "\ + "(will not use board_can_node as identity because custom_ember_do_not_use_can_node_as_identity is set)") + exit(-1) + + identity = env.GetProjectOption('board_can_node', default=None) + if identity is None: + print("Error: board_node_identity or board_can_node must be set in platformio.ini") + exit(-1) + + if int(ARGUMENTS.get("PIOVERBOSE", 0)): + print(f"Using board_can_node as node identity: {identity}") + + env.Append(CPPDEFINES=[("EMBER_NODE_IDENTITY", f'{identity}')]) + + return identity + +if __name__ == "__main__": + Import('env') + get_ember_identity(env) diff --git a/ember-pio-scripts/ember_opencan.py b/ember-pio-scripts/ember_opencan.py new file mode 100644 index 0000000..1449d41 --- /dev/null +++ b/ember-pio-scripts/ember_opencan.py @@ -0,0 +1,26 @@ +Import("env") + +node = env.GetProjectOption("board_can_node") +yml = env.GetProjectOption("custom_opencan_yml") +opencan = env.GetProjectOption("custom_opencan_cli") +build_dir = env.Dir(env['BUILD_DIR']) +gen_dir = build_dir.Dir(f"../opencan_generated/{node}") + +# These Execute calls call opencan-cli every time. +# They could be replaced with a proper Command, but it was tricky getting +# PlatformIO to reliably execute them. You could ask them on GitHub or similar. +if 0 != env.Execute(f'scons -QD deps-opencan'): + print("Error making sure opencan-cli is installed; stopping build.") + exit(-1) + +env.Execute(Mkdir(gen_dir.abspath)) + +if 0 != env.Execute(f'{opencan} codegen {yml} {gen_dir} {node}'): + print("OpenCAN error; stopping build.") + exit(-1) + +# Add gen_dir to the CPPPATH +env.Prepend(CPPPATH=[gen_dir]) + +# Get build sources from gen_dir +env.BuildSources(build_dir.path, gen_dir.path) diff --git a/ember-pio-scripts/ember_ota_target.py b/ember-pio-scripts/ember_ota_target.py new file mode 100644 index 0000000..e54ad95 --- /dev/null +++ b/ember-pio-scripts/ember_ota_target.py @@ -0,0 +1,11 @@ +Import("env") + +from ember_identity import get_ember_identity + +identity = get_ember_identity(env) + +env.AddCustomTarget( + "ota", + "$BUILD_DIR/${PROGNAME}.bin", + f"python3 lib/ember/ember_bl_updater.py --bin $SOURCE --target {identity}" +) diff --git a/ember_bl_updater.py b/ember_bl_updater.py new file mode 100644 index 0000000..765281d --- /dev/null +++ b/ember_bl_updater.py @@ -0,0 +1,171 @@ +# spdx-license-identifier: MPL-2.0 +# ember_bl updater. +# (c) 2023 Daniel Mezhiborsky + +import argparse +import can +import coloredlogs +import datetime +import isotp +import logging + +from cand.client import Bus +from threading import Thread +from time import sleep +from tqdm import trange + +CHUNK_SIZE = 4095 +iface = 'can0' + +SLEEP_WAIT = 0.01 + +TARGET_NODE = 'THROTTLE' + +# current chunk; global and shared +chunk = 0 + +class TargetNode(): + def __init__(self, node: str, total_size: int): + self.bus = Bus() + self.log = logging.getLogger(f"target_node_{node}") + + self.node = node + self.total_size = total_size + + self.stop_set = False + + self.update_control_thread = Thread(target = self.update_control, daemon = True) + self.update_control_thread.start() + self.log.info("Started UpdateControl thread.") + + def stop(self) -> None: + self.stop_set = True + + def update_control(self) -> None: + while True: + if self.stop_set: + exit(0) + + signals = { + 'UPD_updateSizeBytes': self.total_size, + 'UPD_currentIsoTpChunk': chunk + } + + self.bus.send(f'UPD_UpdateControl_{self.node}', signals) + sleep(0.02) + + def state(self) -> str: + message = f'{self.node}BL_Status' + data = self.bus.get_data(message) + if data is None: + self.log.error(f"Missing {message} from cand... is the node present?") + exit(-1) + + return data[f'{self.node}BL_state'] + + def is_alive(self) -> bool: + message = f'{self.node}BL_Status' + dt = self.bus.get_time_delta(message) + + if dt is None: + return False + + ALIVE_DELTA_NS = 500 * 1000 * 1000 # 0.5 seconds + return dt <= ALIVE_DELTA_NS + + +def main(): + parser = argparse.ArgumentParser(description='Update a node over CAN.') + parser.add_argument('--iface', type=str, default='can0', help='CAN interface to use') + parser.add_argument('--target', type=str, metavar='THROTTLE', help='Target node to update', required=True) + parser.add_argument('--bin', type=str, metavar='firmware.bin', help='Firmware binary to send', required=True) + args = parser.parse_args() + + global chunk + + coloredlogs.install(level='info') + log = logging.getLogger('main') + + cand_bus = Bus() + isotp_tx_id = cand_bus.get_message_id(f'UPD_IsoTpTx_{args.target}') + isotp_rx_id = cand_bus.get_message_id(f'{args.target}BL_IsoTpTx') + print(f"Using isotp_tx_id {isotp_tx_id} and isotp_rx_id {isotp_rx_id}.") + + isotp_stack = isotp.CanStack( + can.interface.Bus('can0', bustype = 'socketcan'), + address = isotp.Address(isotp.AddressingMode.Normal_11bits, rxid=isotp_rx_id, txid=isotp_tx_id), + ) + log.info('Created isotp stack') + + firmware = open(args.bin, 'rb').read() + total_size = len(firmware) + log.info(f'Read firmware binary ({total_size} bytes).') + + node = TargetNode(args.target, total_size) + log.info(f'Tracking target node {node.node}') + + log.info(f'Waiting for {node.node}BL to come up...') + init_start = datetime.datetime.now() + while not node.is_alive(): + if datetime.datetime.now() - init_start > datetime.timedelta(seconds=10): + log.error(f"Node {node.node}BL did not come up. Exiting.") + exit(-1) + + sleep(SLEEP_WAIT) + + log.info(f"Waiting for node to be in RECV_CHUNK....") + while node.state() != 'RECV_CHUNK': + sleep(SLEEP_WAIT) + + sleep(SLEEP_WAIT) + + log.info('Begin sending firmware image.') + for i, _ in enumerate(trange(0, len(firmware), CHUNK_SIZE)): + chunk = i + + start = i * CHUNK_SIZE + end = min(start + CHUNK_SIZE, total_size) + + log.debug(f"Sending chunk::: {start}...{end}") + + chunk_data = firmware[start:end] + isotp_stack.send(chunk_data) + + + log.debug('Waiting for isotp stack to be done transmitting') + while isotp_stack.transmitting(): + isotp_stack.process() + + log.debug('Waiting for node to be in RECV_CHUNK again') + while True: + bl_state = node.state() + + if bl_state == 'RECV_CHUNK': + sleep(0.1) # give the node some time, but this shouldn't be needed + break + elif bl_state == 'FAULT': + log.error('Node is in FAULT; aborting.') + node.stop() + exit(-1) + elif bl_state not in ['COMMIT_CHUNK', 'FINALIZE', 'CHECK_DESC']: + log.error(f'Node in unexpected state {bl_state}; aborting.') + exit(-1) + else: + sleep(SLEEP_WAIT) + + # stop the updatecontrol transmission right now so we don't trigger another update + # once we're back in the firmware + node.stop() + + log.info('Update done; waiting for node to exit bootloader.') + while node.is_alive(): + if node.state() == 'FAULT': + log.error('Node is in FAULT; update might have failed') + exit(-1) + + log.info('Done!') + exit(0) + + +if __name__ == "__main__": + main() From a2775dd8cd5ab180c7b501a225d08800628d3927 Mon Sep 17 00:00:00 2001 From: Dan Mezhiborsky <54985569+dmezh@users.noreply.github.com> Date: Tue, 6 Jun 2023 21:41:50 -0400 Subject: [PATCH 2/9] Add EMBER_TASKING_DISABLE_WATCHDOG --- ember-tasking/watchdog.c | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/ember-tasking/watchdog.c b/ember-tasking/watchdog.c index 21478ad..c702143 100644 --- a/ember-tasking/watchdog.c +++ b/ember-tasking/watchdog.c @@ -40,11 +40,13 @@ static wdt_hal_context_t hal = { /* * Kick hardware watchdog. */ -static IRAM_ATTR void kick_rtc_watchdog() +static IRAM_ATTR void kick_rtc_watchdog(void) { +#ifndef EMBER_TASKING_DISABLE_WATCHDOG wdt_hal_write_protect_disable(&hal); wdt_hal_feed(&hal); wdt_hal_write_protect_enable(&hal); +#endif } // ###### PUBLIC FUNCTIONS ###### // @@ -139,6 +141,7 @@ void IRAM_ATTR task_wdt_servicer() */ void set_up_rtc_watchdog(uint32_t timeout_ms) { +#ifndef EMBER_TASKING_DISABLE_WATCHDOG wdt_hal_deinit(&hal); wdt_hal_init(&hal, hal.inst, 0, false); wdt_hal_write_protect_disable(&hal); @@ -150,6 +153,9 @@ void set_up_rtc_watchdog(uint32_t timeout_ms) wdt_hal_config_stage(&hal, WDT_STAGE0, stage_timeout_ticks, WDT_STAGE_ACTION_RESET_RTC); wdt_hal_enable(&hal); wdt_hal_write_protect_enable(&hal); +#else + (void)timeout_ms; +#endif } /* @@ -175,4 +181,4 @@ void set_up_rtc_watchdog_final() void set_up_rtc_watchdog_1sec() { set_up_rtc_watchdog(FW_UPDATE_TIMEOUT_MS); -} \ No newline at end of file +} From 3ce26a338eaadbd2337ca49ee9bc1498034f9449 Mon Sep 17 00:00:00 2001 From: Dan Mezhiborsky <54985569+dmezh@users.noreply.github.com> Date: Tue, 6 Jun 2023 22:09:16 -0400 Subject: [PATCH 3/9] Add ember-bl as pio library --- ember-bl/bl.c | 396 +++++++++++++++++++++++++++++++++++++++ ember-bl/entry.c | 23 +++ ember-bl/leds.c | 50 +++++ ember-bl/library.json | 8 + ember-bl/module_list.inc | 34 ++++ ember_bl_updater.py | 2 - 6 files changed, 511 insertions(+), 2 deletions(-) create mode 100644 ember-bl/bl.c create mode 100644 ember-bl/entry.c create mode 100644 ember-bl/leds.c create mode 100644 ember-bl/library.json create mode 100644 ember-bl/module_list.inc diff --git a/ember-bl/bl.c b/ember-bl/bl.c new file mode 100644 index 0000000..3a8e961 --- /dev/null +++ b/ember-bl/bl.c @@ -0,0 +1,396 @@ +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "ember_app_desc.h" +#include "ember_taskglue.h" +#include "isotp.h" +#include "isotp_user.h" +#include "opencan_callbacks.h" // for enqueue_tx_message +#include "opencan_rx.h" +#include "opencan_tx.h" + +// ###### DEFINES ###### // + +#define log(...) fprintf(stderr, __VA_ARGS__) + +#define ISOTP_CHUNK_SIZE 4095 + +enum bl_state { + BL_STATE_INIT, + BL_STATE_AWAIT_TRIGGER, + BL_STATE_RECV_CHUNK, + BL_STATE_CHECK_DESC, + BL_STATE_COMMIT_CHUNK, + BL_STATE_FINALIZE, + BL_STATE_REBOOT_FW, + BL_STATE_FAULT, + BL_STATE_RESET, +}; + +// ###### PROTOTYPES ###### // + +static void set_state(enum bl_state next_state); +static uint32_t time_in_state(void); +static bool app_desc_ok(const uint8_t * chunk_data); + +// ###### PRIVATE DATA ###### // + +static IsoTpLink isotp_link; +static uint8_t isotp_tx_buf[ISOTP_CHUNK_SIZE + 1]; +static uint8_t isotp_rx_buf[ISOTP_CHUNK_SIZE + 1]; +static uint8_t isotp_chunk_data[ISOTP_CHUNK_SIZE + 1]; + +static enum bl_state bl_state = BL_STATE_INIT; +static uint32_t state_start_time; + +const esp_partition_t *app_partition; + +// ###### CAN ###### // + +#define CANRX_ISOTP_CALLBACK_NAME_(IDENTITY) CANRX_onRxCallback_UPD_IsoTpTx_ ## IDENTITY +#define CANRX_ISOTP_CALLBACK_NAME(IDENTITY) CANRX_ISOTP_CALLBACK_NAME_(IDENTITY) + +void CANRX_ISOTP_CALLBACK_NAME(EMBER_NODE_IDENTITY)( + const uint8_t * const data, + const uint8_t len) +{ + isotp_on_can_message(&isotp_link, data, len); +} + +void CANTX_populateTemplate_Status(struct CAN_TMessage_BlStatus * const m) +{ + typeof(m->state) s; + + switch (bl_state) { + case BL_STATE_INIT: s = CAN_T_BLSTATUS_STATE_AWAIT_TRIGGER; break; + case BL_STATE_AWAIT_TRIGGER: s = CAN_T_BLSTATUS_STATE_AWAIT_TRIGGER; break; + case BL_STATE_RECV_CHUNK: s = CAN_T_BLSTATUS_STATE_RECV_CHUNK; break; + case BL_STATE_CHECK_DESC: s = CAN_T_BLSTATUS_STATE_CHECK_DESC; break; + case BL_STATE_COMMIT_CHUNK: s = CAN_T_BLSTATUS_STATE_COMMIT_CHUNK; break; + case BL_STATE_FINALIZE: s = CAN_T_BLSTATUS_STATE_FINALIZE; break; + case BL_STATE_REBOOT_FW: s = CAN_T_BLSTATUS_STATE_REBOOT_FW; break; + case BL_STATE_FAULT: s = CAN_T_BLSTATUS_STATE_FAULT; break; + case BL_STATE_RESET: s = CAN_T_BLSTATUS_STATE_RESET; break; + default: s = CAN_T_BLSTATUS_STATE_RESET; break; + } + + m->state = s; +} + +// ###### RATE FUNCTIONS ###### // + +static void bl_init(void); +static void bl_step(void); +static void bl_loop(void *unused); + +const ember_rate_funcs_S bl_rf = { + .call_init = bl_init, + // .call_1kHz = bl_1kHz, +}; + +static void bl_init(void) { + set_state(BL_STATE_INIT); + + log("^^^ EMBER BOOTLOADER v0.1.0 ^^^\n"); + log("--> This bootloader image's node identity is: %s\n", ember_app_description.node_identity); + log("*** Looking for app partition...\n"); + + // Find the app partition. + app_partition = + esp_partition_find_first(ESP_PARTITION_TYPE_APP, ESP_PARTITION_SUBTYPE_APP_OTA_0, NULL); + if (!app_partition) { + log("!!! Failed to find app partition.\n"); + set_state(BL_STATE_RESET); + return; + } else { + log("--> Found app partition.\n"); + // esp_ota_get_state_partition not working... + log("*** Getting app information...\n"); + + esp_ota_img_states_t app_state; + + const esp_err_t ret = + esp_ota_get_state_partition(app_partition, &app_state); + + if (ret == ESP_OK) { + log("*** App is in state %d\n.", app_state); + } else { + log("!!! Couldn't get app partition state: %s\n", esp_err_to_name(ret)); + } + } + + #define CANRX_ISOTP_TX_ID_(IDENTITY) CAN_MSG_ ## IDENTITY ## BL_IsoTpTx_ID + #define CANRX_ISOTP_TX_ID(IDENTITY) CANRX_ISOTP_TX_ID_(IDENTITY) + + isotp_init_link( + &isotp_link, + CANRX_ISOTP_TX_ID(EMBER_NODE_IDENTITY), + isotp_tx_buf, + sizeof(isotp_tx_buf), + isotp_rx_buf, + sizeof(isotp_rx_buf) + ); + + static TaskHandle_t bl_loop_handle; + xTaskCreatePinnedToCore(bl_loop, "BL_LOOP", 8192, NULL, 3, &bl_loop_handle, 1); +} + +#define ESP_CHECKED(call) \ +{ \ + const esp_err_t err = (call); \ + if (err != ESP_OK) { \ + log( \ + "!!! Got error in checked op `%s` (err = %d, %s)\n", \ + #call, err, esp_err_to_name(err)); \ + next_state = BL_STATE_FAULT; \ + break; \ + } \ +} \ + +static void bl_step(void) { + static esp_ota_handle_t ota_handle = 0; + static uint32_t update_size = 0; + static uint32_t bytes_so_far = 0; + + enum bl_state next_state = bl_state; + switch (bl_state) { + case BL_STATE_INIT: + next_state = BL_STATE_AWAIT_TRIGGER; + break; + + case BL_STATE_AWAIT_TRIGGER: + if (CANRX_is_node_UPD_ok() && CANRX_get_UPD_currentIsoTpChunk() == 0U) { + log("--> UPD is present; update triggered.\n"); + + // Note the total number of bytes + update_size = CANRX_get_UPD_updateSizeBytes(); + + // Begin OTA update + ESP_CHECKED(esp_ota_begin(app_partition, OTA_SIZE_UNKNOWN, &ota_handle)); + + next_state = BL_STATE_RECV_CHUNK; + } else if (time_in_state() > 1500U) { + log("--> UPD not detected, skipping update.\n"); + next_state = BL_STATE_REBOOT_FW; + } + break; + + case BL_STATE_RECV_CHUNK: + /* Take care of ISOTP */ + isotp_poll(&isotp_link); + + static uint16_t this_chunk_size; + const int isotp_ret = isotp_receive(&isotp_link, isotp_chunk_data, sizeof(isotp_chunk_data), &this_chunk_size); + + static uint16_t current_chunk; + if (isotp_ret == ISOTP_RET_OK) { + log("--> Got new ISOTP full message with size %"PRIu16": %"PRIu16"\n", this_chunk_size, current_chunk); + } + /**********************/ + + if (isotp_ret == ISOTP_RET_OK) { // we got a chunk + // Does our chunk count match the UPD chunk count? + uint16_t upd_chunk = CANRX_get_UPD_currentIsoTpChunk(); + if (upd_chunk != current_chunk) { + log("!!! Chunk count mismatch: UPD is on chunk %"PRIu16" and we're on %"PRIu16"\n", + upd_chunk, current_chunk); + next_state = BL_STATE_FAULT; + } + + bytes_so_far += this_chunk_size; + + if (current_chunk == 0) { + next_state = BL_STATE_CHECK_DESC; + } else { + next_state = BL_STATE_COMMIT_CHUNK; + } + } else if (time_in_state() > 1500U) { // expected chunk but didn't get one + log("!!! Expected isotp chunk but didn't get one.\n"); + next_state = BL_STATE_FAULT; + } + break; + + case BL_STATE_CHECK_DESC:; + if (app_desc_ok(isotp_chunk_data)) { + next_state = BL_STATE_COMMIT_CHUNK; + } else { + log("!!! ember_app_desc check failed. Aborting.\n"); + next_state = BL_STATE_FAULT; + } + + break; + + case BL_STATE_COMMIT_CHUNK: + // Let's write it in. + log("--> Committing chunk %d...\n", current_chunk); + ESP_CHECKED(esp_ota_write(ota_handle, isotp_chunk_data, this_chunk_size)); + + if (bytes_so_far < update_size) { + current_chunk++; + next_state = BL_STATE_RECV_CHUNK; + } else if (bytes_so_far == update_size) { + next_state = BL_STATE_FINALIZE; + } else { + log("Got more update data than expected: we got %"PRIu32" total bytes so far," + " but expected %"PRIu32" total.\n", bytes_so_far, update_size); + next_state = BL_STATE_FAULT; + } + break; + + case BL_STATE_FINALIZE: + // Finalize the update + log("--> Finalizing update...\n"); + ESP_CHECKED(esp_ota_end(ota_handle)); + next_state = BL_STATE_REBOOT_FW; + break; + + case BL_STATE_REBOOT_FW: + log("--> Setting boot partition to app...\n"); + ESP_CHECKED(esp_ota_set_boot_partition(app_partition)); + + next_state = BL_STATE_RESET; + break; + + case BL_STATE_FAULT: + // just linger for a little bit and then reset. + if (time_in_state() > 1000U) { + next_state = BL_STATE_RESET; + } + + break; + + case BL_STATE_RESET: + log("*** Resetting NOW ***\n"); + esp_restart(); + + break; // should be unreachable + + default: + log("!!! Invalid state %d\n", bl_state); + next_state = BL_STATE_RESET; + break; + } + + if (next_state != bl_state) + set_state(next_state); +} + +static void bl_loop(void *unused) { + (void)unused; + + for (;;) { + bl_step(); + vTaskDelay(pdMS_TO_TICKS(1)); + } +} + +// ###### PRIVATE FUNCTIONS ###### // + +static void set_state(const enum bl_state next_state) { + bl_state = next_state; + state_start_time = esp_timer_get_time() / (int64_t)1000; + + const char *s = "INVALID"; + + switch (next_state) { + case BL_STATE_INIT: s = "INIT"; break; + case BL_STATE_AWAIT_TRIGGER: s = "AWAIT_TRIGGER"; break; + case BL_STATE_RECV_CHUNK: s = "RECV_CHUNK"; break; + case BL_STATE_CHECK_DESC: s = "CHECK_DESC"; break; + case BL_STATE_COMMIT_CHUNK: s = "COMMIT_CHUNK"; break; + case BL_STATE_FINALIZE: s = "FINALIZE"; break; + case BL_STATE_REBOOT_FW: s = "REBOOT_FW"; break; + case BL_STATE_RESET: s = "RESET"; break; + case BL_STATE_FAULT: s = "FAULT"; break; + } + + log("--> At %"PRIu32": next state is %s\n", state_start_time, s); +} + +static uint32_t time_in_state(void) { + return (esp_timer_get_time() / (int64_t)1000) - state_start_time; +} + +static bool app_desc_ok(const uint8_t * const chunk_data) { + // get the app description out of the first chunk + const void * const new_app_desc_addr = + // see ember_app_desc.h + chunk_data + + sizeof(esp_image_header_t) + + sizeof(esp_image_segment_header_t) + + sizeof(esp_app_desc_t); + + ember_app_desc_v1_t new_app_desc; + memcpy(&new_app_desc, new_app_desc_addr, sizeof(new_app_desc)); + + // check ember_magic + const bool ember_magic_matches = !strncmp( + EMBER_MAGIC, + new_app_desc.ember_magic, + sizeof(EMBER_MAGIC) + ); + + if (!ember_magic_matches) { + _Static_assert(sizeof(EMBER_MAGIC) == 8, "Need EMBER_MAGIC to be 8 chars for log below"); + log("!!! Invalid ember_magic for new app's ember_app_desc: \"%.8s\".\n", new_app_desc.ember_magic); + return false; + } + + // check app_desc_version + if (new_app_desc.app_desc_version != EMBER_APP_DESC_VERSION) { + log("!!! Unexpected version %"PRIu16" (expected %"PRIu16") for new app's app_desc_version.\n", + new_app_desc.app_desc_version, + EMBER_APP_DESC_VERSION + ); + return false; + } + + // check that node_identity matches ours + const bool identities_match = !strncmp( + ember_app_description.node_identity, + new_app_desc.node_identity, + sizeof(ember_app_description.node_identity)); + + if (identities_match) { + log("--> Identity of new app matches bootloader. Proceeding.\n"); + } else { + log("--> Identity of new app (\"%.16s\") does not match bootloader!\n", new_app_desc.node_identity); + return false; + } + + return true; +} + +// ###### PUBLIC FUNCTIONS ###### // + +void isotp_user_debug(const char* message, ...) { + va_list args; + va_start(args, message); + fprintf(stderr, "*** ISOTP Says: "); + vfprintf(stderr, message, args); + fprintf(stderr, "\n"); + va_end(args); +} + +int isotp_user_send_can( + const uint32_t arbitration_id, + const uint8_t * const data, + const uint8_t size) +{ + CAN_callback_enqueue_tx_message(data, size, arbitration_id); + + return ISOTP_RET_OK; +} + +uint32_t isotp_user_get_ms(void) { + return esp_timer_get_time() / (int64_t)1000; +} diff --git a/ember-bl/entry.c b/ember-bl/entry.c new file mode 100644 index 0000000..264bccc --- /dev/null +++ b/ember-bl/entry.c @@ -0,0 +1,23 @@ +#include +#include + +#include + +#include "ember_tasking.h" +#include "module_list.inc" + +#include "ember_can_callbacks.h" + +void ember_can_callback_notify_lost_can() { + /* stub */ +} + +void app_main() { + /* begin running tasks */ + ember_tasking_begin(); + + // for (;;) { + // printf("Hello :)\n"); + // vTaskDelay(200 / portTICK_PERIOD_MS); + // } +} diff --git a/ember-bl/leds.c b/ember-bl/leds.c new file mode 100644 index 0000000..44dd460 --- /dev/null +++ b/ember-bl/leds.c @@ -0,0 +1,50 @@ +#include + +#include "ember_taskglue.h" +#include "node_pins.h" + +// ###### DEFINES ###### // + +#define LED1_PIN NODE_BOARD_PIN_LED1 +#define LED2_PIN NODE_BOARD_PIN_LED2 + +// ###### PROTOTYPES ###### // + +static void leds_init(void); +static void leds_10Hz(void); + +// ###### PRIVATE DATA ###### // + +// ###### CAN ###### // + +// ###### RATE FUNCTIONS ###### // + +ember_rate_funcs_S leds_rf = { + .call_init = leds_init, + .call_10Hz = leds_10Hz, +}; + +static void leds_init(void) { + gpio_config(&(gpio_config_t){ + .pin_bit_mask = BIT64(LED1_PIN) | BIT64(LED2_PIN), + .mode = GPIO_MODE_OUTPUT, + }); + + gpio_set_level(LED1_PIN, 0); + gpio_set_level(LED2_PIN, 0); +} + +static void leds_10Hz(void) { + static bool led1; + static bool led2; + + led1 = !led1; + led2 = !led2; + + gpio_set_level(LED1_PIN, led1); + gpio_set_level(LED2_PIN, led2); +} + +// ###### PRIVATE FUNCTIONS ###### // + +// ###### PUBLIC FUNCTIONS ###### // diff --git a/ember-bl/library.json b/ember-bl/library.json new file mode 100644 index 0000000..08700ed --- /dev/null +++ b/ember-bl/library.json @@ -0,0 +1,8 @@ +{ + "name": "ember-bl", + "version": "1.0.0", + "description": "Ember CAN Bootloader", + "build": { + "libLDFMode": "deep" + } +} diff --git a/ember-bl/module_list.inc b/ember-bl/module_list.inc new file mode 100644 index 0000000..025c756 --- /dev/null +++ b/ember-bl/module_list.inc @@ -0,0 +1,34 @@ +/* + * this is a logical part of entry.c. It is not meant to be included anywhere + * else and does not have include guards. + */ + +#include "ember_common.h" +#include "ember_taskglue.h" + +/* + * List an extern definition for each module's struct rate_tasks. + */ +extern ember_rate_funcs_S leds_rf; +extern ember_rate_funcs_S can_rf; +extern ember_rate_funcs_S bl_rf; + +/* + * Single extern definition for the one-per-build mod/ module. + */ +extern ember_rate_funcs_S module_rf; + +/* + * List of references to the task structs. Order matters - the modules will be + * initialized in the order they appear here. + * + * The functions within each rate (1Hz, 10Hz, etc) will also run in the order + * they appear here, but it's best not to rely on that fact for program logic. + */ +ember_rate_funcs_S* ember_task_list[] = { + &leds_rf, + &can_rf, + &bl_rf, +}; + +const size_t ember_task_count = ARRAY_SIZE(ember_task_list); diff --git a/ember_bl_updater.py b/ember_bl_updater.py index 765281d..8848f0e 100644 --- a/ember_bl_updater.py +++ b/ember_bl_updater.py @@ -19,8 +19,6 @@ SLEEP_WAIT = 0.01 -TARGET_NODE = 'THROTTLE' - # current chunk; global and shared chunk = 0 From dc57e61274e9623f87f45a467f3f470a9727c69a Mon Sep 17 00:00:00 2001 From: Dan Mezhiborsky <54985569+dmezh@users.noreply.github.com> Date: Wed, 7 Jun 2023 01:16:31 -0400 Subject: [PATCH 4/9] Fix some extra_script bugs and have ota listen to --upload-port --- ember-pio-scripts/ember_define_identity.py | 7 +++++++ ember-pio-scripts/ember_identity.py | 8 -------- ember-pio-scripts/ember_ota_target.py | 7 ++++++- 3 files changed, 13 insertions(+), 9 deletions(-) create mode 100644 ember-pio-scripts/ember_define_identity.py diff --git a/ember-pio-scripts/ember_define_identity.py b/ember-pio-scripts/ember_define_identity.py new file mode 100644 index 0000000..eadc1ff --- /dev/null +++ b/ember-pio-scripts/ember_define_identity.py @@ -0,0 +1,7 @@ +# Simple script to #define EMBER_NODE_IDENTITY +from ember_identity import get_ember_identity + +Import("env") + +identity = get_ember_identity(env) +env.Append(CPPDEFINES=[("EMBER_NODE_IDENTITY", f'{identity}')]) diff --git a/ember-pio-scripts/ember_identity.py b/ember-pio-scripts/ember_identity.py index cb3c2e6..934f53c 100644 --- a/ember-pio-scripts/ember_identity.py +++ b/ember-pio-scripts/ember_identity.py @@ -1,5 +1,3 @@ -# Simple script to add a #define for EMBER_NODE_IDENTITY. - from SCons.Script import ARGUMENTS def get_ember_identity(env): @@ -18,10 +16,4 @@ def get_ember_identity(env): if int(ARGUMENTS.get("PIOVERBOSE", 0)): print(f"Using board_can_node as node identity: {identity}") - env.Append(CPPDEFINES=[("EMBER_NODE_IDENTITY", f'{identity}')]) - return identity - -if __name__ == "__main__": - Import('env') - get_ember_identity(env) diff --git a/ember-pio-scripts/ember_ota_target.py b/ember-pio-scripts/ember_ota_target.py index e54ad95..d8ae0f4 100644 --- a/ember-pio-scripts/ember_ota_target.py +++ b/ember-pio-scripts/ember_ota_target.py @@ -4,8 +4,13 @@ identity = get_ember_identity(env) +# https://community.platformio.org/t/access-upload-port-from-platformio-ini-from-extra-script-py/1350/3 +can_dev = env.get("UPLOAD_PORT") +if can_dev is None: + can_dev = "can0" + env.AddCustomTarget( "ota", "$BUILD_DIR/${PROGNAME}.bin", - f"python3 lib/ember/ember_bl_updater.py --bin $SOURCE --target {identity}" + f"python3 lib/ember/ember_bl_updater.py --bin $SOURCE --target {identity} --iface {can_dev}" ) From df9a03fdc66f08c4f1f6810ecf2b55e74883677e Mon Sep 17 00:00:00 2001 From: Dan Mezhiborsky <54985569+dmezh@users.noreply.github.com> Date: Wed, 7 Jun 2023 09:23:25 -0400 Subject: [PATCH 5/9] Add isotp as ember-bl dependency --- ember-bl/library.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ember-bl/library.json b/ember-bl/library.json index 08700ed..c51c005 100644 --- a/ember-bl/library.json +++ b/ember-bl/library.json @@ -4,5 +4,8 @@ "description": "Ember CAN Bootloader", "build": { "libLDFMode": "deep" + }, + "dependencies": { + "isotp": "https://github.com/dmezh/isotp-c.git#f5add4e6" } } From 9cbfe2e73fd2b050c53f6b0fc61cd8488a24dddb Mon Sep 17 00:00:00 2001 From: Dan Mezhiborsky <54985569+dmezh@users.noreply.github.com> Date: Wed, 7 Jun 2023 11:16:17 -0400 Subject: [PATCH 6/9] Actually use iface arg in ember_bl_updater.py --- ember_bl_updater.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/ember_bl_updater.py b/ember_bl_updater.py index 8848f0e..912fc64 100644 --- a/ember_bl_updater.py +++ b/ember_bl_updater.py @@ -15,7 +15,6 @@ from tqdm import trange CHUNK_SIZE = 4095 -iface = 'can0' SLEEP_WAIT = 0.01 @@ -90,7 +89,7 @@ def main(): print(f"Using isotp_tx_id {isotp_tx_id} and isotp_rx_id {isotp_rx_id}.") isotp_stack = isotp.CanStack( - can.interface.Bus('can0', bustype = 'socketcan'), + can.interface.Bus(args.iface, bustype = 'socketcan'), address = isotp.Address(isotp.AddressingMode.Normal_11bits, rxid=isotp_rx_id, txid=isotp_tx_id), ) log.info('Created isotp stack') From f20f988487757340ff8f65b8a462f41231927d08 Mon Sep 17 00:00:00 2001 From: Dan Mezhiborsky <54985569+dmezh@users.noreply.github.com> Date: Wed, 7 Jun 2023 11:35:02 -0400 Subject: [PATCH 7/9] Add ember_bl_servicing --- ember-bltools/ember_bl_servicing.c | 46 ++++++++++++++++++++++++++++++ ember-bltools/ember_bl_servicing.h | 11 +++++++ 2 files changed, 57 insertions(+) create mode 100644 ember-bltools/ember_bl_servicing.c create mode 100644 ember-bltools/ember_bl_servicing.h diff --git a/ember-bltools/ember_bl_servicing.c b/ember-bltools/ember_bl_servicing.c new file mode 100644 index 0000000..5251259 --- /dev/null +++ b/ember-bltools/ember_bl_servicing.c @@ -0,0 +1,46 @@ +/* + * Servicing for ember_bl. + * + * At the moment, this just helps reboot the node when an update starts running. + * + * 1. Add ember_bl_servicing_rf to your ember_task_list and have the correct CAN definitions. + * 2. Provide the callbacks listed in ember_bl_servicing.h. + * 3. Node should reboot when update comes in and it's safe to do so. +*/ + +#include "ember_bl_servicing.h" + +#include +#include + +#include +#include +#include + +static void bl_servicing_10Hz(void); + +ember_rate_funcs_S ember_bl_servicing_rf = { + .call_10Hz = bl_servicing_10Hz, +}; + +static void bl_servicing_10Hz(void) { + /* Critical section */ + portDISABLE_INTERRUPTS(); + { + if (CANRX_is_node_UPD_ok() && CANRX_getRaw_UPD_currentIsoTpChunk() == 0U) { + if (ember_bl_servicing_cb_are_we_ready_to_reboot()) { + /* It's update time. No going back; we will reboot. */ + + /** + * Set the RTC watchdog timeout to 1 second to give us some time + * since the task_wdt_servicer() is not running anymore. + */ + set_up_rtc_watchdog_1sec(); + + /* Reboot */ + esp_restart(); + } + } + } + portENABLE_INTERRUPTS(); +} diff --git a/ember-bltools/ember_bl_servicing.h b/ember-bltools/ember_bl_servicing.h new file mode 100644 index 0000000..67a3e96 --- /dev/null +++ b/ember-bltools/ember_bl_servicing.h @@ -0,0 +1,11 @@ +#pragma once + +#include + +/* + * Code should provide this callback to tell ember_bl_servicing + * whether the node is ready to reboot. + * + * This is called in a critical section with interrupts disabled. +*/ +bool ember_bl_servicing_cb_are_we_ready_to_reboot(void); From 5f9d53e6344bc36e3dd3b5a7271f5e40d32a0a36 Mon Sep 17 00:00:00 2001 From: Dan Mezhiborsky <54985569+dmezh@users.noreply.github.com> Date: Wed, 7 Jun 2023 13:05:25 -0400 Subject: [PATCH 8/9] Add ember_install_pydeps.py --- ember-pio-scripts/ember_install_pydeps.py | 5 +++++ ember-pio-scripts/requirements.txt | 2 ++ 2 files changed, 7 insertions(+) create mode 100644 ember-pio-scripts/ember_install_pydeps.py create mode 100644 ember-pio-scripts/requirements.txt diff --git a/ember-pio-scripts/ember_install_pydeps.py b/ember-pio-scripts/ember_install_pydeps.py new file mode 100644 index 0000000..9472682 --- /dev/null +++ b/ember-pio-scripts/ember_install_pydeps.py @@ -0,0 +1,5 @@ +# https://docs.platformio.org/en/stable/scripting/examples/extra_python_packages.html + +Import("env") + +env.Execute("pip3 install -q -r lib/ember/ember-pio-scripts/requirements.txt") diff --git a/ember-pio-scripts/requirements.txt b/ember-pio-scripts/requirements.txt new file mode 100644 index 0000000..a327409 --- /dev/null +++ b/ember-pio-scripts/requirements.txt @@ -0,0 +1,2 @@ +can-isotp==1.8 +coloredlogs==15.0.1 From 5f9435c220c6ae84679c91e833be1e8a405b5d5f Mon Sep 17 00:00:00 2001 From: Dan Mezhiborsky Date: Wed, 7 Jun 2023 13:42:46 -0400 Subject: [PATCH 9/9] Add some missing requirements --- ember-pio-scripts/requirements.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ember-pio-scripts/requirements.txt b/ember-pio-scripts/requirements.txt index a327409..f8830a4 100644 --- a/ember-pio-scripts/requirements.txt +++ b/ember-pio-scripts/requirements.txt @@ -1,2 +1,4 @@ can-isotp==1.8 coloredlogs==15.0.1 +opencan-cand==0.1.2 +tqdm==4.65.0