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 ] if [ $USE_ALT_UPDATE_DIRS = yes ]
then 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 # Check for an update of the ext_autostart folder, and replace the old one with it
if [ -e /fs/microsd/ext_autostart_new ] if [ -e /fs/microsd/ext_autostart_new ]
then then
+37 -4
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# Flash PX4 to a device running AuterionOS in the local network # Flash PX4 to a device running AuterionOS in the local network
if [ "$1" == "-h" ] || [ "$1" == "--help" ] || [ $# -lt 2 ]; then 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 exit 1
fi fi
@@ -9,7 +9,7 @@ ssh_port=22
ssh_user=root ssh_user=root
ssh_opts="-o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no" 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 case ${opt} in
f ) f )
if [ -n "$OPTARG" ]; then if [ -n "$OPTARG" ]; then
@@ -27,6 +27,14 @@ while getopts ":f:c:d:p:u:r" opt; do
exit 1 exit 1
fi fi
;; ;;
x )
if [ -f "$OPTARG" ]; then
external_firmware_files+=("$OPTARG")
else
echo "ERROR: -x requires a valid firmware file path."
exit 1
fi
;;
d ) d )
if [ "$OPTARG" ]; then if [ "$OPTARG" ]; then
device="$OPTARG" device="$OPTARG"
@@ -74,12 +82,23 @@ else
tmp_dir="$(mktemp -d)" tmp_dir="$(mktemp -d)"
config_path="" config_path=""
firmware_path="" firmware_path=""
ufw_path=""
if [ -d "$config_dir" ]; then if [ -d "$config_dir" ]; then
cp -r "$config_dir" "$tmp_dir/config" cp -r "$config_dir" "$tmp_dir/config"
config_path=config config_path=config
fi 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 if [ -f "$firmware_file" ]; then
extension="${firmware_file##*.}" extension="${firmware_file##*.}"
cp "$firmware_file" "$tmp_dir/firmware.$extension" cp "$firmware_file" "$tmp_dir/firmware.$extension"
@@ -92,7 +111,7 @@ else
pushd "$tmp_dir" &>/dev/null 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 exit 1
fi fi
@@ -103,7 +122,21 @@ else
tar_name="gtar" tar_name="gtar"
fi 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 # send it to the target to start flashing
scp $ssh_opts -P $ssh_port "$target_file_name" $ssh_user@"$device":$target_dir 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_HOST=10.41.1.1
DEFAULT_AUTOPILOT_PORT=33333 DEFAULT_AUTOPILOT_PORT=33333
DEFAULT_AUTOPILOT_USER=auterion DEFAULT_AUTOPILOT_USER=auterion
EXTERNAL_FIRMWARE_FILES=()
for i in "$@" for i in "$@"
do do
@@ -26,6 +27,9 @@ do
--wifi) --wifi)
DEFAULT_AUTOPILOT_HOST=10.41.0.1 DEFAULT_AUTOPILOT_HOST=10.41.0.1
;; ;;
--ext-fw=*)
EXTERNAL_FIRMWARE_FILES+=("${i#*=}")
;;
*) *)
# unknown option # unknown option
;; ;;
@@ -42,6 +46,9 @@ ARGUMENTS+=(-d "$AUTOPILOT_HOST")
ARGUMENTS+=(-p "$AUTOPILOT_PORT") ARGUMENTS+=(-p "$AUTOPILOT_PORT")
ARGUMENTS+=(-u "$AUTOPILOT_USER") ARGUMENTS+=(-u "$AUTOPILOT_USER")
ARGUMENTS+=(${PX4_BINARY_FILE:+-f "$PX4_BINARY_FILE"}) 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) ARGUMENTS+=($REVERT_AUTOPILOT_ARGUMENT)
echo "Flashing $AUTOPILOT_HOST ..." echo "Flashing $AUTOPILOT_HOST ..."
@@ -94,7 +94,7 @@ protected:
* @return True - the class will begin sending update requests. * @return True - the class will begin sending update requests.
* False - the node will be ignored, no request will be sent. * 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, const uavcan::protocol::GetNodeInfo::Response &node_info,
FirmwareFilePath &out_firmware_file_path) FirmwareFilePath &out_firmware_file_path)
{ {
@@ -174,6 +174,10 @@ protected:
return true; return true;
} }
static constexpr unsigned _MaxChunk = 512 / sizeof(uint64_t);
inline static uint64_t _chunk[_MaxChunk]
px4_cache_aligned_data() = {};
public: public:
struct AppDescriptor { struct AppDescriptor {
uavcan::uint8_t signature[sizeof(uavcan::uint64_t)]; uavcan::uint8_t signature[sizeof(uavcan::uint64_t)];
@@ -193,9 +197,6 @@ public:
static int getFileInfo(const char *path, AppDescriptor &descriptor, int limit = 0) static int getFileInfo(const char *path, AppDescriptor &descriptor, int limit = 0)
{ {
using namespace std; using namespace std;
const unsigned MaxChunk = 512 / sizeof(uint64_t);
// Make sure this does not present as a valid descriptor // Make sure this does not present as a valid descriptor
struct { struct {
union { union {
@@ -206,14 +207,13 @@ public:
} s; } s;
int rv = -ENOENT; int rv = -ENOENT;
uint64_t chunk[MaxChunk];
int fd = open(path, O_RDONLY); int fd = open(path, O_RDONLY);
if (fd >= 0) { if (fd >= 0) {
AppDescriptor *pdescriptor = UAVCAN_NULLPTR; AppDescriptor *pdescriptor = UAVCAN_NULLPTR;
while (pdescriptor == UAVCAN_NULLPTR && limit >= 0) { while (pdescriptor == UAVCAN_NULLPTR && limit >= 0) {
int len = read(fd, chunk, sizeof(chunk)); int len = read(fd, _chunk, sizeof(_chunk));
if (len == 0) { if (len == 0) {
break; break;
@@ -224,10 +224,10 @@ public:
goto out_close; goto out_close;
} }
uint64_t *p = &chunk[0]; uint64_t *p = &_chunk[0];
if (limit > 0) { if (limit > 0) {
limit -= sizeof(chunk); limit -= sizeof(_chunk);
} }
do { do {
@@ -237,7 +237,7 @@ public:
rv = 0; rv = 0;
break; break;
} }
} while (p++ <= &chunk[MaxChunk - (sizeof(AppDescriptor) / sizeof(chunk[0]))]); } while (p++ <= &_chunk[_MaxChunk - (sizeof(AppDescriptor) / sizeof(_chunk[0]))]);
} }
out_close: out_close:
+1
View File
@@ -51,6 +51,7 @@
#define UAVCAN_MAX_PATH_LENGTH (128 + 40) #define UAVCAN_MAX_PATH_LENGTH (128 + 40)
#define UAVCAN_SD_ROOT_PATH CONFIG_BOARD_ROOT_PATH "/" #define UAVCAN_SD_ROOT_PATH CONFIG_BOARD_ROOT_PATH "/"
#define UAVCAN_FIRMWARE_PATH UAVCAN_SD_ROOT_PATH"ufw" #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_PATH "/etc/uavcan/fw"
#define UAVCAN_ROMFS_FW_PREFIX "_" #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/dynamic_node_id_server/file_storage_backend.hpp>
#include <uavcan_posix/firmware_version_checker.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 * @file uavcan_servers.cpp
* *
@@ -136,12 +142,20 @@ int UavcanServers::init()
return ret; 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 Check for firmware in the root directory, move it to appropriate location on
the SD card, as defined by the APDesc. the SD card, as defined by the APDesc.
*/ */
migrateFWFromRoot(UAVCAN_FIRMWARE_PATH, UAVCAN_SD_ROOT_PATH); 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 */ /* Start the Node */
return 0; 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); snprintf(dstpath, sizeof(dstpath), "%s/%d.bin", sd_path, descriptor.board_id);
if (copyFw(dstpath, srcpath) >= 0) { if (copyFw(dstpath, srcpath) >= 0) {
updateFwDatabase(dstpath, bin_names[i]);
unlink(srcpath); 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 UavcanServers::copyFw(const char *dst, const char *src)
{ {
int rv = 0; int rv = 0;
uint8_t buffer[512] {};
int dfd = open(dst, O_WRONLY | O_CREAT, 0666); 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; ssize_t size = 0;
do { do {
size = read(sfd, buffer, sizeof(buffer)); size = read(sfd, _buffer, sizeof(_buffer));
if (size < 0) { if (size < 0) {
PX4_ERR("copyFw: couldn't read"); PX4_ERR("copyFw: couldn't read");
@@ -263,7 +277,7 @@ int UavcanServers::copyFw(const char *dst, const char *src)
ssize_t written = 0; ssize_t written = 0;
do { do {
written = write(dfd, &buffer[total_written], remaining); written = write(dfd, &_buffer[total_written], remaining);
if (written < 0) { if (written < 0) {
PX4_ERR("copyFw: couldn't write"); PX4_ERR("copyFw: couldn't write");
@@ -282,3 +296,112 @@ int UavcanServers::copyFw(const char *dst, const char *src)
return rv; 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 #pragma once
#include <px4_platform_common/px4_config.h> #include <px4_platform_common/px4_config.h>
#include <drivers/drv_hrt.h>
#include <uavcan/protocol/dynamic_node_id_server/centralized.hpp> #include <uavcan/protocol/dynamic_node_id_server/centralized.hpp>
#include <uavcan/protocol/file_server.hpp> #include <uavcan/protocol/file_server.hpp>
@@ -74,6 +75,8 @@ private:
void unpackFwFromROMFS(const char *sd_path, const char *romfs_path); void unpackFwFromROMFS(const char *sd_path, const char *romfs_path);
void migrateFWFromRoot(const char *sd_path, const char *sd_root_path); void migrateFWFromRoot(const char *sd_path, const char *sd_root_path);
int copyFw(const char *dst, const char *src); 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::FileEventTracer _tracer;
uavcan_posix::dynamic_node_id_server::FileStorageBackend _storage_backend; uavcan_posix::dynamic_node_id_server::FileStorageBackend _storage_backend;