feat(uavcan): flash firmware onto CAN nodes from SD card (#27043)

Place firmware .bin files at the SD card root or staging directory
(/fs/microsd/ufw_staging/); on boot the UAVCAN server migrates them
to /fs/microsd/ufw/ and updates FW.db, then flashes any connected
node whose firmware version mismatches.

- Add firmware migration from SD root and staging dir into /fs/microsd/ufw/
- Maintain FW.db flat-file database mapping board IDs to original filenames
- Use cache-aligned DMA-safe read/write buffers (required on STM32H7)
- Add Tools/auterion/remote_update_fmu.sh for SSH-based FMU+canio updates
This commit is contained in:
Phil-Engljaehringer
2026-05-18 16:19:25 +02:00
committed by GitHub
parent 11fa1dae1e
commit aa7c66e0a7
7 changed files with 191 additions and 16 deletions
+8
View File
@@ -150,6 +150,14 @@ fi
if [ $USE_ALT_UPDATE_DIRS = yes ]
then
# Check for new CAN-firmware directory
if [ -e /fs/microsd/ufw_staging_tmp ]
then
echo "Updating CAN firmware staging directory"
rm -r /fs/microsd/ufw_staging
mv /fs/microsd/ufw_staging_tmp /fs/microsd/ufw_staging
fi
# Check for an update of the ext_autostart folder, and replace the old one with it
if [ -e /fs/microsd/ext_autostart_new ]
then
+37 -4
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env bash
# Flash PX4 to a device running AuterionOS in the local network
if [ "$1" == "-h" ] || [ "$1" == "--help" ] || [ $# -lt 2 ]; then
echo "Usage: $0 -f <firmware.px4|.elf> [-c <configuration_dir>] -d <IP/Device> [-u <user>] [-p <ssh_port>] [--revert]"
echo "Usage: $0 -f <firmware.px4|.elf> [-c <configuration_dir>] [-x <canio_firmware.bin>] -d <IP/Device> [-u <user>] [-p <ssh_port>] [--revert]"
exit 1
fi
@@ -9,7 +9,7 @@ ssh_port=22
ssh_user=root
ssh_opts="-o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no"
while getopts ":f:c:d:p:u:r" opt; do
while getopts ":f:c:x:d:p:u:r" opt; do
case ${opt} in
f )
if [ -n "$OPTARG" ]; then
@@ -27,6 +27,14 @@ while getopts ":f:c:d:p:u:r" opt; do
exit 1
fi
;;
x )
if [ -f "$OPTARG" ]; then
external_firmware_files+=("$OPTARG")
else
echo "ERROR: -x requires a valid firmware file path."
exit 1
fi
;;
d )
if [ "$OPTARG" ]; then
device="$OPTARG"
@@ -74,12 +82,23 @@ else
tmp_dir="$(mktemp -d)"
config_path=""
firmware_path=""
ufw_path=""
if [ -d "$config_dir" ]; then
cp -r "$config_dir" "$tmp_dir/config"
config_path=config
fi
if [ ${#external_firmware_files[@]} -gt 0 ]; then
mkdir -p "$tmp_dir/ufw"
for _ext_fw in "${external_firmware_files[@]}"; do
external_firmware_name="$(basename "$_ext_fw")"
cp "$_ext_fw" "$tmp_dir/ufw/$external_firmware_name"
echo "Including external firmware in update-dev.tar: ufw/$external_firmware_name"
done
ufw_path="ufw"
fi
if [ -f "$firmware_file" ]; then
extension="${firmware_file##*.}"
cp "$firmware_file" "$tmp_dir/firmware.$extension"
@@ -92,7 +111,7 @@ else
pushd "$tmp_dir" &>/dev/null
if [ -z $firmware_path ] && [ -z $config_path ]; then
if [ -z "$firmware_path" ] && [ -z "$config_path" ] && [ -z "$ufw_path" ]; then
exit 1
fi
@@ -103,7 +122,21 @@ else
tar_name="gtar"
fi
$tar_name -C "$tmp_dir" --sort=name --owner=root:0 --group=root:0 --mtime='2019-01-01 00:00:00' -cvf $target_file_name $firmware_path $config_path
tar_entries=()
if [ -n "$firmware_path" ]; then
tar_entries+=("$firmware_path")
fi
if [ -n "$config_path" ]; then
tar_entries+=("$config_path")
fi
if [ -n "$ufw_path" ]; then
tar_entries+=("$ufw_path")
fi
if ! $tar_name -C "$tmp_dir" --sort=name --owner=root:0 --group=root:0 --mtime='2019-01-01 00:00:00' -cvf $target_file_name "${tar_entries[@]}"; then
echo "ERROR: Failed to create $target_file_name"
exit 1
fi
# send it to the target to start flashing
scp $ssh_opts -P $ssh_port "$target_file_name" $ssh_user@"$device":$target_dir
+7
View File
@@ -4,6 +4,7 @@ DIR="$(dirname $(readlink -f $0))"
DEFAULT_AUTOPILOT_HOST=10.41.1.1
DEFAULT_AUTOPILOT_PORT=33333
DEFAULT_AUTOPILOT_USER=auterion
EXTERNAL_FIRMWARE_FILES=()
for i in "$@"
do
@@ -26,6 +27,9 @@ do
--wifi)
DEFAULT_AUTOPILOT_HOST=10.41.0.1
;;
--ext-fw=*)
EXTERNAL_FIRMWARE_FILES+=("${i#*=}")
;;
*)
# unknown option
;;
@@ -42,6 +46,9 @@ ARGUMENTS+=(-d "$AUTOPILOT_HOST")
ARGUMENTS+=(-p "$AUTOPILOT_PORT")
ARGUMENTS+=(-u "$AUTOPILOT_USER")
ARGUMENTS+=(${PX4_BINARY_FILE:+-f "$PX4_BINARY_FILE"})
for _ext_fw in "${EXTERNAL_FIRMWARE_FILES[@]}"; do
ARGUMENTS+=(-x "$_ext_fw")
done
ARGUMENTS+=($REVERT_AUTOPILOT_ARGUMENT)
echo "Flashing $AUTOPILOT_HOST ..."
@@ -94,7 +94,7 @@ protected:
* @return True - the class will begin sending update requests.
* False - the node will be ignored, no request will be sent.
*/
virtual bool shouldRequestFirmwareUpdate(uavcan::NodeID,
virtual bool shouldRequestFirmwareUpdate(uavcan::NodeID node_id,
const uavcan::protocol::GetNodeInfo::Response &node_info,
FirmwareFilePath &out_firmware_file_path)
{
@@ -174,6 +174,10 @@ protected:
return true;
}
static constexpr unsigned _MaxChunk = 512 / sizeof(uint64_t);
inline static uint64_t _chunk[_MaxChunk]
px4_cache_aligned_data() = {};
public:
struct AppDescriptor {
uavcan::uint8_t signature[sizeof(uavcan::uint64_t)];
@@ -193,9 +197,6 @@ public:
static int getFileInfo(const char *path, AppDescriptor &descriptor, int limit = 0)
{
using namespace std;
const unsigned MaxChunk = 512 / sizeof(uint64_t);
// Make sure this does not present as a valid descriptor
struct {
union {
@@ -206,14 +207,13 @@ public:
} s;
int rv = -ENOENT;
uint64_t chunk[MaxChunk];
int fd = open(path, O_RDONLY);
if (fd >= 0) {
AppDescriptor *pdescriptor = UAVCAN_NULLPTR;
while (pdescriptor == UAVCAN_NULLPTR && limit >= 0) {
int len = read(fd, chunk, sizeof(chunk));
int len = read(fd, _chunk, sizeof(_chunk));
if (len == 0) {
break;
@@ -224,10 +224,10 @@ public:
goto out_close;
}
uint64_t *p = &chunk[0];
uint64_t *p = &_chunk[0];
if (limit > 0) {
limit -= sizeof(chunk);
limit -= sizeof(_chunk);
}
do {
@@ -237,7 +237,7 @@ public:
rv = 0;
break;
}
} while (p++ <= &chunk[MaxChunk - (sizeof(AppDescriptor) / sizeof(chunk[0]))]);
} while (p++ <= &_chunk[_MaxChunk - (sizeof(AppDescriptor) / sizeof(_chunk[0]))]);
}
out_close:
+1
View File
@@ -51,6 +51,7 @@
#define UAVCAN_MAX_PATH_LENGTH (128 + 40)
#define UAVCAN_SD_ROOT_PATH CONFIG_BOARD_ROOT_PATH "/"
#define UAVCAN_FIRMWARE_PATH UAVCAN_SD_ROOT_PATH"ufw"
#define UAVCAN_SD_STAGING_PATH UAVCAN_SD_ROOT_PATH"ufw_staging/"
#define UAVCAN_ROMFS_FW_PATH "/etc/uavcan/fw"
#define UAVCAN_ROMFS_FW_PREFIX "_"
+126 -3
View File
@@ -56,6 +56,12 @@
#include <uavcan_posix/dynamic_node_id_server/file_storage_backend.hpp>
#include <uavcan_posix/firmware_version_checker.hpp>
static constexpr size_t FW_DB_LINE_SIZE = 256; // key(~15) + '=' + filename(NAME_MAX) + '\n'
static const char FW_DB_PATH[] = UAVCAN_FIRMWARE_PATH "/FW.db";
static const char FW_DB_TMP_PATH[] = UAVCAN_FIRMWARE_PATH "/FW.db.tmp";
static uint8_t _buffer[512]
px4_cache_aligned_data() = {};
/**
* @file uavcan_servers.cpp
*
@@ -136,12 +142,20 @@ int UavcanServers::init()
return ret;
}
/* Check if all entries in the firmware database are still valid */
validateFwDatabase();
/*
Check for firmware in the root directory, move it to appropriate location on
the SD card, as defined by the APDesc.
*/
migrateFWFromRoot(UAVCAN_FIRMWARE_PATH, UAVCAN_SD_ROOT_PATH);
/*
Check for firmware in the staging directory. Using a staging dir avoids concurrent write conflicts.
*/
migrateFWFromRoot(UAVCAN_FIRMWARE_PATH, UAVCAN_SD_STAGING_PATH);
/* Start the Node */
return 0;
}
@@ -222,6 +236,7 @@ void UavcanServers::migrateFWFromRoot(const char *sd_path, const char *sd_root_p
snprintf(dstpath, sizeof(dstpath), "%s/%d.bin", sd_path, descriptor.board_id);
if (copyFw(dstpath, srcpath) >= 0) {
updateFwDatabase(dstpath, bin_names[i]);
unlink(srcpath);
}
}
@@ -230,7 +245,6 @@ void UavcanServers::migrateFWFromRoot(const char *sd_path, const char *sd_root_p
int UavcanServers::copyFw(const char *dst, const char *src)
{
int rv = 0;
uint8_t buffer[512] {};
int dfd = open(dst, O_WRONLY | O_CREAT, 0666);
@@ -250,7 +264,7 @@ int UavcanServers::copyFw(const char *dst, const char *src)
ssize_t size = 0;
do {
size = read(sfd, buffer, sizeof(buffer));
size = read(sfd, _buffer, sizeof(_buffer));
if (size < 0) {
PX4_ERR("copyFw: couldn't read");
@@ -263,7 +277,7 @@ int UavcanServers::copyFw(const char *dst, const char *src)
ssize_t written = 0;
do {
written = write(dfd, &buffer[total_written], remaining);
written = write(dfd, &_buffer[total_written], remaining);
if (written < 0) {
PX4_ERR("copyFw: couldn't write");
@@ -282,3 +296,112 @@ int UavcanServers::copyFw(const char *dst, const char *src)
return rv;
}
void UavcanServers::updateFwDatabase(const char *fw_path, const char *original_filename)
{
// DB key is the filename portion of fw_path, e.g. "12345.bin"
const char *last_slash = strrchr(fw_path, '/');
const char *new_filename = last_slash ? (last_slash + 1) : fw_path;
// The new/updated DB entry, e.g. "122.bin=122-1.17.63eeff1a.uavcan-dev.bin\n"
char new_entry[FW_DB_LINE_SIZE];
snprintf(new_entry, sizeof(new_entry), "%s=%s\n", new_filename, original_filename);
// Open the existing DB for reading (may not exist yet) and a temp file for writing.
FILE *db_in = fopen(FW_DB_PATH, "r");
FILE *db_out = fopen(FW_DB_TMP_PATH, "w");
if (!db_out) {
if (db_in) { fclose(db_in); }
PX4_WARN("updateFwDatabase: couldn't open tmp file");
return;
}
// Copy all existing entries to the temp file, replacing the matching key in-place.
bool line_updated = false;
char line[FW_DB_LINE_SIZE];
if (db_in) {
while (fgets(line, sizeof(line), db_in)) {
if (strncmp(line, new_filename, strlen(new_filename)) == 0 && line[strlen(new_filename)] == '=') {
fputs(new_entry, db_out);
line_updated = true;
} else {
fputs(line, db_out);
}
}
fclose(db_in);
}
// Key was not present in the existing DB — append it as a new entry.
if (!line_updated) {
fputs(new_entry, db_out);
}
fflush(db_out);
fsync(fileno(db_out));
fclose(db_out);
// Atomically replace the old DB with the updated version.
rename(FW_DB_TMP_PATH, FW_DB_PATH);
}
void UavcanServers::validateFwDatabase()
{
FILE *db_in = fopen(FW_DB_PATH, "r");
if (!db_in) {
return; // no DB yet, nothing to validate
}
FILE *db_out = fopen(FW_DB_TMP_PATH, "w");
if (!db_out) {
fclose(db_in);
PX4_WARN("validateFwDatabase: couldn't open tmp file");
return;
}
bool changed = false;
char line[FW_DB_LINE_SIZE];
while (fgets(line, sizeof(line), db_in)) {
// Each valid line is "key=original_filename\n", e.g. "122.bin=122-1.17.abc.bin"
const char *eq = strchr(line, '=');
if (eq == nullptr) {
changed = true; // malformed line — drop it
continue;
}
// Build the full path to the firmware file to check if it still exists.
char full_path[UAVCAN_MAX_PATH_LENGTH + 1];
snprintf(full_path, sizeof(full_path), "%s/%.*s", UAVCAN_FIRMWARE_PATH,
(int)(eq - line), line);
struct stat sb;
if (stat(full_path, &sb) == 0 && S_ISREG(sb.st_mode)) {
fputs(line, db_out);
} else {
PX4_INFO("validateFwDatabase: removing stale entry for '%.*s'", (int)(eq - line), line);
changed = true;
}
}
fclose(db_in);
fflush(db_out);
fsync(fileno(db_out));
fclose(db_out);
if (changed) {
rename(FW_DB_TMP_PATH, FW_DB_PATH);
} else {
unlink(FW_DB_TMP_PATH);
}
}
+3
View File
@@ -34,6 +34,7 @@
#pragma once
#include <px4_platform_common/px4_config.h>
#include <drivers/drv_hrt.h>
#include <uavcan/protocol/dynamic_node_id_server/centralized.hpp>
#include <uavcan/protocol/file_server.hpp>
@@ -74,6 +75,8 @@ private:
void unpackFwFromROMFS(const char *sd_path, const char *romfs_path);
void migrateFWFromRoot(const char *sd_path, const char *sd_root_path);
int copyFw(const char *dst, const char *src);
void updateFwDatabase(const char *fw_path, const char *original_filename);
void validateFwDatabase();
uavcan_posix::dynamic_node_id_server::FileEventTracer _tracer;
uavcan_posix::dynamic_node_id_server::FileStorageBackend _storage_backend;