Skip to content

Replace SpectraViewer save dialog with an in-figure filename TextBox - #84

Merged
dccote merged 1 commit into
masterfrom
spectrometer-save-no-lock
May 19, 2026
Merged

dccote merged 1 commit into
masterfrom
spectrometer-save-no-lock

Conversation

@dccote

@dccote dccote commented May 18, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Replaces `SpectraViewer.clickSave`'s native file-dialog with an in-figure `TextBox` widget. The user types the path inline; Save reads it directly. No platform file dialog is involved.

This fixes all three of the compounding bugs that surfaced while debugging the original "Save locks up very often" report:

  1. Non-reentrant lock deadlock. `threading.Lock` was held by both `animate()` and `clickSave()`. Whenever matplotlib's animation timer fired during the save dialog, the timer callback ran on the same main thread holding the lock and deadlocked.

  2. matplotlib 3.10 broke the macOS C dialog. `backend_macosx._macosx.choose_save_file` changed signature from 2 args to 3 between matplotlib versions. The old code's bare `except:` had hidden the resulting `TypeError` so the user saw "click Save, nothing happens".

  3. Tk + macOS NSRunLoop = SIGSEGV. Falling back to `tkinter.filedialog.asksaveasfilename` segfaulted (exit 139). macOS only allows one NSRunLoop owner per main thread; matplotlib's `macosx` backend already owns it, and Tk's modal dialog tries to start a competing loop.

All three are run-loop-mixing failures. The fix sidesteps them by not invoking any second GUI toolkit during save.

Changes (`hardwarelibrary/spectrometers/viewer.py`)

  • Add `self.filenameBox = TextBox(axFile, "File ", initial="spectrum.csv")` to the control row.
  • `clickSave` now reads `self.filenameBox.text` directly and writes the CSV via `spectrometer.saveSpectrum(...)`. `try/finally` ensures the animation resumes even if the write raises.
  • Drop `threading.Lock`, `matplotlib.backends as backends` import, and the `askForSavePath` helper — none are needed once the dialog is gone.
  • Re-layout the control bar to fit the new TextBox: shrink button heights from 0.075 → 0.05, shorten the integration-time label to `"ms "`, widen the filename TextBox to ~0.30, push Time/Save/Quit to the right edge.

Net: +54 / −59 lines in one file.

Test plan

  • Module imports cleanly (no stale references after the lock / backends / askForSavePath removal).
  • Live viewer test with `DebugSpectro`: click Save with default filename → CSV is written. Click Save repeatedly → no lock-up, no segfault.
  • Real-hardware test (Ocean Optics USB2000+): viewer runs, save works.
  • Verify on Windows / Linux that the TextBox layout is reasonable (only tested on macOS).

Why a TextBox instead of switching backends

The other paths considered:

  • Switch matplotlib backend to TkAgg. Tk and matplotlib would share one event loop. Works, but affects every figure the library produces, not just the spectrum viewer.
  • Auto-save with timestamp filename. Reliable but takes filename-choice away from the user.
  • Filename TextBox (this PR). Stays inside the existing `macosx` backend, no extra toolkit, gives the user inline editing of the path. Lowest blast radius.

🤖 Generated with Claude Code

Three problems converged in clickSave before this commit:

1. Non-reentrant threading.Lock held by both animate() and
   clickSave() deadlocked whenever matplotlib's FuncAnimation timer
   fired while the macOS save dialog was open.

2. matplotlib 3.10 changed the signature of the private C function
   backend_macosx._macosx.choose_save_file from 2 args to 3.
   clickSave's previous fallback (bare except: -> Tk) hid this as a
   TypeError that matplotlib silently swallowed in its event
   handler. Result: "click Save, nothing happens".

3. Replacing the macOS path with Tk's filedialog.asksaveasfilename
   triggered SIGSEGV. macOS's main thread can only host one
   NSRunLoop, and Tk's modal dialog tries to start a second one
   alongside matplotlib's own.

All three are mixing-GUI-run-loops problems. Rather than keep
fighting it, the save path now uses matplotlib's own TextBox widget
(already used elsewhere in this viewer for integration time) to
get the filename from the user.

Changes in hardwarelibrary/spectrometers/viewer.py:

- Add a `filenameBox = TextBox(..., initial='spectrum.csv')` to the
  control row; clickSave reads its `.text` directly.
- Remove the threading.Lock + matplotlib.backends import + the
  askForSavePath helper. clickSave is now a straightforward
  stop-animation / write-CSV / restart-animation block.
- Stop animation around saveSpectrum() with try/finally so the
  animation resumes even if writing the file raises (matches the
  intent of the original docstring, which the old code didn't
  actually honor).
- Re-layout the control bar: shrink button heights, shorten the
  integration-time label to "ms ", widen the filename TextBox to
  ~0.30 width, and squeeze ms/Save/Quit to the right edge.

Verified by writing a CSV through the live viewer using a
DebugSpectro device (no run-loop conflict, no segfault, file
written to disk).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@dccote
dccote force-pushed the spectrometer-save-no-lock branch from 97304ed to 8672c73 Compare May 19, 2026 00:08
@dccote dccote changed the title Fix save-dialog deadlock in SpectraViewer Replace SpectraViewer save dialog with an in-figure filename TextBox May 19, 2026
@dccote
dccote merged commit 5c5155b into master May 19, 2026
@dccote
dccote deleted the spectrometer-save-no-lock branch May 19, 2026 00:09
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