Add support for firmware update

This commit is contained in:
Geoffrey D. Bennett
2024-02-17 00:57:53 +10:30
parent c9441b2591
commit 9026eb76c5
13 changed files with 667 additions and 2 deletions

View File

@@ -26,7 +26,7 @@ CFLAGS += $(shell $(PKG_CONFIG) --cflags alsa)
LDFLAGS += $(shell $(PKG_CONFIG) --libs glib-2.0)
LDFLAGS += $(shell $(PKG_CONFIG) --libs gtk4)
LDFLAGS += $(shell $(PKG_CONFIG) --libs alsa)
LDFLAGS += -lm
LDFLAGS += -lm -lcrypto
COMPILE.c = $(CC) $(DEPFLAGS) $(CFLAGS) -c

View File

@@ -14,6 +14,16 @@
border-radius: 20px;
}
/* Title of the window */
.window-title {
font-size: large;
}
/* Links */
.linktext {
color: #89CFF0;
}
/* Label above controls-content */
.controls-label {
font-size: smaller;

View File

@@ -0,0 +1,140 @@
// SPDX-FileCopyrightText: 2024 Geoffrey D. Bennett <g@b4.vu>
// SPDX-License-Identifier: GPL-3.0-or-later
#include <gtk/gtk.h>
#include "device-reset-config.h"
#include "scarlett2.h"
#include "scarlett2-firmware.h"
#include "scarlett2-ioctls.h"
#include "window-modal.h"
static gpointer update_progress(
struct modal_data *modal_data,
char *text,
int progress
) {
struct progress_data *progress_data = g_new0(struct progress_data, 1);
progress_data->modal_data = modal_data;
progress_data->text = text;
progress_data->progress = progress;
g_main_context_invoke(NULL, modal_update_progress, progress_data);
return NULL;
}
#define fail(msg) { \
if (hwdep) \
scarlett2_close(hwdep); \
if (firmware) \
scarlett2_free_firmware_file(firmware); \
return update_progress(modal_data, msg, -1); \
}
#define failsndmsg(msg) g_strdup_printf(msg, snd_strerror(err))
gpointer update_firmware_thread(gpointer user_data) {
struct modal_data *modal_data = user_data;
struct alsa_card *card = modal_data->card;
int err = 0;
snd_hwdep_t *hwdep = NULL;
// read the firmware file
update_progress(modal_data, g_strdup("Checking firmware..."), 0);
struct scarlett2_firmware_file *firmware =
scarlett2_get_best_firmware(card->pid);
// if no firmware, fail
if (!firmware)
fail(failsndmsg("No update firmware found for device: %s"));
if (firmware->header.usb_pid != card->pid)
fail(g_strdup("Firmware file does not match device"));
update_progress(modal_data, g_strdup("Resetting configuration..."), 0);
err = scarlett2_open_card(card->device, &hwdep);
if (err < 0)
fail(failsndmsg("Unable to open hwdep interface: %s"));
err = scarlett2_erase_config(hwdep);
if (err < 0)
fail(failsndmsg("Unable to reset configuration: %s"));
while (1) {
g_usleep(50000);
err = scarlett2_get_erase_progress(hwdep);
if (err < 0)
fail(failsndmsg("Unable to get erase progress: %s"));
if (err == 255)
break;
update_progress(modal_data, NULL, err);
}
update_progress(modal_data, g_strdup("Erasing flash..."), 0);
err = scarlett2_erase_firmware(hwdep);
if (err < 0)
fail(failsndmsg("Unable to erase upgrade firmware: %s"));
while (1) {
g_usleep(50000);
err = scarlett2_get_erase_progress(hwdep);
if (err < 0)
fail(failsndmsg("Unable to get erase progress: %s"));
if (err == 255)
break;
update_progress(modal_data, NULL, err);
}
update_progress(modal_data, g_strdup("Writing firmware..."), 0);
size_t offset = 0;
size_t len = firmware->header.firmware_length;
unsigned char *buf = firmware->firmware_data;
while (offset < len) {
err = snd_hwdep_write(hwdep, buf + offset, len - offset);
if (err < 0)
fail(failsndmsg("Unable to write firmware: %s"));
offset += err;
update_progress(modal_data, NULL, (offset * 100) / len);
}
g_main_context_invoke(NULL, modal_start_reboot_progress, modal_data);
scarlett2_reboot(hwdep);
scarlett2_close(hwdep);
return NULL;
}
static void join_thread(gpointer thread) {
g_thread_join(thread);
}
static void update_firmware_yes_callback(struct modal_data *modal_data) {
GThread *thread = g_thread_new(
"update_firmware_thread", update_firmware_thread, modal_data
);
g_object_set_data_full(
G_OBJECT(modal_data->button_box), "thread", thread, join_thread
);
}
void create_update_firmware_window(GtkWidget *w, struct alsa_card *card) {
create_modal_window(
w, card,
"Confirm Update Firmware",
"Updating Firmware",
"The firmware update process will take about 15 seconds.\n"
"Please do not disconnect the device while updating.\n"
"Ready to proceed?",
update_firmware_yes_callback
);
}

View File

@@ -0,0 +1,9 @@
// SPDX-FileCopyrightText: 2024 Geoffrey D. Bennett <g@b4.vu>
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <gtk/gtk.h>
#include "alsa.h"
void create_update_firmware_window(GtkWidget *w, struct alsa_card *card);

36
src/hardware.c Normal file
View File

@@ -0,0 +1,36 @@
// SPDX-FileCopyrightText: 2023-2024 Geoffrey D. Bennett <g@b4.vu>
// SPDX-License-Identifier: GPL-3.0-or-later
#include <stddef.h>
#include "hardware.h"
struct scarlett2_device scarlett2_supported[] = {
{ 0x8203, "Scarlett 2nd Gen 6i6" },
{ 0x8204, "Scarlett 2nd Gen 18i8" },
{ 0x8201, "Scarlett 2nd Gen 18i20" },
{ 0x8211, "Scarlett 3rd Gen Solo" },
{ 0x8210, "Scarlett 3rd Gen 2i2" },
{ 0x8212, "Scarlett 3rd Gen 4i4" },
{ 0x8213, "Scarlett 3rd Gen 8i6" },
{ 0x8214, "Scarlett 3rd Gen 18i8" },
{ 0x8215, "Scarlett 3rd Gen 18i20" },
{ 0x8218, "Scarlett 4th Gen Solo" },
{ 0x8219, "Scarlett 4th Gen 2i2" },
{ 0x821a, "Scarlett 4th Gen 4i4" },
{ 0x8206, "Clarett USB 2Pre" },
{ 0x8207, "Clarett USB 4Pre" },
{ 0x8208, "Clarett USB 8Pre" },
{ 0x820a, "Clarett+ 2Pre" },
{ 0x820b, "Clarett+ 4Pre" },
{ 0x820c, "Clarett+ 8Pre" },
{ 0, NULL }
};
struct scarlett2_device *get_device_for_pid(int pid) {
for (int i = 0; scarlett2_supported[i].name; i++)
if (scarlett2_supported[i].pid == pid)
return &scarlett2_supported[i];
return NULL;
}

10
src/hardware.h Normal file
View File

@@ -0,0 +1,10 @@
// SPDX-FileCopyrightText: 2023-2024 Geoffrey D. Bennett <g@b4.vu>
// SPDX-License-Identifier: GPL-3.0-or-later
// Supported devices
struct scarlett2_device {
int pid;
const char *name;
};
struct scarlett2_device *get_device_for_pid(int pid);

62
src/iface-update.c Normal file
View File

@@ -0,0 +1,62 @@
// SPDX-FileCopyrightText: 2024 Geoffrey D. Bennett <g@b4.vu>
// SPDX-License-Identifier: GPL-3.0-or-later
#include <gtk/gtk.h>
#include "alsa.h"
#include "device-update-firmware.h"
#include "gtkhelper.h"
#include "scarlett2-firmware.h"
GtkWidget *create_iface_update_main(struct alsa_card *card) {
GtkWidget *top = gtk_frame_new(NULL);
gtk_widget_add_css_class(top, "window-frame");
GtkWidget *content = gtk_box_new(GTK_ORIENTATION_VERTICAL, 30);
gtk_widget_add_css_class(content, "window-content");
gtk_widget_add_css_class(content, "top-level-content");
gtk_widget_add_css_class(content, "big-padding");
gtk_frame_set_child(GTK_FRAME(top), content);
// explanation
GtkWidget *w;
w = gtk_label_new("Firmware Update Required");
gtk_widget_add_css_class(w, "window-title");
gtk_box_append(GTK_BOX(content), w);
uint32_t best_firmware_version =
scarlett2_get_best_firmware_version(card->pid);
if (!best_firmware_version) {
w = gtk_label_new(NULL);
gtk_label_set_markup(
GTK_LABEL(w),
"A firmware update is required for this device in order to\n"
"access all of its features. Please obtain the firmware from\n"
"<a class=\"linktext\" "
"href=\"https://github.com/geoffreybennett/scarlett2-firmware\">"
"https://github.com/geoffreybennett/scarlett2-firmware</a>,\n"
"and restart this application."
);
gtk_box_append(GTK_BOX(content), w);
return top;
}
w = gtk_label_new(
"A firmware update is required for this device in order to\n"
"access all of its features. This process will take about 15\n"
"seconds. Please do not disconnect the device during the\n"
"update."
);
gtk_box_append(GTK_BOX(content), w);
w = gtk_button_new_with_label("Update");
g_signal_connect(
GTK_BUTTON(w), "clicked", G_CALLBACK(create_update_firmware_window), card
);
gtk_box_append(GTK_BOX(content), w);
return top;
}

8
src/iface-update.h Normal file
View File

@@ -0,0 +1,8 @@
// SPDX-FileCopyrightText: 2024 Geoffrey D. Bennett <g@b4.vu>
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include "alsa.h"
GtkWidget *create_iface_update_main(struct alsa_card *card);

View File

@@ -5,6 +5,7 @@
#include "alsa-sim.h"
#include "main.h"
#include "menu.h"
#include "scarlett2-firmware.h"
#include "window-hardware.h"
#include "window-iface.h"
@@ -34,6 +35,7 @@ static void startup(GtkApplication *app, gpointer user_data) {
load_css();
scarlett2_enum_firmware();
alsa_init();
create_no_card_window();

289
src/scarlett2-firmware.c Normal file
View File

@@ -0,0 +1,289 @@
// SPDX-FileCopyrightText: 2023-2024 Geoffrey D. Bennett <g@b4.vu>
// SPDX-License-Identifier: GPL-3.0-or-later
#include <glib.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <arpa/inet.h>
#include <openssl/sha.h>
#include <sys/stat.h>
#include "scarlett2-firmware.h"
// List of found firmware files
struct found_firmware {
char *fn;
struct scarlett2_firmware_header *firmware;
};
GHashTable *best_firmware = NULL;
static int verify_sha256(
const unsigned char *data,
size_t length,
const unsigned char *expected_hash
) {
unsigned char computed_hash[SHA256_DIGEST_LENGTH];
SHA256(data, length, computed_hash);
return memcmp(computed_hash, expected_hash, SHA256_DIGEST_LENGTH) == 0;
}
static struct scarlett2_firmware_file *read_header(FILE *file) {
struct scarlett2_firmware_file *firmware = calloc(
1, sizeof(struct scarlett2_firmware_file)
);
if (!firmware) {
perror("Failed to allocate memory for firmware structure");
goto error;
}
size_t read_count = fread(
&firmware->header, sizeof(struct scarlett2_firmware_header), 1, file
);
if (read_count != 1) {
if (feof(file))
fprintf(stderr, "Unexpected end of file\n");
else
perror("Failed to read header");
goto error;
}
if (strncmp(firmware->header.magic, MAGIC_STRING, 8) != 0) {
fprintf(stderr, "Invalid magic number\n");
goto error;
}
firmware->header.usb_vid = ntohs(firmware->header.usb_vid);
firmware->header.usb_pid = ntohs(firmware->header.usb_pid);
firmware->header.firmware_version = ntohl(firmware->header.firmware_version);
firmware->header.firmware_length = ntohl(firmware->header.firmware_length);
return firmware;
error:
free(firmware);
return NULL;
}
struct scarlett2_firmware_header *scarlett2_read_firmware_header(
const char *fn
) {
FILE *file = fopen(fn, "rb");
if (!file) {
perror("fopen");
fprintf(stderr, "Unable to open %s\n", fn);
return NULL;
}
struct scarlett2_firmware_file *firmware = read_header(file);
if (!firmware) {
fprintf(stderr, "Error reading firmware header from %s\n", fn);
return NULL;
}
fclose(file);
return realloc(firmware, sizeof(struct scarlett2_firmware_header));
}
struct scarlett2_firmware_file *scarlett2_read_firmware_file(const char *fn) {
FILE *file = fopen(fn, "rb");
if (!file) {
perror("fopen");
fprintf(stderr, "Unable to open %s\n", fn);
return NULL;
}
struct scarlett2_firmware_file *firmware = read_header(file);
if (!firmware) {
fprintf(stderr, "Error reading firmware header from %s\n", fn);
return NULL;
}
firmware->firmware_data = malloc(firmware->header.firmware_length);
if (!firmware->firmware_data) {
perror("Failed to allocate memory for firmware data");
goto error;
}
size_t read_count = fread(
firmware->firmware_data, 1, firmware->header.firmware_length, file
);
if (read_count != firmware->header.firmware_length) {
if (feof(file))
fprintf(stderr, "Unexpected end of file\n");
else
perror("Failed to read firmware data");
fprintf(stderr, "Error reading firmware data from %s\n", fn);
goto error;
}
if (!verify_sha256(
firmware->firmware_data,
firmware->header.firmware_length,
firmware->header.sha256
)) {
fprintf(stderr, "Corrupt firmware (failed checksum) in %s\n", fn);
goto error;
}
fclose(file);
return firmware;
error:
scarlett2_free_firmware_file(firmware);
fclose(file);
return NULL;
}
void scarlett2_free_firmware_header(struct scarlett2_firmware_header *firmware) {
if (firmware)
free(firmware);
}
void scarlett2_free_firmware_file(struct scarlett2_firmware_file *firmware) {
if (firmware) {
free(firmware->firmware_data);
free(firmware);
}
}
static void free_found_firmware(gpointer data) {
struct found_firmware *found = data;
free(found->fn);
scarlett2_free_firmware_header(found->firmware);
free(found);
}
static void init_best_firmware(void) {
if (best_firmware)
return;
best_firmware = g_hash_table_new_full(
g_direct_hash, g_direct_equal, NULL, free_found_firmware
);
}
// Add a firmware file to the list of found firmware
// files, if it's better than the one already found
// for the same device.
static void add_found_firmware(
char *fn,
struct scarlett2_firmware_header *firmware
) {
gpointer key = GINT_TO_POINTER(firmware->usb_pid);
struct found_firmware *found = g_hash_table_lookup(best_firmware, key);
// already have a firmware file for this device?
if (found) {
// lower version number, ignore
if (firmware->firmware_version <= found->firmware->firmware_version) {
free(fn);
scarlett2_free_firmware_header(firmware);
return;
}
// higher version number, replace
g_hash_table_remove(best_firmware, key);
}
found = malloc(sizeof(struct found_firmware));
if (!found) {
perror("Failed to allocate memory for firmware structure");
return;
}
found->fn = fn;
found->firmware = firmware;
g_hash_table_insert(best_firmware, key, found);
}
// look for firmware files in the given directory
static void enum_firmware_dir(const char *dir_name) {
DIR *dir = opendir(dir_name);
if (!dir) {
if (errno == ENOENT) {
fprintf(stderr, "Firmware directory %s does not exist\n", dir_name);
return;
}
fprintf(
stderr, "Error opening directory %s: %s\n", dir_name, strerror(errno)
);
return;
}
struct dirent *entry;
while ((entry = readdir(dir))) {
char *full_fn;
// check if the file is a .bin file
if (strlen(entry->d_name) < 4 ||
strcmp(entry->d_name + strlen(entry->d_name) - 4, ".bin") != 0)
continue;
// check if the file is a regular file
if (entry->d_type == DT_UNKNOWN) {
struct stat st;
full_fn = g_build_filename(dir_name, entry->d_name, NULL);
if (stat(full_fn, &st) < 0) {
perror("stat");
g_free(full_fn);
continue;
}
if (!S_ISREG(st.st_mode)) {
g_free(full_fn);
continue;
}
} else if (entry->d_type != DT_REG) {
continue;
} else {
full_fn = g_build_filename(dir_name, entry->d_name, NULL);
}
struct scarlett2_firmware_header *firmware =
scarlett2_read_firmware_header(full_fn);
if (!firmware) {
fprintf(stderr, "Error reading firmware file %s\n", full_fn);
g_free(full_fn);
continue;
}
add_found_firmware(full_fn, firmware);
}
closedir(dir);
}
void scarlett2_enum_firmware(void) {
init_best_firmware();
enum_firmware_dir(SCARLETT2_FIRMWARE_DIR);
}
uint32_t scarlett2_get_best_firmware_version(uint32_t pid) {
struct found_firmware *found = g_hash_table_lookup(
best_firmware, GINT_TO_POINTER(pid)
);
if (!found)
return 0;
return found->firmware->firmware_version;
}
struct scarlett2_firmware_file *scarlett2_get_best_firmware(uint32_t pid) {
struct found_firmware *found = g_hash_table_lookup(
best_firmware, GINT_TO_POINTER(pid)
);
if (!found)
return NULL;
return scarlett2_read_firmware_file(found->fn);
}

46
src/scarlett2-firmware.h Normal file
View File

@@ -0,0 +1,46 @@
// SPDX-FileCopyrightText: 2023-2024 Geoffrey D. Bennett <g@b4.vu>
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <stdint.h>
// System-wide firmware directory
#define SCARLETT2_FIRMWARE_DIR "/usr/lib/firmware/scarlett2"
#define MAGIC_STRING "SCARLETT"
struct scarlett2_firmware_header {
char magic[8]; // "SCARLETT"
uint16_t usb_vid; // Big-endian
uint16_t usb_pid; // Big-endian
uint32_t firmware_version; // Big-endian
uint32_t firmware_length; // Big-endian
uint8_t sha256[32];
} __attribute__((packed));
struct scarlett2_firmware_file {
struct scarlett2_firmware_header header;
uint8_t *firmware_data;
};
struct scarlett2_firmware_header *scarlett2_read_firmware_header(
const char *fn
);
void scarlett2_free_firmware_header(
struct scarlett2_firmware_header *firmware
);
struct scarlett2_firmware_file *scarlett2_read_firmware_file(
const char *fn
);
void scarlett2_free_firmware_file(
struct scarlett2_firmware_file *firmware
);
void scarlett2_enum_firmware(void);
uint32_t scarlett2_get_best_firmware_version(uint32_t pid);
struct scarlett2_firmware_file *scarlett2_get_best_firmware(uint32_t pid);

View File

@@ -7,6 +7,7 @@
#include "iface-no-mixer.h"
#include "iface-none.h"
#include "iface-unknown.h"
#include "iface-update.h"
#include "main.h"
#include "menu.h"
#include "window-iface.h"
@@ -27,8 +28,25 @@ void create_card_window(struct alsa_card *card) {
int has_startup = true;
int has_mixer = true;
struct alsa_elem *firmware_elem =
get_elem_by_name(card->elems, "Firmware Version");
struct alsa_elem *min_firmware_elem =
get_elem_by_name(card->elems, "Minimum Firmware Version");
int firmware_version = 0;
int min_firmware_version = 0;
if (firmware_elem && min_firmware_elem) {
firmware_version = alsa_get_elem_value(firmware_elem);
min_firmware_version = alsa_get_elem_value(min_firmware_elem);
}
// Firmware update required
if (firmware_version < min_firmware_version) {
card->window_main_contents = create_iface_update_main(card);
has_startup = false;
has_mixer = false;
// Gen 2 or Gen 3 4i4+
if (get_elem_by_prefix(card->elems, "Mixer")) {
} else if (get_elem_by_prefix(card->elems, "Mixer")) {
card->window_main_contents = create_iface_mixer_main(card);
// Gen 3 Solo or 2i2

View File

@@ -2,8 +2,10 @@
// SPDX-License-Identifier: GPL-3.0-or-later
#include "device-reset-config.h"
#include "device-update-firmware.h"
#include "gtkhelper.h"
#include "scarlett2.h"
#include "scarlett2-firmware.h"
#include "scarlett2-ioctls.h"
#include "widget-boolean.h"
#include "window-startup.h"
@@ -227,6 +229,39 @@ static void add_reset_actions(
"factory default settings. The firmware will be left unchanged.",
G_CALLBACK(create_reset_config_window)
);
// Update Firmware
struct alsa_elem *firmware_elem =
get_elem_by_name(card->elems, "Firmware Version");
if (!firmware_elem)
return;
int firmware_version = alsa_get_elem_value(firmware_elem);
uint32_t best_firmware_version =
scarlett2_get_best_firmware_version(card->pid);
if (firmware_version >= best_firmware_version)
return;
char *s = g_strdup_printf(
"Updating the firmware will reset the interface to its "
"factory default settings and update the firmware from version "
"%d to %d.",
firmware_version,
best_firmware_version
);
add_reset_action(
card,
grid,
grid_y,
"Update Firmware",
"Update",
s,
G_CALLBACK(create_update_firmware_window)
);
g_free(s);
}
static void add_no_startup_controls_msg(GtkWidget *grid) {