Skip to content

feat: wireless stack flashing for STM32WB5xxG#5

Merged
nedseb merged 35 commits into
mainfrom
stack_flash
Apr 28, 2026
Merged

feat: wireless stack flashing for STM32WB5xxG#5
nedseb merged 35 commits into
mainfrom
stack_flash

Conversation

@nedseb
Copy link
Copy Markdown
Contributor

@nedseb nedseb commented Apr 28, 2026

Summary

  • New Wireless Stack tab alongside the existing DapLink flow, switchable via an iced_aw tab bar (refactor of the GUI into src/ui/{main_window,tab_daplink,tab_wireless_stack}).
  • End-to-end flashing of the STM32WB5xxG co-processor stack: flash the custom operator relay firmware over SWD with OpenOCD, then drive the FUS through it on a serial port (STATUS / VERSION / DELETE / UPGRADE commands) to install FUS + the user-selected wireless stack.
  • Bundles 20+ precompiled stack images (BLE / Thread / Zigbee / MAC / Phy and combinations), both FUS variants, and the operator hex under wireless_stack/. Stack metadata table added to both READMEs.
  • OpenOCD config rework: existing scripts renamed to f1x-*.cfg, new configs/wb5x.cfg for the WB55 over CMSIS-DAP, scripts now resolved relative to the executable directory (new dirs::set_exe_dir + get_configs_dir / get_wireless_stack_dir) instead of being materialized into the platform data dir.
  • open_ocd_task now streams stdout/stderr lines live to the UI through an mpsc Sender<Message> (generic over the tab's message type via WithLogMessage) instead of buffering until the process exits.
  • Settings load moved to the Window::Opened event so serial ports can be refreshed after deserialization; per-tab structs serialized under MainWindow.
  • Bump to 0.2.0; new deps: serialport 4.7.1, iced_aw features tabs + tab_bar.

Test plan

  • cargo build --release --locked on Linux (CI-equivalent).
  • DapLink tab: full flash flow on a STeaMi / L475 / WB55 target — unchanged behavior vs. main.
  • Wireless Stack tab: select a serial port, pick a stack (e.g. BleStackFullExt), confirm the operator is flashed, FUS upgrade reaches FUS_STATE_IDLE, then the chosen WS image installs successfully.
  • Verify Refresh repopulates the serial port list after plugging/unplugging the board.
  • Close + reopen the app: confirm the previously selected stack is restored from fields.json and that serial ports are re-enumerated.
  • Sanity-check Windows release build via cross (release workflow).

Copilot AI review requested due to automatic review settings April 28, 2026 08:44
@nedseb nedseb self-assigned this Apr 28, 2026
@nedseb nedseb added the enhancement New feature or request label Apr 28, 2026
@nedseb
Copy link
Copy Markdown
Contributor Author

nedseb commented Apr 28, 2026

Revue exhaustive de la branche. Le design global est cohérent (state-machine FwStep, abstraction WithLogMessage, séparation par onglets), mais plusieurs régressions et bugs réels avant qu'on puisse merger.

🔴 Bloquants

1. La release ne bundle plus configs/ ni wireless_stack/

.github/workflows/release.yaml n'a pas été mis à jour. Avec le nouveau modèle (src/dirs.rs:32-60), get_configs_dir() et get_wireless_stack_dir() renvoient une erreur si les dossiers n'existent pas à côté de l'exécutable, et l'ancien create_script_file() qui matérialisait les .cfg à partir d'include_str! a été supprimé. Les zips publiés ne contiennent que le binaire + xpack-openocd → même l'onglet DapLink se cassera sur "The configs folder does not exist".

→ Ajouter dans les jobs release linux + windows :

cp -r configs linux/
cp -r wireless_stack linux/

2. CI ne build plus — libudev-dev manquant

Cargo.toml ajoute serialport = "4.7.1" avec features par défaut. Sur Linux, ça active la backend libudev qui requiert libudev-dev via pkg-config. ci.yaml et release.yaml n'installent que libatk1.0-dev pkg-config libgtk-3-dev.

→ Ajouter libudev-dev au apt install, ou serialport = { version = "4.7.1", default-features = false }.

3. Panic au démarrage si aucun port série

src/ui/tab_wireless_stack.rs:579-606 :

if let Ok(ports) = ports {
    // …
    self.serial_selected = Some(self.serial_available_port[0].clone())  // ← panic si vide
}

available_ports() retourne Ok(vec![]) quand aucun port n'est dispo. L'indexation [0] panique. Reproduit à coup sûr sur une machine sans port série, et c'est le Default::default() de MainWindow qui le déclenche → l'app crashe avant même d'afficher la fenêtre.

self.serial_selected = self.serial_available_port.first().cloned();

🟠 Sérieux

4. static mut BASE_PATH — UB potentiel et erreur Rust 2024

src/dirs.rs:5-30 utilise static mut + unsafe avec #[allow(static_mut_refs)]. Erreur dure en édition 2024, warning fort en 2021.

→ Remplacer par static BASE_PATH: OnceLock<PathBuf> = OnceLock::new(); (stable depuis 1.70). API publique inchangée.

5. thread::sleep dans des futures async → UI gelée

Plusieurs thread::sleep(Duration::from_secs(...)) dans des closures async move exécutées via Task::run(channel(...)) :

  • tab_wireless_stack.rs:242 — 1 s avant ouverture port
  • tab_wireless_stack.rs:393 — 1 s entre tentatives delete
  • tab_wireless_stack.rs:7475 s avant UPGRADE

Ces sleep bloquent l'executor iced ⇒ gel de l'UI cumulé d'~10 s sur un cycle complet.

tokio::time::sleep (avec feature time) ou équivalent compatible iced.

6. Busy-loop CPU dans run_command_sender

src/open_ocd_task.rs:186-206 : la boucle qui draine le mutex et sonde try_wait() n'a aucun sleep ni yield. Tant qu'OpenOCD tourne (15 à 60 s pour un flash WB55), un cœur tourne à 100 %.

→ Ajouter tokio::time::sleep(Duration::from_millis(50)).await en fin de boucle.

7. stdout/stderr inversés et mal classés

src/open_ocd_task.rs:147-180 : la variable thread_stdout lit en réalité stderr (et la classe Info), thread_stderr lit stdout et classe tout en Error (rouge dans l'UI). OpenOCD écrit ses messages "OK" sur stdout (** Programming Started **, Verified OK) → l'utilisateur les voit en rouge. Les noms de variables sont mensongers, et la sévérité change le sens.

→ Renommer correctement et LogType::Info pour les deux sorties.

8. BleLld absent du picker

src/ui/tab_wireless_stack.rs:76-97 : ALL_STACK[20] ne contient pas WirelessStackFile::BleLld, alors que la variante existe, le hex est dans wireless_stack/ et wireless_stack_config() la mappe. Inaccessible depuis l'UI.

→ Ajouter dans ALL_STACK et passer le tableau en […; 21].

9. if let None = fus {} mort

tab_wireless_stack.rs:302 : bloc vide, vestige de refactor. À supprimer.

🟡 Bugs logiques à confirmer

10. send_double_status continue après succès

tab_wireless_stack.rs:709-737 : la boucle for nb in 0..2 ne break/return jamais sur succès. Le break interne ne sort que de 0..3. Donc en cas de réussite immédiate, la fonction envoie quand même un second STATUS — peut-être l'intention (la FUS attend deux STATUS pour basculer), mais alors le flag success est trompeur, et un échec sur la 2ᵉ passe est ignoré. À clarifier ou corriger.

11. step_flash_fus peut boucler indéfiniment

La séquence StepFlashFUS → UPGRADE → StepUpgradeFUS re-vérifie la version FUS et reflashe si elle reste < 1.2. S'il y a un échec silencieux côté FUS, boucle infinie.

→ Compteur de tentatives FUS (max 3) dans le state.

12. Pas de garde à la fermeture pendant un flash

src/ui/main_window.rs:69-83 ferme la fenêtre dès CloseRequested même si un OpenOCD est en cours → child tué → device potentiellement à moitié flashé.

→ Ignorer CloseRequested quand is_readonly est vrai sur un onglet, ou afficher un dialog.

13. read_line propage les timeouts comme erreurs

tab_wireless_stack.rs:678-689 : tout Err de port.read (y compris TimedOut) fait sortir la fonction avec une erreur. Avec un timeout par défaut à 10 ms et un opérateur qui peut mettre 2-3 s à répondre, le bon fonctionnement repose entièrement sur le sleep(1s) placé avant le read_line dans send_and_read_serial. Risqué pour les futurs ajouts.

14. MainWindow::default() rafraîchit les ports série au boot

tab_wireless_stack.rs:786-799 : Default::default() appelle refresh_serial_ports(). Bloque le démarrage le temps que serialport::available_ports() revienne, et combiné au #3 → panic. Le rafraîchissement explicite sur Window::Opened rend le refresh dans Default redondant : à supprimer.

🟢 Qualité de code

  • Dead code & lints : OperatorStatusResult jamais utilisé, i32 import (open_ocd_task.rs:2) inutile, use std::io::Write ajouté mais jamais appelé. cargo clippy lèverait plusieurs warnings.
  • Doublons : path_ws_file("wb55_operator_no_end.hex") + path_result = tmp/merge.hex + merge_ws_hex(...) est répété mot pour mot dans step_flash_fus et step_flash_fw. Extraire un helper.
  • mpsc::channel(1, …) : capacité 1 force une sérialisation stricte. Passer à 16 améliore les rafales de logs OpenOCD.
  • #[serde(skip)] active_tab : voulu, mais le commentaire // #[derive(Default, Debug)] mort à main_window.rs:18 à enlever.
  • -s scripts dans open_ocd_task.rs:126 : reliquat de l'ancienne archi, à nettoyer.
  • Cargo.toml : features tabs ET tab_bar — on utilise seulement TabBar, pas Tabs. tab_bar seul devrait suffire.
  • README : la section "Run from sources" ne mentionne pas qu'il faut copier configs/ et wireless_stack/ dans target/release/ — sinon cargo run plantera après ce changement de modèle.

🔵 Architecture — observations

  • Bonne idée : générique WithLogMessage pour partager run_command_sender entre les deux onglets.
  • Le state-machine FwStep par message d'auto-transition est élégant pour un flow async iced-friendly. Mériterait un commentaire d'intro qui décrit la séquence en haut de fichier.
  • Les hex bundlés (~150 MB checked-in) : volumineux pour un repo Git. Envisager git-lfs pour wireless_stack/, ou un download au release-time depuis un mirror ST officiel.
  • merge_ws_hex vs Intel HEX : la concaténation brute marche parce que wb55_operator_no_end.hex se termine par \r\n (vérifié) et les fichiers FUS commencent par :. Fragile : si un futur firmware vient sans newline final, les enregistrements fusionnés deviennent invalides. Ajouter un \n de sécurité entre les deux.

Suggestion de priorisation

1, 2, 3 sinon la PR sort cassée. 4, 5, 6, 7, 8 dans la foulée — ~30 min cumulées et ça stabilise vraiment l'UX. Le reste peut suivre en PR de cleanup.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds a new “Wireless Stack” flashing workflow for STM32WB5xxG devices and refactors the GUI into a tabbed MainWindow that hosts both the existing DapLink flow and the new wireless-stack flow.

Changes:

  • Introduce a new Wireless Stack tab that flashes an operator firmware over SWD (OpenOCD), then drives the FUS over serial to install FUS + a selected stack image.
  • Refactor UI architecture into src/ui/* with tab-specific message enums and settings persisted under MainWindow.
  • Rework OpenOCD config/script handling to use configs/ resolved relative to the executable directory and add live OpenOCD log streaming support.

Reviewed changes

Copilot reviewed 20 out of 46 changed files in this pull request and generated 22 comments.

Show a summary per file
File Description
wireless_stack/stm32wb5xxG_FUS_fw_for_fus_0_5_3.hex Adds a bundled precompiled FUS image (for older FUS versions).
src/ui/tab_wireless_stack.rs Implements the Wireless Stack tab UI and flashing/serial/FUS state machine.
src/ui/tab_daplink.rs Refactors DapLink flow to use tab-specific messages under the new MainWindow.
src/ui/mod.rs Exposes new UI modules.
src/ui/messages.rs Adds top-level Message and per-tab message enums + WithLogMessage trait.
src/ui/main_window.rs New tabbed main window, settings load/save, and app event subscription handling.
src/ui/log_widget.rs Adjusts imports for new UI module layout.
src/stackfile_config.rs Defines wireless stack/FUS file enums and filename mapping helpers.
src/operator_tool.rs Adds deserialization structs and helper string mapping for operator/FUS statuses.
src/open_ocd_task.rs Updates OpenOCD script resolution, adds WB55 flashing, and streams logs via optional sender.
src/messages.rs Removes legacy single-tab message enum (replaced by src/ui/messages.rs).
src/main.rs Switches app entrypoint to MainWindow and sets exe_dir at startup.
src/dirs.rs Adds exe_dir concept and resolves configs/ and wireless_stack/ relative to it.
configs/wb5x.cfg Adds OpenOCD config for STM32WB55 over CMSIS-DAP.
configs/f1x-unlock.cfg Adds/renames OpenOCD unlock config under configs/.
configs/f1x-mass-erase.cfg Adds OpenOCD mass erase config under configs/.
configs/f1x-flash.cfg Adds/renames OpenOCD flash config under configs/.
README_fr.md Documents Wireless Stack flow and adds stack metadata table (FR).
README.md Documents Wireless Stack flow and adds stack metadata table (EN).
Cargo.toml Bumps version to 0.2.0; adds serialport; enables iced_aw tab features.
Cargo.lock Updates lockfile for new deps/version bump.
Comments suppressed due to low confidence (1)

src/open_ocd_task.rs:150

  • The stdio variable/thread naming is confusing here: thread_stdout is reading from stderr. Consider renaming the thread/vars to match the actual stream being read to reduce debugging friction.
    let thread_message = mutex_messages.clone();
    let thread_stdout = thread::spawn(move || {
        let lines = BufReader::new(stderr).lines();

        for line in lines {

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/ui/tab_wireless_stack.rs Outdated
Comment on lines +223 to +231
Ok(result) => {
if result.code.is_some() && result.code.unwrap() != 0 {
Self::send_log(&mut o, LogType::Error("Flash failed".into())).await;
Self::send_step(&mut o, FwStep::Ready).await;
} else {
Self::send_logs(&mut o, result.log).await;
Self::send_step(&mut o, FwStep::StepUpgradeFUS).await;
}
}
Copy link

Copilot AI Apr 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If OpenOCD exits without a code (result.code == None, e.g. terminated by signal), this condition treats it as success and proceeds to the next step. Treat None as a failure (log it and return to Ready) instead of continuing.

Suggested change
Ok(result) => {
if result.code.is_some() && result.code.unwrap() != 0 {
Self::send_log(&mut o, LogType::Error("Flash failed".into())).await;
Self::send_step(&mut o, FwStep::Ready).await;
} else {
Self::send_logs(&mut o, result.log).await;
Self::send_step(&mut o, FwStep::StepUpgradeFUS).await;
}
}
Ok(result) => match result.code {
Some(0) => {
Self::send_logs(&mut o, result.log).await;
Self::send_step(&mut o, FwStep::StepUpgradeFUS).await;
}
Some(_) => {
Self::send_log(&mut o, LogType::Error("Flash failed".into())).await;
Self::send_step(&mut o, FwStep::Ready).await;
}
None => {
Self::send_log(
&mut o,
LogType::Error("Flash failed: OpenOCD exited without a status code".into()),
)
.await;
Self::send_step(&mut o, FwStep::Ready).await;
}
},

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 437eb38 — exhaustive match on result.code so Some(0) / Some(other) / None are each handled explicitly. Signal-killed OpenOCD is now treated as a flash failure.

Comment thread src/ui/tab_wireless_stack.rs Outdated
Comment on lines +350 to +355
if result.code.is_some() && result.code.unwrap() != 0 {
Self::send_log(&mut o, LogType::Error("Flash failed".into())).await;
Self::send_step(&mut o, FwStep::Ready).await;
return;
} else {
Self::send_logs(&mut o, result.log).await;
Copy link

Copilot AI Apr 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If OpenOCD exits without a code (result.code == None, e.g. terminated by signal), this check treats it as success. Consider handling None as an error and aborting rather than continuing the FUS upgrade flow.

Suggested change
if result.code.is_some() && result.code.unwrap() != 0 {
Self::send_log(&mut o, LogType::Error("Flash failed".into())).await;
Self::send_step(&mut o, FwStep::Ready).await;
return;
} else {
Self::send_logs(&mut o, result.log).await;
match result.code {
Some(0) => {
Self::send_logs(&mut o, result.log).await;
}
Some(_) => {
Self::send_log(&mut o, LogType::Error("Flash failed".into())).await;
Self::send_step(&mut o, FwStep::Ready).await;
return;
}
None => {
Self::send_log(
&mut o,
LogType::Error("Flash failed: OpenOCD did not exit normally".into()),
)
.await;
Self::send_step(&mut o, FwStep::Ready).await;
return;
}

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 437eb38 (same exhaustive-match rewrite for the FUS flash step).

Comment thread src/open_ocd_task.rs Outdated
const ERASE_SCRIPT: &str = include_str!("../configs/openocd-mass-erase.cfg");
const FLASH_SCRIPT: &str = include_str!("../configs/openocd-flash.cfg");
pub const UNLOCK_SCRIPT_FILENAME: &str = "f1x-unlock.cfg";
pub const ERASE_SCRIPT_FILENAME: &str = "f1x-erase.cfg";
Copy link

Copilot AI Apr 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ERASE_SCRIPT_FILENAME is set to f1x-erase.cfg, but the PR adds configs/f1x-mass-erase.cfg (no f1x-erase.cfg). As a result, erase_target() will look for a non-existent config and fail at runtime. Rename the file or update the constant (and any references) so the filename matches what’s shipped in configs/.

Suggested change
pub const ERASE_SCRIPT_FILENAME: &str = "f1x-erase.cfg";
pub const ERASE_SCRIPT_FILENAME: &str = "f1x-mass-erase.cfg";

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in c492e57ERASE_SCRIPT_FILENAME now matches the shipped f1x-mass-erase.cfg. Good catch, this one would have failed at runtime.

Comment thread src/ui/tab_wireless_stack.rs Outdated
}
});

self.serial_selected = Some(self.serial_available_port[0].clone())
Copy link

Copilot AI Apr 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

refresh_serial_ports() unconditionally sets serial_selected to self.serial_available_port[0]. If available_ports() returns an empty list, this will panic and crash the app. Guard on is_empty() before indexing and leave serial_selected = None when no ports are present.

Suggested change
self.serial_selected = Some(self.serial_available_port[0].clone())
self.serial_selected = self.serial_available_port.first().cloned();

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 1dcd05dself.serial_available_port.first().cloned(). Default::default() on TabWirelessStack no longer calls refresh_serial_ports either.

Comment thread README.md Outdated
5. _(optionnal)_ Send test program

### Stack Wireless
The stack is flashed to the co-processor using the [FUS](https://wiki.st.com/stm32mcu/wiki/Connectivity:STM32WB_FUS) and a program called `operator`, which acts as a relay between high-level comands and the FUS. The `operator` is precompiled, but **is not** the official ST Microelectronics version, the sources are available [here](https://github.com/steamicc/codal-steami-samples/tree/main/samples/Peripherals/FUS_WS_Operator) ([https://github.com/steamicc/codal-steami-samples/tree/main/samples/Peripherals/FUS_WS_Operator](https://github.com/steamicc/codal-steami-samples/tree/main/samples/Peripherals/FUS_WS_Operator)).
Copy link

Copilot AI Apr 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Spelling: comands should be commands.

Suggested change
The stack is flashed to the co-processor using the [FUS](https://wiki.st.com/stm32mcu/wiki/Connectivity:STM32WB_FUS) and a program called `operator`, which acts as a relay between high-level comands and the FUS. The `operator` is precompiled, but **is not** the official ST Microelectronics version, the sources are available [here](https://github.com/steamicc/codal-steami-samples/tree/main/samples/Peripherals/FUS_WS_Operator) ([https://github.com/steamicc/codal-steami-samples/tree/main/samples/Peripherals/FUS_WS_Operator](https://github.com/steamicc/codal-steami-samples/tree/main/samples/Peripherals/FUS_WS_Operator)).
The stack is flashed to the co-processor using the [FUS](https://wiki.st.com/stm32mcu/wiki/Connectivity:STM32WB_FUS) and a program called `operator`, which acts as a relay between high-level commands and the FUS. The `operator` is precompiled, but **is not** the official ST Microelectronics version, the sources are available [here](https://github.com/steamicc/codal-steami-samples/tree/main/samples/Peripherals/FUS_WS_Operator) ([https://github.com/steamicc/codal-steami-samples/tree/main/samples/Peripherals/FUS_WS_Operator](https://github.com/steamicc/codal-steami-samples/tree/main/samples/Peripherals/FUS_WS_Operator)).

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 6c12321 — "comands" → "commands".

Comment thread src/ui/tab_wireless_stack.rs Outdated
Comment on lines +302 to +303
if let None = fus {}

Copy link

Copilot AI Apr 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There’s an empty if let None = fus {} statement that has no effect. It reads like leftover debug/placeholder code and makes the control flow harder to follow; consider removing it.

Suggested change
if let None = fus {}

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 3c6de75 — empty if let None = fus {} removed.

Comment thread src/ui/main_window.rs Outdated
fields_file,
serde_json::to_string_pretty(&self).unwrap_or("{}".into()),
) {
Ok(_) => println!("Settings succesfully saved"),
Copy link

Copilot AI Apr 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo in log message: Settings succesfully saved should be Settings successfully saved.

Suggested change
Ok(_) => println!("Settings succesfully saved"),
Ok(_) => println!("Settings successfully saved"),

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 6c12321 — "succesfully" → "successfully".

Comment thread src/ui/main_window.rs Outdated
DAPLINK_TAB => col.push(self.tab_daplink.view()),
WIRELESS_STACK_TAB => col.push(self.tab_ws.view()),
_ => {
eprintln!("Invalid selectd tab ({})", self.active_tab);
Copy link

Copilot AI Apr 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo in error message: Invalid selectd tab should be Invalid selected tab.

Suggested change
eprintln!("Invalid selectd tab ({})", self.active_tab);
eprintln!("Invalid selected tab ({})", self.active_tab);

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 6c12321 — "selectd" → "selected".

Comment thread src/ui/tab_wireless_stack.rs Outdated
Self::send_log(
o,
LogType::Warning(format!(
"STATUS #{}, attempt #{} failed (Error: {e}.",
Copy link

Copilot AI Apr 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The warning message format string looks unbalanced: it opens (... but doesn’t close the ) (currently ends with Error: {e}.). Consider fixing the punctuation so logs are easier to read.

Suggested change
"STATUS #{}, attempt #{} failed (Error: {e}.",
"STATUS #{}, attempt #{} failed (Error: {e}).",

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 7806b2c (the send_double_status rewrite also closed the parenthesis).

Comment thread src/open_ocd_task.rs
Comment on lines +164 to 166
let thread_message = mutex_messages.clone();
let thread_stderr = thread::spawn(move || {
let lines = BufReader::new(stdout).lines();
Copy link

Copilot AI Apr 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Related to the above: this thread reads stdout but is named thread_stderr, which makes it easy to misinterpret where messages originate. Renaming the thread/vars (or swapping to match) would improve maintainability.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 3c6de75thread_stdout/thread_stderr now actually read from the matching stream, and stdout output is tagged LogType::Info instead of Error (it was painting OpenOCD's normal "Programming Started"/"Verified OK" lines red).

nedseb added 2 commits April 28, 2026 10:55
Without configs/ next to the executable, dirs::get_configs_dir()
errors out on startup since the runtime no longer materializes
OpenOCD scripts from include_str!. Same applies to wireless_stack/
for the WB55 flow. Both folders are now copied into the xpack
release archive (linux + windows).

libudev-dev is required by serialport on Linux when the libudev
backend is enabled (default).
refresh_serial_ports() unconditionally indexed [0] on the freshly
populated Vec, panicking when serialport returns Ok(vec![]) — which
happens on any machine without a connected device. Use first().cloned()
to leave serial_selected as None instead.

Also drop the refresh from TabWirelessStack::default(): it ran during
MainWindow construction (before the iced runtime had a window) and was
already covered by the Window::Opened handler in main_window.rs, which
now refreshes ports unconditionally instead of only when settings load.
nedseb added 6 commits April 28, 2026 11:01
static mut + unsafe access is a hard error in Rust 2024 edition and
emits a lint warning under #[allow(static_mut_refs)] in 2021. Since
the value is written once at startup and read concurrently from iced
tasks, OnceLock<PathBuf> is the right primitive.

Public API unchanged.
Bundles five related fixes:

1. busy-loop: run_command_sender polled child.try_wait() with no
   yield, pinning a CPU during the 15-60s OpenOCD runs. Add a 50ms
   Timer::after between polls.

2. blocking sleeps in async: thread::sleep was called from inside
   async closures driving the wireless stack flow (1s, 1s, 5s) and
   from send_and_read_serial, freezing the iced executor for ~10s
   per FUS+WS cycle. Switch to async_io::Timer::after;
   send_and_read_serial becomes async.

3. log severity: the two pipe-reader threads had their variable names
   swapped (thread_stdout was reading stderr) and stdout output was
   tagged LogType::Error, painting OpenOCD's normal "Programming
   Started"/"Verified OK" lines red. Rename and tag both as Info.

4. BleLld missing from picker: the WirelessStackFile::BleLld variant
   exists, the hex is shipped under wireless_stack/, and
   wireless_stack_config maps it — but it wasn't in ALL_STACK so the
   user couldn't pick it. Added.

5. dead block: drop the leftover `if let None = fus {}` left over
   from a refactor.

Adds async-io as a direct dep (already transitively present).
- Extract prepare_merged_hex helper, dedup'd from step_flash_fus and
  step_flash_fw (path resolution + merge_ws_hex call were copy-pasted).
- Remove the unused OperatorStatusResult struct.
- Drop the dead `-s scripts` arg in run_command_sender (leftover from
  the old script-dir layout).
- Bump message_runner channel capacity from 1 to 16 so OpenOCD log
  bursts don't backpressure-stall the worker.
- Drop unused iced_aw "tabs" feature; we only use TabBar.
- Remove the dead `// #[derive(Default, Debug)]` comment in MainWindow.
The runtime now resolves these folders relative to the executable
(target/debug/easy_flash_daplink), so a fresh checkout fails on
"The configs folder does not exist" until the user wires them up.
STM32WB's SHCI_C2_FUS_GetState swaps CPU2 from the wireless stack to
the FUS on the first call and only returns the real state on the
second — confirmed against the operator firmware sources. So sending
STATUS twice is intentional, both round-trips need to succeed.

The previous flag was hoisted across both passes: if the first pass
succeeded on attempt #1 but the second pass exhausted all 3 retries,
the function still returned Ok and the flow proceeded with the FUS
half-locked. Reset success per pass and bail out as soon as one pass
exhausts its retries.

Also add a doc comment so the "double" name isn't a mystery for the
next reader.
step_flash_fus transitions back to StepUpgradeFUS to verify the
version has actually bumped, which is necessary because some chips
need an intermediate FUS (0.5.3 -> 1.x -> 1.2). But if the reported
version doesn't move forward, the flow re-enters StepFlashFUS
forever.

Track attempts on TabWirelessStack and bail out after 3 with a clear
error. Counter is reset on each Start.

The webusb-wireless-stack reference tool doesn't auto-loop here at
all — the user clicks UPGRADE manually, observes, decides. Our
automation chain needs the bound; this matches the spirit (one
deliberate sequence) without breaking the multi-step FUS path.
nedseb added 5 commits April 28, 2026 11:20
The third check in validate_fields was a copy-paste of the first:
it tested !self.bootloader_path.exists() (already short-circuited
above) instead of !self.user_file_path.exists(), so the warning
about a missing user file never fired.

Also drop the to_str().unwrap() — non-UTF-8 paths panicked the app.
Use as_os_str().len() to detect "user left the field empty" without
allocating or going through UTF-8.

Found by an external review on the main branch.
ERASE_SCRIPT_FILENAME pointed to "f1x-erase.cfg" but the file shipped
under configs/ is named "f1x-mass-erase.cfg" (matching the
openocd-mass-erase.cfg origin). erase_target() therefore failed at
runtime with "no such file or directory" before reaching the chip.

Caught by Copilot's review on PR #5.
Two issues in the WB55 flow flagged by Copilot on PR #5:

- The three flash steps (operator, FUS, WS) treated result.code == None
  as success via `code.is_some() && code.unwrap() != 0`. When OpenOCD is
  killed by signal, code() returns None — currently the flow proceeded
  to the next step on top of a possibly aborted flash. Match exhaustively
  on Some(0) / Some(other) / None and abort on the latter two.

- fus_upgrade_cmd polled the operator's progress JSON over serial and
  printed it via println!, which never reached the in-app log widget.
  Pass the existing TabWsMessage sender through and emit each line as
  LogType::Info so the user sees the FUS state transitions in real time.
dirs::set_exe_dir was hard-wired to current_exe().parent(), which under
cargo run lives in target/debug — so configs/ and wireless_stack/ had
to be symlinked there or the app errored out at startup. The previous
docs change asked users to set those symlinks manually; this restores
the original "just `cargo run`" experience.

Cargo populates CARGO_MANIFEST_DIR at runtime when invoking the binary
via `cargo run`. We prefer it when set, else fall back to the exe's
parent dir for shipped builds where the env var is absent. README
symlink instructions reverted.

Also fix the misleading "Failed to create/update scripts" error message
on current_exe() failure.
- FUS_SATE_* → FUS_STATE_* (3 occurrences)
- "Unknow code" → "Unknown code"
- "Settings succesfully saved" → "Settings successfully saved"
- "Invalid selectd tab" → "Invalid selected tab"
- README EN: "high-level comands" → "commands"
- README FR: "à cette [addresse]" → "[adresse]"
@nedseb nedseb merged commit 8040441 into main Apr 28, 2026
nedseb added a commit that referenced this pull request Apr 28, 2026
ERASE_SCRIPT_FILENAME pointed to "f1x-erase.cfg" but the file shipped
under configs/ is named "f1x-mass-erase.cfg" (matching the
openocd-mass-erase.cfg origin). erase_target() therefore failed at
runtime with "no such file or directory" before reaching the chip.

Caught by Copilot's review on PR #5.
nedseb added a commit that referenced this pull request Apr 28, 2026
Two issues in the WB55 flow flagged by Copilot on PR #5:

- The three flash steps (operator, FUS, WS) treated result.code == None
  as success via `code.is_some() && code.unwrap() != 0`. When OpenOCD is
  killed by signal, code() returns None — currently the flow proceeded
  to the next step on top of a possibly aborted flash. Match exhaustively
  on Some(0) / Some(other) / None and abort on the latter two.

- fus_upgrade_cmd polled the operator's progress JSON over serial and
  printed it via println!, which never reached the in-app log widget.
  Pass the existing TabWsMessage sender through and emit each line as
  LogType::Info so the user sees the FUS state transitions in real time.
@nedseb nedseb deleted the stack_flash branch April 28, 2026 09:44
nedseb added a commit that referenced this pull request Apr 28, 2026
Three issues with the previous CI:

- Triggered only on push-to-main and workflow_dispatch, so PRs were
  never validated before merge — code only got verified after it had
  already landed. PR #5 was effectively merged unchecked.

- Built on ubuntu-latest (currently ubuntu-24.04) while releases
  target both ubuntu-22.04 and ubuntu-24.04. A version-specific
  glibc/libudev issue could break the release without warning.

- No clippy or rustfmt at all, so style and obvious bugs went
  uncaught.

Add `pull_request` trigger, build matrix on the same OSes the release
matrix uses, and a separate lint job running clippy + fmt. The lint
job is `continue-on-error: true` for now because the codebase has 41
clippy warnings and isn't currently fmt-clean — making them blocking
would force a large cleanup PR. They'll show up in the checks tab so
maintainers see the trend, and a follow-up PR can flip them to
blocking once the warnings are addressed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants