Skip to content

feature: Provide WiFi credentials / support for improv#80

Merged
Jason2866 merged 13 commits intofactoryfrom
improv
Apr 5, 2026
Merged

feature: Provide WiFi credentials / support for improv#80
Jason2866 merged 13 commits intofactoryfrom
improv

Conversation

@Jason2866
Copy link
Copy Markdown
Owner

@Jason2866 Jason2866 commented Apr 5, 2026

Summary by CodeRabbit

  • New Features

    • Improv Wi‑Fi provisioning modal added to main window; modal disables console input while active, shows scanning, provisioning progress, and final status.
    • Device Info dialog displays live device details, polls for state with timeout handling, and shows network scan results in real time.
  • Bug Fixes

    • Improved serial reader shutdown to suppress stray background signals and clear stale thread references.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 5, 2026

📝 Walkthrough

Walkthrough

Adds Improv Wi‑Fi provisioning support: new esp_flasher/improv.py implements the Improv serial protocol and ImprovManager; esp_flasher/gui.py adds DeviceInfoDialog, ImprovDialog, and MainWindow.open_improv() to drive provisioning via an open serial port; SerialReader shutdown is hardened.

Changes

Cohort / File(s) Summary
Improv protocol & manager
esp_flasher/improv.py
New module implementing Improv packet framing, checksums, TLV parsing, RPC builders/parsers, a byte‑level background reader state machine, per‑request synchronization/events/timeouts, and PyQt signals for state, errors, device info, wifi networks, provisioning results, and logs.
GUI — dialogs & integration
esp_flasher/gui.py
Added DeviceInfoDialog(QDialog) and ImprovDialog(QDialog) that reuse an already-open serial port, instantiate/manage ImprovManager, poll device state with a QTimer, run Wi‑Fi scans and provisioning on background threads (with signals for results/failures), manage UI transitions (ready→provisioning→provisioned), and added MainWindow.open_improv() plus an "Improv WiFi" button which pauses the SerialReader, shows the modal dialog, then recreates/restarts SerialReader on close.
Serial reader shutdown
esp_flasher/serial_console.py
SerialReader gains a _muted flag to suppress background-thread signal emissions during shutdown; read loop and line extraction gate on self.running; exceptions during read respect _muted; stop() joins and clears self.thread.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant MainWindow
    participant ImprovDialog
    participant ImprovManager
    participant SerialPort
    participant Device as "Device\n(via Serial)"

    User->>MainWindow: Click "Improv WiFi"
    MainWindow->>MainWindow: Stop SerialReader thread
    MainWindow->>ImprovDialog: Create & show modal dialog

    ImprovDialog->>ImprovManager: Init with open serial port
    ImprovManager->>ImprovManager: Start background reader thread

    ImprovDialog->>ImprovManager: request_current_state()
    ImprovManager->>SerialPort: Send CURRENT_STATE RPC packet
    SerialPort->>Device: Transmit packet
    Device->>SerialPort: Respond with state packet
    SerialPort->>ImprovManager: Receive & parse packet
    ImprovManager->>ImprovDialog: Emit state_changed

    User->>ImprovDialog: Trigger WiFi scan
    ImprovDialog->>ImprovManager: request_wifi_networks()
    ImprovManager->>SerialPort: Send WiFi scan RPC
    Device->>SerialPort: Send network list packets
    SerialPort->>ImprovManager: Parse networks
    ImprovManager->>ImprovDialog: Emit wifi_networks_received

    User->>ImprovDialog: Enter creds & click "Provision"
    ImprovDialog->>ImprovManager: send_wifi_settings(ssid,password)
    ImprovManager->>SerialPort: Send provisioning RPC (TLV)
    Device->>SerialPort: Send provision result
    SerialPort->>ImprovManager: Parse result
    ImprovManager->>ImprovDialog: Emit provisioned / error

    User->>ImprovDialog: Close dialog
    ImprovDialog->>ImprovManager: stop()
    ImprovManager->>ImprovManager: Stop reader thread
    ImprovDialog->>MainWindow: Return control
    MainWindow->>MainWindow: Restart SerialReader on same port
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰
I nibble bytes and hop through frames with glee,
I chase the SSIDs so devices roam free.
With checksums checked and threads all neat,
I bring a network to your tiny fleet.
Hooray—provisioned! Time for carrot tea. 🥕✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 55.32% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feature: Provide WiFi credentials / support for improv' clearly and specifically describes the main changes introduced in the PR, which add WiFi provisioning functionality via the Improv protocol.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch improv

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@esp_flasher/gui.py`:
- Around line 48-50: The scan button triggers _scan_networks which spawns daemon
threads while ImprovManager.request_wifi_networks reuses the same
accumulator/event pair, allowing results to overlap; fix by preventing
re-entrant scans: in the GUI disable the Scan Networks button (or set a
scan_in_progress flag) at start of _scan_networks and re-enable it when that
scan thread completes, and in esp_flasher/improv.py change request_wifi_networks
to allocate a fresh accumulator and threading.Event (do not reuse any shared
accumulator/event), or protect shared state with a lock and return immediately
if a scan is already running; update references in _scan_networks and
ImprovManager.request_wifi_networks accordingly so each request uses its own
accumulator/event or is serialized.

In `@esp_flasher/improv.py`:
- Around line 143-146: The _rpc_command field is set but never used to match
incoming TYPE_RPC_RESULT packets, so concurrent RPCs (e.g.,
ImprovDialog._on_state_changed() triggering request_device_info() while
send_wifi_settings() is waiting) can race and wake the wrong waiter; change
_send_rpc() and related RPC code paths (references: _rpc_command, _rpc_event,
_rpc_result, _rpc_error, and the _send_rpc method) to serialize RPC calls
(single outstanding RPC) and on receiving TYPE_RPC_RESULT verify the result
correlates to the pending _rpc_command before satisfying the waiter—if the
incoming result does not match, either queue it for the matching command or
ignore it and leave the current waiter blocked; apply the same fix patterns to
the other RPC usage blocks noted (around the sections corresponding to lines
198-215 and 324-349) so only the matching command wakes its waiter.
- Around line 175-188: request_wifi_networks (and _send_rpc used by
request_device_info) can block forever because _write() only logs failures and
callers still wait on events; modify the call sites so write errors are
propagated instead of silently logging: have _write return success/failure (or
raise) and in request_wifi_networks check the result of _write(pkt) and, on
failure, set/notify _wifi_scan_done and return an empty list or raise a specific
exception; similarly update _send_rpc to detect failed writes when sending the
RPC (use the same return/exception behavior) so request_device_info won't block;
reference symbols: request_wifi_networks, _wifi_scan_done, _wifi_networks,
CMD_REQUEST_WIFI_NETWORKS, _send_rpc, and _write.
- Around line 281-285: The else branch incorrectly sets self._is_improv to None
which resumes header scanning immediately; change it to enter “skip-line” mode
by setting self._is_improv = False (keeping self._line = []), so the state
machine will ignore the rest of the current console line until a newline is
seen; update the else block in improv.py where _line and _is_improv are reset
accordingly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 6d86c072-1b44-4107-92d8-7d93dd9c8c90

📥 Commits

Reviewing files that changed from the base of the PR and between ba75eb9 and 04dea8e.

📒 Files selected for processing (3)
  • esp_flasher/gui.py
  • esp_flasher/improv.py
  • esp_flasher/serial_console.py

Comment thread esp_flasher/gui.py Outdated
Comment thread esp_flasher/improv.py
Comment thread esp_flasher/improv.py Outdated
Comment thread esp_flasher/improv.py Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

♻️ Duplicate comments (4)
esp_flasher/gui.py (1)

48-50: ⚠️ Potential issue | 🟠 Major

Prevent overlapping Wi‑Fi scans from the dialog.

Line 196 launches a new scan thread on every click, and scan completion is handled through shared state; rapid re-clicks can interleave results and completion handling.

🛠️ Proposed fix
-        scan_btn = QPushButton("Scan Networks")
-        scan_btn.clicked.connect(self._scan_networks)
-        net_layout.addWidget(scan_btn)
+        self.scan_btn = QPushButton("Scan Networks")
+        self.scan_btn.clicked.connect(self._scan_networks)
+        net_layout.addWidget(self.scan_btn)
+        self._scan_in_progress = False
@@
     def _scan_networks(self):
+        if self._scan_in_progress:
+            return
+        self._scan_in_progress = True
+        self.scan_btn.setEnabled(False)
         self.network_list.clear()
         self.status_label.setText("Scanning WiFi networks...")
         self.progress.setVisible(True)
         threading.Thread(target=self._scan_bg, daemon=True).start()
@@
     def _update_network_list(self, networks):
+        self._scan_in_progress = False
+        self.scan_btn.setEnabled(True)
         self.network_list.clear()
         self.progress.setVisible(False)

Also applies to: 196-211

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@esp_flasher/gui.py` around lines 48 - 50, The scan button currently starts a
new thread each click via _scan_networks which can cause overlapping scans and
race conditions when updating shared state; modify _scan_networks / the scan
worker logic to prevent concurrent scans by tracking a boolean/in-progress flag
(e.g., self._scan_in_progress) or using a threading.Lock, disable the scan_btn
at start of a scan and re-enable it only in the scan completion handler (e.g.,
_on_scan_complete) after moving GUI updates to the main thread via
signals/slots, and ensure any shared state used to collect results is
accessed/updated thread-safely so rapid clicks cannot interleave results.
esp_flasher/improv.py (3)

305-309: ⚠️ Potential issue | 🟠 Major

Keep skip-line mode after a 9-byte header mismatch.

At Line 308, resetting to None resumes scanning too early; remaining bytes on the same non-Improv line can be misparsed as a packet start.

🛠️ Proposed fix
         else:
             # Not an Improv header — discard and go back to scanning
             # (JS does: isImprov = false, line = [])
             self._line = []
-            self._is_improv = None
+            self._is_improv = False
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@esp_flasher/improv.py` around lines 305 - 309, The code resets
self._is_improv to None after a 9-byte header mismatch, which resumes scanning
too early and can misparse remaining bytes; change the reset behavior so that
after discarding the tentative header you clear the buffer (self._line = []) but
set self._is_improv = False (retain "skip-line" mode) so the parser continues
skipping the rest of that non‑Improv line until a newline resets state.

142-147: ⚠️ Potential issue | 🟠 Major

Serialize RPC calls and correlate TYPE_RPC_RESULT with the pending command.

_rpc_command is assigned (Line 204) but not enforced when unblocking waiters. Concurrent RPCs can wake the wrong waiter and return mismatched results.

🛠️ Proposed fix
 class ImprovManager(QObject):
@@
         self._rpc_event = threading.Event()
         self._rpc_result = None
         self._rpc_error = None
         self._rpc_command = None
+        self._rpc_call_lock = threading.Lock()
+        self._rpc_state_lock = threading.Lock()
@@
     def _send_rpc(self, command, payload=b"", timeout=None):
@@
-        self._rpc_event.clear()
-        self._rpc_result = None
-        self._rpc_error = None
-        self._rpc_command = command
-
-        pkt = _build_rpc(command, payload)
-        self._write(pkt)
-
-        if self._rpc_event.wait(timeout=timeout):
-            if self._rpc_error is not None and self._rpc_error != ERROR_NONE:
-                return None
-            return self._rpc_result
+        with self._rpc_call_lock:
+            with self._rpc_state_lock:
+                self._rpc_event.clear()
+                self._rpc_result = None
+                self._rpc_error = None
+                self._rpc_command = command
+
+            pkt = _build_rpc(command, payload)
+            if not self._write(pkt):
+                with self._rpc_state_lock:
+                    self._rpc_command = None
+                return None
+
+            if self._rpc_event.wait(timeout=timeout):
+                with self._rpc_state_lock:
+                    rpc_error = self._rpc_error
+                    rpc_result = self._rpc_result
+                    self._rpc_command = None
+                if rpc_error is not None and rpc_error != ERROR_NONE:
+                    return None
+                return rpc_result
+
+            with self._rpc_state_lock:
+                self._rpc_command = None
         self.log_message.emit("RPC timeout")
         self.error_received.emit(ERROR_TIMEOUT)
         return None
@@
         elif msg_type == TYPE_RPC_RESULT:
@@
-            if command == CMD_REQUEST_INFO:
+            with self._rpc_state_lock:
+                pending = self._rpc_command
+            if pending is not None and command != pending:
+                return
+
+            if command == CMD_REQUEST_INFO:

Also applies to: 198-215, 347-376


161-167: ⚠️ Potential issue | 🟠 Major

Don’t wait indefinitely when writes fail or shutdown begins.

Line 217 _write() only logs failures; callers still block on events (Lines 187, 209). Also, stop() (Lines 161-167) does not release those waiters.

🛠️ Proposed fix
     def stop(self):
         """Stop the Improv receiver thread."""
         self._running = False
+        self._wifi_scan_done.set()
+        self._rpc_error = ERROR_TIMEOUT
+        self._rpc_event.set()
         if self._thread:
             self._thread.join(timeout=3.0)
             self._thread = None
@@
     def request_wifi_networks(self):
@@
         self._wifi_networks = []
         self._wifi_scan_done.clear()
         pkt = _build_rpc(CMD_REQUEST_WIFI_NETWORKS)
-        self._write(pkt)
+        if not self._write(pkt):
+            return []
         self._wifi_scan_done.wait()
         return list(self._wifi_networks)
@@
     def _write(self, data):
         """Write data to the serial port."""
         try:
             if self._port and self._port.is_open:
                 self._port.write(data)
                 self._port.flush()
+                return True
         except Exception as e:
             logger.error("Improv write error: %s", e)
             self.log_message.emit(f"Write error: {e}")
+        return False

Also applies to: 180-188, 198-208, 217-226

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@esp_flasher/improv.py` around lines 161 - 167, The _write() method currently
only logs failures so callers that wait on Events remain blocked, and stop()
only clears _running and joins the thread without releasing those waiters;
update _write() to set/notify any Event(s) or condition variables it uses (so
callers unblock) when a write fails or when _running is False, and modify stop()
to explicitly set/notify those same Event(s) or conditions before joining the
thread (and ensure any blocking waits use a timeout or check _running to avoid
indefinite blocking). Locate the relevant synchronization primitives referenced
by _write() and other waiters and ensure both failure paths and stop() trigger
them (names: _write, stop, and the Event/Condition attributes used by the class)
so no callers remain stuck.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@esp_flasher/gui.py`:
- Around line 48-50: The scan button currently starts a new thread each click
via _scan_networks which can cause overlapping scans and race conditions when
updating shared state; modify _scan_networks / the scan worker logic to prevent
concurrent scans by tracking a boolean/in-progress flag (e.g.,
self._scan_in_progress) or using a threading.Lock, disable the scan_btn at start
of a scan and re-enable it only in the scan completion handler (e.g.,
_on_scan_complete) after moving GUI updates to the main thread via
signals/slots, and ensure any shared state used to collect results is
accessed/updated thread-safely so rapid clicks cannot interleave results.

In `@esp_flasher/improv.py`:
- Around line 305-309: The code resets self._is_improv to None after a 9-byte
header mismatch, which resumes scanning too early and can misparse remaining
bytes; change the reset behavior so that after discarding the tentative header
you clear the buffer (self._line = []) but set self._is_improv = False (retain
"skip-line" mode) so the parser continues skipping the rest of that non‑Improv
line until a newline resets state.
- Around line 161-167: The _write() method currently only logs failures so
callers that wait on Events remain blocked, and stop() only clears _running and
joins the thread without releasing those waiters; update _write() to set/notify
any Event(s) or condition variables it uses (so callers unblock) when a write
fails or when _running is False, and modify stop() to explicitly set/notify
those same Event(s) or conditions before joining the thread (and ensure any
blocking waits use a timeout or check _running to avoid indefinite blocking).
Locate the relevant synchronization primitives referenced by _write() and other
waiters and ensure both failure paths and stop() trigger them (names: _write,
stop, and the Event/Condition attributes used by the class) so no callers remain
stuck.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f62e0cf6-d7fc-4a68-b26f-a7164342cb4b

📥 Commits

Reviewing files that changed from the base of the PR and between 04dea8e and 607360c.

📒 Files selected for processing (3)
  • esp_flasher/gui.py
  • esp_flasher/improv.py
  • esp_flasher/serial_console.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • esp_flasher/serial_console.py

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (3)
esp_flasher/gui.py (2)

590-600: Redundant thread join after stop().

The stop() method already calls self.thread.join(timeout=1.0) internally (per esp_flasher/serial_console.py Lines 43-49). The additional join on Lines 593-594 is redundant and may reference a None thread since stop() sets self.thread = None.

♻️ Proposed simplification
         if self._serial_reader:
             self._serial_reader.stop()  # sets _muted=True and running=False, joins thread
-            # Verify the thread is truly dead before handing the port to Improv
-            if self._serial_reader.thread and self._serial_reader.thread.is_alive():
-                self._serial_reader.thread.join(timeout=2.0)
             self._serial_reader.line_received.disconnect(self.append_log_line)
             self._serial_reader.error_occurred.disconnect(self.handle_serial_error)
             # Flush any already-queued cross-thread events so they are discarded
             from PyQt6.QtWidgets import QApplication
             QApplication.processEvents()
             self._serial_reader = None
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@esp_flasher/gui.py` around lines 590 - 600, The extra join after calling
self._serial_reader.stop() is redundant and unsafe because SerialReader.stop()
already joins and sets self.thread = None; remove the block that checks
self._serial_reader.thread and calls join(timeout=2.0), and simply call
self._serial_reader.stop(), then disconnect the signals
(self._serial_reader.line_received.disconnect and
self._serial_reader.error_occurred.disconnect) and flush events with
QApplication.processEvents() before setting self._serial_reader = None to avoid
referencing a None thread.

103-107: Consider narrowing the exception type for serial port operations.

The broad Exception catch is flagged by static analysis (Ruff BLE001). While serial operations can fail in various ways, narrowing to more specific exception types improves debuggability.

♻️ Proposed fix
         try:
             self._serial_port.reset_input_buffer()
-        except Exception as e:
+        except (OSError, serial.SerialException) as e:
             self.status_label.setText(f"Port error: {e}")
             return

This requires adding import serial at the top of the file or using a local import.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@esp_flasher/gui.py` around lines 103 - 107, The try/except around
self._serial_port.reset_input_buffer() is catching a broad Exception; narrow it
to serial-specific errors by importing the serial module (add `import serial` at
top or do a local import) and catch serial.SerialException (and optionally
serial.SerialTimeoutException) instead, then update the status_label.setText to
include the caught exception message for context while returning. Ensure the
exception handler references the same method name
self._serial_port.reset_input_buffer() so the change is localized.
esp_flasher/improv.py (1)

244-247: Consider logging exceptions when setting serial timeout.

The static analysis (Ruff S110) flags the silent try-except-pass patterns. While these are intentional fallbacks, logging at debug level would aid troubleshooting.

♻️ Proposed fix
         try:
             self._port.timeout = 0.1
-        except Exception:
-            pass
+        except Exception as e:
+            logger.debug("Could not set serial timeout: %s", e)

Apply the same pattern to the finally block (Lines 272-275).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@esp_flasher/improv.py` around lines 244 - 247, The silent try/except around
setting self._port.timeout should capture the exception as e and log it at debug
level instead of passing; update the block that sets self._port.timeout (and the
symmetric timeout-reset in the finally block) to use "except Exception as e:
logger.debug(...)" (or the module/class logger in this file) so any failure to
adjust the serial timeout is recorded for troubleshooting while preserving the
fallback behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@esp_flasher/gui.py`:
- Around line 590-600: The extra join after calling self._serial_reader.stop()
is redundant and unsafe because SerialReader.stop() already joins and sets
self.thread = None; remove the block that checks self._serial_reader.thread and
calls join(timeout=2.0), and simply call self._serial_reader.stop(), then
disconnect the signals (self._serial_reader.line_received.disconnect and
self._serial_reader.error_occurred.disconnect) and flush events with
QApplication.processEvents() before setting self._serial_reader = None to avoid
referencing a None thread.
- Around line 103-107: The try/except around
self._serial_port.reset_input_buffer() is catching a broad Exception; narrow it
to serial-specific errors by importing the serial module (add `import serial` at
top or do a local import) and catch serial.SerialException (and optionally
serial.SerialTimeoutException) instead, then update the status_label.setText to
include the caught exception message for context while returning. Ensure the
exception handler references the same method name
self._serial_port.reset_input_buffer() so the change is localized.

In `@esp_flasher/improv.py`:
- Around line 244-247: The silent try/except around setting self._port.timeout
should capture the exception as e and log it at debug level instead of passing;
update the block that sets self._port.timeout (and the symmetric timeout-reset
in the finally block) to use "except Exception as e: logger.debug(...)" (or the
module/class logger in this file) so any failure to adjust the serial timeout is
recorded for troubleshooting while preserving the fallback behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 732d592a-fb99-4a3b-8e11-37edcffd9c38

📥 Commits

Reviewing files that changed from the base of the PR and between 607360c and 1dd8311.

📒 Files selected for processing (2)
  • esp_flasher/gui.py
  • esp_flasher/improv.py

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

♻️ Duplicate comments (1)
esp_flasher/improv.py (1)

365-389: ⚠️ Potential issue | 🟠 Major

Don’t drop Wi‑Fi scan packets behind the global RPC mismatch guard.

Lines 372-377 return early for any TYPE_RPC_RESULT whose command does not equal _rpc_command. Wi-Fi scan results use the same packet type, so a concurrent CMD_REQUEST_INFO or CMD_SEND_WIFI_SETTINGS makes the scan stream look “mismatched” and request_wifi_networks() can sit until timeout. ImprovDialog._on_state_changed() auto-requests device info, so this is reachable in the current GUI flow.

Patch suggestion
         elif msg_type == TYPE_RPC_RESULT:
             if not data:
                 return
             command = data[0]
             strings = _parse_tlv_strings(data)
 
+            if command == CMD_REQUEST_WIFI_NETWORKS:
+                if not strings:
+                    self.log_message.emit(
+                        f"WiFi scan complete: {len(self._wifi_networks)} networks"
+                    )
+                    self._wifi_scan_done.set()
+                elif len(strings) >= 3:
+                    try:
+                        rssi = int(strings[1])
+                    except (ValueError, IndexError):
+                        rssi = 0
+                    self._wifi_networks.append(
+                        (strings[0], rssi, strings[2] == "YES")
+                    )
+                return
+
             # Verify result matches pending RPC command to prevent race conditions
             if self._rpc_command is not None and command != self._rpc_command:
                 logger.warning(
                     "Ignoring RPC result for command %d (expected %d)",
                     command, self._rpc_command
                 )
                 return
@@
-            elif command == CMD_REQUEST_WIFI_NETWORKS:
-                if not strings:
-                    # Empty result = scan complete (matches JS: receivedData done)
-                    self.log_message.emit(f"WiFi scan complete: {len(self._wifi_networks)} networks")
-                    self._wifi_scan_done.set()
-                else:
-                    if len(strings) >= 3:
-                        try:
-                            rssi = int(strings[1])
-                        except (ValueError, IndexError):
-                            rssi = 0
-                        self._wifi_networks.append(
-                            (strings[0], rssi, strings[2] == "YES")
-                        )
-
             elif command == CMD_SEND_WIFI_SETTINGS:
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@esp_flasher/improv.py` around lines 365 - 389, The early return in the
TYPE_RPC_RESULT branch is too aggressive: it ignores any RPC result whose
command != self._rpc_command, which drops interleaved Wi‑Fi scan packets; modify
the handler in improv.py so CMD_REQUEST_WIFI_NETWORKS is processed regardless of
self._rpc_command (i.e., check and handle command == CMD_REQUEST_WIFI_NETWORKS
before the global mismatch guard or whitelist it), ensuring you still keep the
mismatch warning for other commands; preserve existing behavior of appending to
self._wifi_networks, emitting log_message, setting self._wifi_scan_done, and
only set self._rpc_result/_rpc_event when command == self._rpc_command.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@esp_flasher/gui.py`:
- Around line 265-278: The background threads (_request_info_bg, _scan_bg,
_provision_bg) currently dereference self._improv after being started, which can
race with the dialog teardown that clears self._improv; fix this by capturing a
stable reference to self._improv before starting each thread and using that
captured reference inside the worker instead of accessing self._improv directly
(e.g., read improv = self._improv and pass it into the thread via args or wrap
the worker in a closure that closes over improv); apply this pattern to the
Thread starts that invoke _request_info_bg, _scan_bg, and _provision_bg so the
detached workers never read the attribute after it may have been cleared.
- Around line 215-219: The except clause in the reset_input_buffer handling
references serial.SerialException but serial is only imported locally inside
MainWindow.connect_to_port(); add a top-level import for the serial module
(e.g., import serial) at module scope so serial.SerialException is defined when
the except runs, or alternatively import SerialException into the module
namespace and remove the local import in MainWindow.connect_to_port(); update
references accordingly (target symbols: serial.SerialException,
MainWindow.connect_to_port, self._serial_port.reset_input_buffer).

In `@esp_flasher/improv.py`:
- Around line 184-192: The request_wifi_networks caller (and other RPCs like
request_device_info) can block forever if the receiver thread dies because
waiters (_wifi_scan_done and others) are never woken and subsequent RPCs queue
behind a locked _rpc_lock; to fix this, add a shared "receiver_dead" flag/state
that the read/receiver loop sets when it exits on a closed port or read error,
and ensure the receiver exit path also sets this flag and signals/wakes all
pending waiters (e.g., call _wifi_scan_done.set() and any other
condition/events) and releases/notifies any RPC waiters so they immediately
fail; then update request_wifi_networks (and other request_* methods that wait)
to check the receiver_dead flag early and raise a clear exception (e.g.,
IOError/RuntimeError) instead of blocking, and ensure the _rpc_lock acquisition
path respects the receiver_dead state so queued RPCs fail fast.

---

Duplicate comments:
In `@esp_flasher/improv.py`:
- Around line 365-389: The early return in the TYPE_RPC_RESULT branch is too
aggressive: it ignores any RPC result whose command != self._rpc_command, which
drops interleaved Wi‑Fi scan packets; modify the handler in improv.py so
CMD_REQUEST_WIFI_NETWORKS is processed regardless of self._rpc_command (i.e.,
check and handle command == CMD_REQUEST_WIFI_NETWORKS before the global mismatch
guard or whitelist it), ensuring you still keep the mismatch warning for other
commands; preserve existing behavior of appending to self._wifi_networks,
emitting log_message, setting self._wifi_scan_done, and only set
self._rpc_result/_rpc_event when command == self._rpc_command.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 2d09edcf-6d99-44e8-8aaa-6ae564e59ecb

📥 Commits

Reviewing files that changed from the base of the PR and between 1dd8311 and abd41a1.

📒 Files selected for processing (2)
  • esp_flasher/gui.py
  • esp_flasher/improv.py

Comment thread esp_flasher/gui.py
Comment thread esp_flasher/gui.py Outdated
Comment thread esp_flasher/improv.py Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
esp_flasher/improv.py (1)

311-314: Consider logging the exception when restoring serial timeout.

The try-except-pass pattern here is acceptable for defensive cleanup, but logging could help with debugging rare edge cases.

♻️ Optional: Add debug logging
             try:
                 self._port.timeout = old_timeout
-            except Exception:
-                pass
+            except Exception as e:
+                logger.debug("Could not restore serial timeout: %s", e)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@esp_flasher/improv.py` around lines 311 - 314, Replace the silent except in
the cleanup that restores self._port.timeout so failures are logged for
debugging: change the except to "except Exception as e" and emit a
debug/exception log that includes the old_timeout and the caught exception. Use
the class logger if available (e.g., self.logger or self._logger), otherwise use
the module logger via logging.exception or logging.debug, and preserve the
original behavior by not re-raising the exception.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@esp_flasher/gui.py`:
- Around line 329-345: _guard against using a None _improv in network
scan/provision flows by checking self._improv before launching background work:
in _scan_networks (and similarly in _provision) return early and reset UI state
(re-enable scan_btn, hide progress, set status_label) if self._improv is None or
_start_improv failed; do not start the thread calling _scan_bg when self._improv
is None so _scan_bg never receives None and avoids calling
improv.request_wifi_networks(); keep using the _scan_finished signal to update
the UI when a valid improv exists.

---

Nitpick comments:
In `@esp_flasher/improv.py`:
- Around line 311-314: Replace the silent except in the cleanup that restores
self._port.timeout so failures are logged for debugging: change the except to
"except Exception as e" and emit a debug/exception log that includes the
old_timeout and the caught exception. Use the class logger if available (e.g.,
self.logger or self._logger), otherwise use the module logger via
logging.exception or logging.debug, and preserve the original behavior by not
re-raising the exception.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 38deaffe-b44a-4815-b946-3e0371798615

📥 Commits

Reviewing files that changed from the base of the PR and between abd41a1 and f44598e.

📒 Files selected for processing (2)
  • esp_flasher/gui.py
  • esp_flasher/improv.py

Comment thread esp_flasher/gui.py Outdated
Repository owner deleted a comment from coderabbitai Bot Apr 5, 2026
@Jason2866
Copy link
Copy Markdown
Owner Author

@coderabbitai full review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 5, 2026

✅ Actions performed

Full review triggered.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

♻️ Duplicate comments (1)
esp_flasher/improv.py (1)

187-201: ⚠️ Potential issue | 🟠 Major

Wi‑Fi scans still bypass the serialized RPC path.

_handle_packet() now drops any TYPE_RPC_RESULT whose command does not match _rpc_command, but request_wifi_networks() never acquires _rpc_lock or claims _rpc_command. If the automatic info request is still in flight, scan results are ignored and the scan times out even though the device replied.

Also applies to: 403-409, 417-421

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@esp_flasher/improv.py` around lines 187 - 201, request_wifi_networks
currently sends the RPC without reserving the serialized RPC slot so responses
can be dropped by _handle_packet; fix by acquiring the _rpc_lock, set
self._rpc_command = CMD_REQUEST_WIFI_NETWORKS before calling
_write(_build_rpc(...)), then use try/finally to clear self._rpc_command and
release the lock after the wait (preserving behavior of _wifi_scan_done and
_wifi_networks); apply the same locking/preset-then-clear pattern to the other
similar scan/info methods referenced (the blocks at the other occurrences).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@esp_flasher/gui.py`:
- Around line 343-348: Wrap calls to ImprovManager methods (e.g.,
improv.request_wifi_networks(), improv.request_device_info(),
improv.send_wifi_settings()) inside the worker thread functions (_scan_bg and
the other background methods referenced) with try/except that catches exceptions
raised when the receiver dies; on exception, log or record the error and always
emit the UI-reset signal used by the main thread (for _scan_bg emit
self._scan_finished with an empty list or an error sentinel, and in the other
background functions emit their respective completion/reset signals) so the
dialog unblocks and controls are re-enabled even if the Improv call fails.

In `@esp_flasher/improv.py`:
- Around line 182-185: request_device_info (and other auto-probe callers in the
211-247 range) can block indefinitely inside _send_rpc while holding _rpc_lock,
preventing later send_wifi_settings from sending; change _send_rpc (or its
callers) so the lock is only held while accessing the transport to send the
request and register the response waiter, then release _rpc_lock before waiting
for the response (or enforce a bounded timeout and ensure _rpc_lock is released
in a finally block). Update request_device_info to pass a sensible timeout or
rely on the revised _send_rpc behavior so background probes cannot hold
_rpc_lock forever and subsequent send_wifi_settings calls can proceed.

---

Duplicate comments:
In `@esp_flasher/improv.py`:
- Around line 187-201: request_wifi_networks currently sends the RPC without
reserving the serialized RPC slot so responses can be dropped by _handle_packet;
fix by acquiring the _rpc_lock, set self._rpc_command =
CMD_REQUEST_WIFI_NETWORKS before calling _write(_build_rpc(...)), then use
try/finally to clear self._rpc_command and release the lock after the wait
(preserving behavior of _wifi_scan_done and _wifi_networks); apply the same
locking/preset-then-clear pattern to the other similar scan/info methods
referenced (the blocks at the other occurrences).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 013bf24d-085c-4a7b-b704-bbd34a12e806

📥 Commits

Reviewing files that changed from the base of the PR and between ba75eb9 and 4e56016.

📒 Files selected for processing (3)
  • esp_flasher/gui.py
  • esp_flasher/improv.py
  • esp_flasher/serial_console.py

Comment thread esp_flasher/gui.py Outdated
Comment thread esp_flasher/improv.py Outdated
Comment thread esp_flasher/serial_console.py
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@esp_flasher/gui.py`:
- Around line 350-351: The exception handlers in _scan_bg, _request_info_bg, and
_provision_bg use an undefined logger causing NameError; fix by adding a
module-level logger (e.g., import logging and create logger =
logging.getLogger(__name__)) at top of the module or by replacing those
logger.error(...) calls with print(...) if you prefer no logging infrastructure,
then update the three exception blocks to use that module-level logger
identifier to avoid masking the original exceptions.

In `@esp_flasher/improv.py`:
- Around line 210-212: Remove the duplicate unreachable return statement that
repeats "return list(self._wifi_networks)"; keep the single return and ensure
any preceding side-effects (e.g., setting self._rpc_command = None) remain
before that return. Locate the method using self._rpc_command and
self._wifi_networks, delete the extraneous second "return
list(self._wifi_networks)" so there is only one return at the end of the
function.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: fd827cd6-06bb-4dfb-9664-2ae8070ee87c

📥 Commits

Reviewing files that changed from the base of the PR and between abd41a1 and ef306c1.

📒 Files selected for processing (2)
  • esp_flasher/gui.py
  • esp_flasher/improv.py

Comment thread esp_flasher/gui.py
Comment thread esp_flasher/improv.py Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (1)
esp_flasher/improv.py (1)

194-206: ⚠️ Potential issue | 🔴 Critical

Do not release the shared RPC slot before the matching response arrives.

These paths still use one global waiter (_rpc_command, _rpc_event, _rpc_result, _wifi_networks), but they drop _rpc_lock immediately after writing. A later request_device_info(), request_wifi_networks(), or send_wifi_settings() can overwrite that shared state while the first call is still waiting, and _handle_packet() then drops the first response as a mismatch on Lines 420-425 or wakes the wrong waiter. esp_flasher/gui.py now issues these calls from separate threads, so this race is reachable in normal use.

With the current single-slot design, keep the lock held until the response/timeout is consumed, or replace the shared waiter fields with per-request state before allowing overlap.

Also applies to: 230-263, 419-425

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@esp_flasher/improv.py` around lines 194 - 206, The code releases the shared
RPC slot (_rpc_lock) immediately after sending a request which allows other
threads calling request_device_info(), request_wifi_networks(), or
send_wifi_settings() to overwrite shared waiter state (_rpc_command, _rpc_event,
_rpc_result, _wifi_networks) before the matching response arrives; either hold
_rpc_lock across the send-and-wait sequence (acquire before setting
_rpc_command/_rpc_event/_rpc_result, send via _write(pkt), then wait on
_rpc_event/_wifi_scan_done and only release the lock after the wait completes or
times out) or refactor to allocate per-request waiter state (create a
request-local object with its own event/result and pass an identifier in
_rpc_command so _handle_packet() can route responses to the correct waiter) and
update _handle_packet() to match responses to that per-request state instead of
the global fields.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@esp_flasher/gui.py`:
- Around line 67-79: The code inserts unescaped device-supplied strings into
RichText, enabling HTML injection; import the html module at top-level and
replace the direct interpolation of val when constructing the QLabel in the loop
(where self._device_info and labels are used to create info_row) with an escaped
version (use html.escape(val)) so the QLabel receives safe text, then keep
forcing RichText via setTextFormat as before.

---

Duplicate comments:
In `@esp_flasher/improv.py`:
- Around line 194-206: The code releases the shared RPC slot (_rpc_lock)
immediately after sending a request which allows other threads calling
request_device_info(), request_wifi_networks(), or send_wifi_settings() to
overwrite shared waiter state (_rpc_command, _rpc_event, _rpc_result,
_wifi_networks) before the matching response arrives; either hold _rpc_lock
across the send-and-wait sequence (acquire before setting
_rpc_command/_rpc_event/_rpc_result, send via _write(pkt), then wait on
_rpc_event/_wifi_scan_done and only release the lock after the wait completes or
times out) or refactor to allocate per-request waiter state (create a
request-local object with its own event/result and pass an identifier in
_rpc_command so _handle_packet() can route responses to the correct waiter) and
update _handle_packet() to match responses to that per-request state instead of
the global fields.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f7fec019-aa51-4b98-84cb-9996d4c1036c

📥 Commits

Reviewing files that changed from the base of the PR and between ef306c1 and dd6d193.

📒 Files selected for processing (2)
  • esp_flasher/gui.py
  • esp_flasher/improv.py

Comment thread esp_flasher/gui.py
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
esp_flasher/gui.py (1)

764-771: Consider flushing the serial buffer before restarting the console reader.

When ImprovDialog closes, the ImprovManager receiver thread may have left unconsumed bytes in the serial buffer. The new SerialReader will read these as console output, potentially causing brief garbled text. Per the relevant code snippets, SerialReader.start() does not flush the input buffer.

♻️ Proposed fix to drain stale Improv bytes
         # Restart console reader on the same open port (like JS reconnectConsole)
         if self._serial_port and self._serial_port.is_open:
+            # Drain any leftover Improv bytes from the buffer
+            try:
+                self._serial_port.reset_input_buffer()
+            except (OSError, serial.SerialException):
+                pass
             self._serial_reader = SerialReader(self._serial_port)
             self._serial_reader.line_received.connect(self.append_log_line)
             self._serial_reader.error_occurred.connect(self.handle_serial_error)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@esp_flasher/gui.py` around lines 764 - 771, Before starting a new
SerialReader, drain any stale bytes left by ImprovManager by flushing the serial
input buffer on self._serial_port; call the pyserial method
(reset_input_buffer()) (and optionally reset_output_buffer()) on
self._serial_port inside a try/except before instantiating SerialReader so the
new SerialReader (SerialReader.start) doesn't consume leftover Improv bytes and
produce garbled console output; reference self._serial_port and SerialReader
when making this change and handle exceptions to avoid crashing the UI.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@esp_flasher/gui.py`:
- Around line 764-771: Before starting a new SerialReader, drain any stale bytes
left by ImprovManager by flushing the serial input buffer on self._serial_port;
call the pyserial method (reset_input_buffer()) (and optionally
reset_output_buffer()) on self._serial_port inside a try/except before
instantiating SerialReader so the new SerialReader (SerialReader.start) doesn't
consume leftover Improv bytes and produce garbled console output; reference
self._serial_port and SerialReader when making this change and handle exceptions
to avoid crashing the UI.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 3ced1204-9fd0-4a2e-9475-926b62d67b59

📥 Commits

Reviewing files that changed from the base of the PR and between dd6d193 and 6de5507.

📒 Files selected for processing (2)
  • esp_flasher/gui.py
  • esp_flasher/improv.py

@Jason2866 Jason2866 changed the title WiP: improv feature: Provide WiFi credentials / support for improv Apr 5, 2026
@Jason2866 Jason2866 merged commit 8e29621 into factory Apr 5, 2026
8 checks passed
@Jason2866 Jason2866 deleted the improv branch April 5, 2026 18:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant