diff --git a/src/Makefile b/src/Makefile index 20bb70c..4ccf95d 100644 --- a/src/Makefile +++ b/src/Makefile @@ -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 diff --git a/src/alsa-scarlett-gui.css b/src/alsa-scarlett-gui.css index 9b26eeb..98f5dc6 100644 --- a/src/alsa-scarlett-gui.css +++ b/src/alsa-scarlett-gui.css @@ -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; diff --git a/src/device-update-firmware.c b/src/device-update-firmware.c new file mode 100644 index 0000000..2b02814 --- /dev/null +++ b/src/device-update-firmware.c @@ -0,0 +1,140 @@ +// SPDX-FileCopyrightText: 2024 Geoffrey D. Bennett +// SPDX-License-Identifier: GPL-3.0-or-later + +#include +#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 + ); +} diff --git a/src/device-update-firmware.h b/src/device-update-firmware.h new file mode 100644 index 0000000..127fef6 --- /dev/null +++ b/src/device-update-firmware.h @@ -0,0 +1,9 @@ +// SPDX-FileCopyrightText: 2024 Geoffrey D. Bennett +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include +#include "alsa.h" + +void create_update_firmware_window(GtkWidget *w, struct alsa_card *card); diff --git a/src/hardware.c b/src/hardware.c new file mode 100644 index 0000000..1021583 --- /dev/null +++ b/src/hardware.c @@ -0,0 +1,36 @@ +// SPDX-FileCopyrightText: 2023-2024 Geoffrey D. Bennett +// SPDX-License-Identifier: GPL-3.0-or-later + +#include + +#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; +} diff --git a/src/hardware.h b/src/hardware.h new file mode 100644 index 0000000..cde3eec --- /dev/null +++ b/src/hardware.h @@ -0,0 +1,10 @@ +// SPDX-FileCopyrightText: 2023-2024 Geoffrey D. Bennett +// 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); diff --git a/src/iface-update.c b/src/iface-update.c new file mode 100644 index 0000000..e8b326c --- /dev/null +++ b/src/iface-update.c @@ -0,0 +1,62 @@ +// SPDX-FileCopyrightText: 2024 Geoffrey D. Bennett +// SPDX-License-Identifier: GPL-3.0-or-later + +#include + +#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" + "" + "https://github.com/geoffreybennett/scarlett2-firmware,\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; +} diff --git a/src/iface-update.h b/src/iface-update.h new file mode 100644 index 0000000..5469a51 --- /dev/null +++ b/src/iface-update.h @@ -0,0 +1,8 @@ +// SPDX-FileCopyrightText: 2024 Geoffrey D. Bennett +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include "alsa.h" + +GtkWidget *create_iface_update_main(struct alsa_card *card); diff --git a/src/main.c b/src/main.c index 969bd38..0ed62f3 100644 --- a/src/main.c +++ b/src/main.c @@ -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(); diff --git a/src/scarlett2-firmware.c b/src/scarlett2-firmware.c new file mode 100644 index 0000000..d46362d --- /dev/null +++ b/src/scarlett2-firmware.c @@ -0,0 +1,289 @@ +// SPDX-FileCopyrightText: 2023-2024 Geoffrey D. Bennett +// SPDX-License-Identifier: GPL-3.0-or-later + +#include +#include +#include +#include +#include +#include +#include + +#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); +} diff --git a/src/scarlett2-firmware.h b/src/scarlett2-firmware.h new file mode 100644 index 0000000..1ed47ec --- /dev/null +++ b/src/scarlett2-firmware.h @@ -0,0 +1,46 @@ +// SPDX-FileCopyrightText: 2023-2024 Geoffrey D. Bennett +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include + +// 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); diff --git a/src/window-iface.c b/src/window-iface.c index 2afba4f..8dd7779 100644 --- a/src/window-iface.c +++ b/src/window-iface.c @@ -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 diff --git a/src/window-startup.c b/src/window-startup.c index 67105c8..74ebc6c 100644 --- a/src/window-startup.c +++ b/src/window-startup.c @@ -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) {