-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathwave_master_gui.py
More file actions
500 lines (444 loc) · 21.3 KB
/
wave_master_gui.py
File metadata and controls
500 lines (444 loc) · 21.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
import tkinter as tk
from tkinter import ttk, messagebox
import threading
import time
from rwave_api import RemoteWave
class RemoteWaveMaster:
def __init__(self, root):
self.root = root
self.root.title("Remote Wave Master for neuroConn TDCS v1.2")
self.mywave = RemoteWave()
# Scan devices
self.devices = self.mywave.scan("")
self.device_entries = []
for dev in self.devices:
if isinstance(dev, dict):
display = dev.get('product_string', str(dev))
else:
display = str(dev)
self.device_entries.append(display)
self.attached = False
# Flags to avoid setter calls during initialization
self._init_in_progress = True
# Variables (use StringVar so we can validate free text entries)
self.device_var = tk.StringVar()
self.dc_curr_var = tk.StringVar(value="0.0")
self.theta_freq_var = tk.StringVar(value="6.0")
self.theta_ampl_var = tk.StringVar(value="2.0")
self.gamma_freq_var = tk.StringVar(value="80.0")
self.gamma_ampl_var = tk.StringVar(value="0")
self.gamma_mod_depth_var = tk.StringVar(value="100.0")
self.gamma_start_var = tk.StringVar(value="0.0")
self.gamma_stop_var = tk.StringVar(value="180.0")
self.ramping_interval_var = tk.StringVar(value="3")
self.comp_var = tk.IntVar(value=0) # 0 additive, 1 modulation
self.invert_var = tk.IntVar(value=0) # 0 normal, 1 inverted
self.ramp_var = tk.IntVar(value=0) # 0 linear, 1 exponential
# Variables for phases
self.theta_phase_var = tk.StringVar(value="0.0")
self.gamma_phase_var = tk.StringVar(value="0.0")
self.trigger_phase_var = tk.StringVar(value="0.0")
self.status_var = tk.StringVar(value="Device status: -")
self._build_widgets()
# traces for checkboxes
self.comp_var.trace_add("write", lambda *args: self._on_comp_change())
self.invert_var.trace_add("write", lambda *args: self._on_invert_change())
self.ramp_var.trace_add("write", lambda *args: self._on_ramp_change())
self._init_in_progress = False
# initialize enabled/disabled fields
self._on_comp_change()
def _build_widgets(self):
notebook = ttk.Notebook(self.root)
notebook.grid(row=0, column=0, sticky='NSEW')
# === Global control bar (always visible) ===
bottom = ttk.Frame(self.root, padding=10)
bottom.grid(row=1, column=0, sticky='EW')
# Make both button columns equal width
bottom.grid_columnconfigure(0, weight=1, uniform="buttons")
bottom.grid_columnconfigure(1, weight=1, uniform="buttons")
bottom.grid_columnconfigure(2, weight=3) # status label expands
start_btn = ttk.Button(bottom, text="Wave update/start", command=self._on_start)
start_btn.grid(row=0, column=0, padx=5, ipadx=10, ipady=10, sticky="EW")
stop_btn = ttk.Button(bottom, text="Wave stop", command=self._on_stop)
stop_btn.grid(row=0, column=1, padx=5, ipadx=10, ipady=10, sticky="EW")
ttk.Label(bottom, textvariable=self.status_var).grid(
row=0, column=2, sticky='W', padx=10
)
# --- Tab 1 (Control panel) ---
frm = ttk.Frame(notebook, padding=10)
notebook.add(frm, text="Control panel")
# --- Tab 2 (Phase settings) ---
phase_frame = ttk.Frame(notebook, padding=10)
notebook.add(phase_frame, text="Phase settings")
self.root.columnconfigure(0, weight=1)
style = ttk.Style()
style.configure("TLabelframe", borderwidth=2, relief="groove")
style.configure("TLabelframe.Label", font=("TkDefaultFont", 10, "bold"))
# -----------------------------
# Control settings tab (TAB 1)
# -----------------------------
# --- Device selection ---
ttk.Label(frm, text="Select Device:").grid(row=0, column=0, sticky='W')
self.device_cb = ttk.Combobox(frm, textvariable=self.device_var,
values=self.device_entries, state="readonly",
width=50)
self.device_cb.grid(row=0, column=1, sticky='W', columnspan=3)
ttk.Button(frm, text="Connect", command=self.connect).grid(row=0, column=4, padx=5)
ttk.Button(frm, text="Disconnect", command=self.disconnect).grid(row=0, column=5, padx=5)
row_tab1 = 1
# Helper to create rows inside any parent frame
def make_row_tab1(parent, label_text, var, vmin, vmax, setter_callable, label):
r = make_row_tab1.counter
ttk.Label(parent, text=label_text).grid(row=r, column=0, sticky='W', pady=3, padx=5)
ent = ttk.Entry(parent, textvariable=var, width=12)
ent.grid(row=r, column=1, sticky='W', pady=3)
def apply_change(event=None):
self._on_float_change(var, vmin, vmax, setter_callable, label)
ent.bind("<Return>", apply_change)
ent.bind("<FocusOut>", apply_change)
make_row_tab1.counter += 1
return ent
make_row_tab1.counter = 0
# --- Grouped parameter frames ---
# ---- DC group ----
dc_frame = ttk.LabelFrame(frm, text="DC Current Setting", padding=10)
dc_frame.grid(row=row_tab1, column=0, columnspan=6, sticky='EW', pady=(10,5))
make_row_tab1(dc_frame, "DC output current (mA) [-4.0..+4.0]:",
self.dc_curr_var, -4.0, 4.0, self.mywave.write_dc_current, "DC output current")
row_tab1 += 1
# ---- Theta group ----
make_row_tab1.counter = 0
theta_frame = ttk.LabelFrame(frm, text="Theta Wave Settings", padding=10)
theta_frame.grid(row=row_tab1, column=0, columnspan=6, sticky='EW', pady=(10,5))
make_row_tab1(theta_frame, "Theta wave frequency (Hz) [1.0..20.0]:",
self.theta_freq_var, 1.0, 20.0, self.mywave.write_freq_theta, "Theta wave frequency")
make_row_tab1(theta_frame, "Theta wave amplitude (mA) [0.0..4.0]:",
self.theta_ampl_var, 0.0, 4.0, self.mywave.write_ampl_theta, "Theta wave amplitude")
row_tab1 += 1
# ---- Gamma group ----
make_row_tab1.counter = 0
gamma_frame = ttk.LabelFrame(frm, text="Gamma Wave Settings", padding=10)
gamma_frame.grid(row=row_tab1, column=0, columnspan=6, sticky='EW', pady=(10,5))
make_row_tab1(gamma_frame, "Gamma wave frequency (Hz) [40.0..200.0]:",
self.gamma_freq_var, 40.0, 200.0, self.mywave.write_freq_gamma1, "Gamma wave frequency")
self.ent_gamma_ampl = make_row_tab1(gamma_frame,
"Gamma wave amplitude (mA) [0.0..4.0]:",
self.gamma_ampl_var, 0.0, 4.0,
self.mywave.write_ampl_gamma1,
"Gamma wave amplitude"
)
self.ent_gamma_mdepth = make_row_tab1(gamma_frame,
"Gamma wave modulation depth (%) [0.0..100.0]:",
self.gamma_mod_depth_var, 0.0, 100.0,
self.mywave.write_mdepth_gamma1,
"Gamma wave modulation depth"
)
row_tab1 += 1
# ---- Ramping group ----
make_row_tab1.counter = 0
ramp_frame = ttk.LabelFrame(frm, text="Ramping Setting", padding=10)
ramp_frame.grid(row=row_tab1, column=0, columnspan=6, sticky='EW', pady=(10,5))
make_row_tab1(ramp_frame, "Ramping interval (s) [0..240]:",
self.ramping_interval_var, 0.0, 240.0, self.mywave.write_ramping_interval, "Ramping interval")
row_tab1 += 1
# Checkboxes
ttk.Checkbutton(frm, text="Wave composition: superposition (off) / modulation (on)",
variable=self.comp_var).grid(row=row_tab1, column=0, columnspan=3, sticky='W', pady=6)
row_tab1 += 1
ttk.Checkbutton(frm, text="Output polarity: TGP (off) / TGT (on)",
variable=self.invert_var).grid(row=row_tab1, column=0, columnspan=3, sticky='W', pady=6)
row_tab1 += 1
ttk.Checkbutton(frm, text="Ramping profile: linear (off) / exponential (on)",
variable=self.ramp_var).grid(row=row_tab1, column=0, columnspan=3, sticky='W', pady=6)
row_tab1 += 1
# -----------------------------
# Phase settings tab (TAB 2)
# -----------------------------
# Helper for rows inside groups
def make_phase_row(parent, label_text, var, vmin, vmax, setter_callable, label, row):
ttk.Label(parent, text=label_text).grid(row=row, column=0, sticky='W', pady=4)
ent = ttk.Entry(parent, textvariable=var, width=12)
ent.grid(row=row, column=1, sticky='W')
def apply_change(event=None):
self._on_float_change(var, vmin, vmax, setter_callable, label)
ent.bind("<Return>", apply_change)
ent.bind("<FocusOut>", apply_change)
return ent
# ---- Theta group ----
theta_group = ttk.LabelFrame(phase_frame, text="Theta Phase Setting", padding=10)
theta_group.grid(row=0, column=0, sticky="EW", pady=(5,10))
make_phase_row(
theta_group,
"Theta wave phase (°) [0..360]:",
self.theta_phase_var, 0.0, 360.0,
self.mywave.write_phase_theta,
"Theta wave phase",
row=0
)
# ---- Gamma group ----
gamma_group = ttk.LabelFrame(phase_frame, text="Gamma Phase Settings", padding=10)
gamma_group.grid(row=1, column=0, sticky="EW", pady=(5,10))
make_phase_row(gamma_group, "Gamma wave phase (°) [0..360]:",
self.gamma_phase_var, 0.0, 360.0,
self.mywave.write_phase_gamma1,
"Gamma wave phase", row=0)
make_phase_row(gamma_group, "Gamma wave starting angle (°) [0.0..360]:",
self.gamma_start_var, 0.0, 360.0,
self.mywave.write_start_phase_gamma1,
"Gamma start", row=2)
make_phase_row(gamma_group, "Gamma wave stopping angle (°) [0.0..360]:",
self.gamma_stop_var, 0.0, 360.0,
self.mywave.write_stop_phase_gamma1,
"Gamma stop", row=3)
# ---- Trigger group ----
trigger_group = ttk.LabelFrame(phase_frame, text="Trigger Phase Setting", padding=10)
trigger_group.grid(row=2, column=0, sticky="EW", pady=(5,10))
make_phase_row(
trigger_group,
"Trigger phase (°) [0..360]:",
self.trigger_phase_var, 0.0, 360.0,
self.mywave.write_trigger_phase,
"Trigger phase",
row=0
)
# -------------------------
# Device connect/disconnect
# -------------------------
def connect(self):
"""
Attach Device
"""
if not self.attached and self.device_var.get():
try:
ok = self.mywave.attach(self.device_var.get())
if ok:
self.attached = True
self.device_cb.state(['disabled'])
self.status_var.set("Device status: attached")
# After attach, re-send current UI values to device so GUI and device are synced
self._push_all_settings()
else:
messagebox.showerror("Error", "Connect failed (attach returned False).")
except Exception as e:
messagebox.showerror("Error", f"Connect failed: {e}")
def disconnect(self):
"""
Disconnect Device
"""
if self.attached:
try:
self.mywave.close()
except Exception as e:
# close() returns False if no device, but try/except for IO issues
print("close() exception:", e)
self.attached = False
self.device_cb.state(['!disabled'])
self.status_var.set("Device status: detached")
# -------------------------
# Live-updating handlers
# -------------------------
def _on_float_change(self, var: tk.StringVar, vmin: float, vmax: float, setter_callable, label):
"""
Called every time the associated StringVar changes. Parse float, clamp to bounds,
call the setter_callable(current_float) if device attached. If parsing fails, ignore.
"""
if self._init_in_progress:
return
txt = var.get().strip()
if txt == "":
return
try:
val = float(txt)
except ValueError:
# invalid input - ignore (user typing) -- do not call setter
return
# clamp to allowed range
if val < vmin:
val = vmin
var.set(f"{val:.3f}")
elif val > vmax:
val = vmax
var.set(f"{val:.3f}")
# attempt to call setter immediately (live updating)
if not self.attached:
# not attached -> update status but don't raise
self.status_var.set(f"Device status: not attached (change: {label}={val})")
return
try:
result = setter_callable(val)
# Many rwave setters return None on success, or False when requires_device fails.
# We only update status text if no exception.
self.status_var.set(f"Device status: set {label} = {val}")
except Exception as e:
# Show the error but don't crash
self.status_var.set(f"Device status: failed to set {label}")
messagebox.showerror("Error", f"Failed to set {label}: {e}")
def _on_comp_change(self):
if self._init_in_progress:
return
val = int(self.comp_var.get()) # 0 additive, 1 modulation
if val == 1:
# modulation: amplitude disabled, depth enabled
self.ent_gamma_ampl.configure(state="disabled")
self.ent_gamma_mdepth.configure(state="normal")
else:
# additive: amplitude enabled, depth disabled
self.ent_gamma_ampl.configure(state="normal")
self.ent_gamma_mdepth.configure(state="disabled")
# existing device-setting logic
if not self.attached:
self.status_var.set(f"Device status: not attached (composition={val})")
return
try:
self.mywave.set_composition(val)
self.status_var.set(f"Device status: set composition = {'modulation' if val else 'additive'}")
except Exception as e:
self.status_var.set("Device status: failed to set composition")
messagebox.showerror("Error", f"Failed to set composition: {e}")
def _on_invert_change(self):
if self._init_in_progress:
return
val = int(self.invert_var.get()) # 0 normal, 1 inverted
if not self.attached:
self.status_var.set(f"Device status: not attached (invert={val})")
return
try:
self.mywave.set_output_mode(val)
self.status_var.set(f"Device status: set output invert = {val}")
except Exception as e:
self.status_var.set("Device status: failed to set invert")
messagebox.showerror("Error", f"Failed to set output invert: {e}")
def _on_ramp_change(self):
if self._init_in_progress:
return
val = int(self.ramp_var.get()) # 0 linear, 1 exponential
if not self.attached:
self.status_var.set(f"Device status: not attached (invert={val})")
return
try:
self.mywave.set_ramping_profile(val)
self.status_var.set(f"Device status: set ramping profile")
except Exception as e:
self.status_var.set("Device status: failed to set the ramping profile")
messagebox.showerror("Error", f"Failed to set the ramping profile: {e}")
# -------------------------
# Start / Stop wave handling
# -------------------------
def _on_start(self):
if not self.attached:
messagebox.showwarning("Not attached", "No device attached.")
return
def worker():
try:
# send start command
self.mywave.start()
except Exception as e:
# if start raised (e.g., not attached), show error
self.root.after(0, lambda: messagebox.showerror("Start failed", f"start() failed: {e}"))
self.root.after(0, lambda: self.status_var.set("Device status: failed to start"))
return
# wait for ack (timeout ms) - example: wait up to 2000 ms
try:
resp, elapsed = self.mywave.wait_for_ack(2000)
# wait_for_ack returns (-1, elapsed) on timeout OR (last_event, elapsed)
if resp == -1:
self.root.after(0, lambda: self.status_var.set("Device status: failed (no ack)"))
else:
self.root.after(0, lambda: self.status_var.set("Device status: OK (ack received)"))
except Exception as e:
# wait_for_ack might return tuple differently or raise
# handle both styles: if it returned something else above, we covered that.
self.root.after(0, lambda: self.status_var.set("Device status: failed (wait error)"))
self.root.after(0, lambda: messagebox.showerror("Ack error", f"wait_for_ack failed: {e}"))
threading.Thread(target=worker, daemon=True).start()
def _on_stop(self):
if not self.attached:
messagebox.showwarning("Not attached", "No device attached.")
return
try:
self.mywave.stop()
self.status_var.set("Device status: stopped (stop command sent)")
except Exception as e:
messagebox.showerror("Stop failed", f"stop() failed: {e}")
self.status_var.set("Device status: failed to stop")
# -------------------------
# Helper to push UI to device on connect
# -------------------------
def _push_all_settings(self):
"""Push all current control values to device after connect so UI + device are synced."""
# Call setters but ignore exceptions (they'll be shown via messageboxes inside setters)
try:
# DC
try:
self.mywave.write_dc_current(float(self.dc_curr_var.get()))
except Exception:
pass
# theta wave
try:
self.mywave.write_freq_theta(float(self.theta_freq_var.get()))
except Exception:
pass
try:
self.mywave.write_ampl_theta(float(self.theta_ampl_var.get()))
except Exception:
pass
try:
self.mywave.write_phase_theta(float(self.theta_phase_var.get()))
except Exception:
pass
# gamma wave
try:
self.mywave.write_freq_gamma1(float(self.gamma_freq_var.get()))
except Exception:
pass
try:
self.mywave.write_ampl_gamma1(float(self.gamma_ampl_var.get()))
except Exception:
pass
try:
self.mywave.write_phase_gamma1(float(self.gamma_phase_var.get()))
except Exception:
pass
try:
self.mywave.write_mdepth_gamma1(float(self.gamma_mod_depth_var.get()))
except Exception:
pass
# start/stop phases
try:
self.mywave.write_start_phase_gamma1(float(self.gamma_start_var.get()))
except Exception:
pass
try:
self.mywave.write_stop_phase_gamma1(float(self.gamma_stop_var.get()))
except Exception:
pass
# trigger phase
try:
self.mywave.write_trigger_phase(float(self.trigger_phase_var.get()))
except Exception:
pass
# Ramping interval
try:
self.mywave.write_ramping_interval(float(self.ramping_interval_var.get()))
except Exception:
pass
# composition & invert
try:
self.mywave.set_composition(int(self.comp_var.get()))
except Exception:
pass
try:
self.mywave.set_output_mode(int(self.invert_var.get()))
except Exception:
pass
try:
self.mywave.set_ramping_profile(int(self.ramp_var.get()))
except Exception:
pass
self.status_var.set("Device status: synced")
except Exception as e:
self.status_var.set(f"Device status: sync failed ({e})")
if __name__ == "__main__":
root = tk.Tk()
app = RemoteWaveMaster(root)
root.mainloop()