99# Configure logging
1010logging .basicConfig (level = logging .DEBUG , format = '%(asctime)s - %(levelname)s - %(message)s' )
1111
12- from PyQt5 .QtWidgets import QApplication , QWidget , QVBoxLayout , QHBoxLayout , QLabel , QSlider , QCheckBox , QPushButton
12+ from PyQt5 .QtWidgets import QApplication , QWidget , QVBoxLayout , QHBoxLayout , QLabel , QSlider , QCheckBox , QPushButton , QLineEdit , QFormLayout , QGridLayout
1313from PyQt5 .QtCore import Qt , QTimer , pyqtSignal , QObject
1414
1515OPCODE_READ_REQUEST = 0x0A
@@ -44,6 +44,7 @@ def connect(self):
4444 logging .info ("Attempting to connect to ATT socket" )
4545 self .sock = socket .socket (socket .AF_BLUETOOTH , socket .SOCK_SEQPACKET , socket .BTPROTO_L2CAP )
4646 self .sock .connect ((self .mac_address , PSM_ATT ))
47+ self .sock .settimeout (0.1 )
4748 self .running = True
4849 self .notification_thread = threading .Thread (target = self ._listen_notifications )
4950 self .notification_thread .start ()
@@ -53,9 +54,11 @@ def disconnect(self):
5354 logging .info ("Disconnecting from ATT socket" )
5455 self .running = False
5556 if self .sock :
57+ logging .info ("Closing socket" )
5658 self .sock .close ()
5759 if self .notification_thread :
58- self .notification_thread .join ()
60+ logging .info ("Stopping notification thread" )
61+ self .notification_thread .join (timeout = 1.0 )
5962 logging .info ("Disconnected from ATT socket" )
6063
6164 def register_listener (self , handle , listener ):
@@ -115,9 +118,14 @@ def _write_raw(self, pdu):
115118 logging .debug (f"Sent PDU: { pdu .hex ()} " )
116119
117120 def _read_pdu (self ):
118- data = self .sock .recv (512 )
119- logging .debug (f"Received PDU: { data .hex ()} " )
120- return data
121+ try :
122+ data = self .sock .recv (512 )
123+ logging .debug (f"Received PDU: { data .hex ()} " )
124+ return data
125+ except socket .timeout :
126+ return None
127+ except :
128+ raise
121129
122130 def _read_response (self , timeout = 2.0 ):
123131 try :
@@ -133,19 +141,26 @@ def _listen_notifications(self):
133141 while self .running :
134142 try :
135143 pdu = self ._read_pdu ()
136- if len (pdu ) > 0 and pdu [0 ] == OPCODE_HANDLE_VALUE_NTF :
137- handle = pdu [1 ] | (pdu [2 ] << 8 )
138- value = pdu [3 :]
139- logging .debug (f"Notification for handle { handle } : { value .hex ()} " )
140- if handle in self .listeners :
141- for listener in self .listeners [handle ]:
142- listener (value )
143- else :
144- self .responses .put (pdu )
145144 except :
146- logging .warning ("Error in notification listener" )
147145 break
148- logging .info ("Notification listener thread stopped" )
146+ if pdu is None :
147+ continue
148+ if len (pdu ) > 0 and pdu [0 ] == OPCODE_HANDLE_VALUE_NTF :
149+ logging .debug (f"Notification PDU received: { pdu .hex ()} " )
150+ handle = pdu [1 ] | (pdu [2 ] << 8 )
151+ value = pdu [3 :]
152+ logging .debug (f"Notification for handle { handle } : { value .hex ()} " )
153+ if handle in self .listeners :
154+ for listener in self .listeners [handle ]:
155+ listener (value )
156+ else :
157+ self .responses .put (pdu )
158+ logging .info ("Notification listener thread stopped, trying to reconnect" )
159+ if self .running :
160+ try :
161+ self .connect ()
162+ except Exception as e :
163+ logging .error (f"Reconnection failed: { e } " )
149164
150165class HearingAidSettings :
151166 def __init__ (self , left_eq , right_eq , left_amp , right_amp , left_tone , right_tone ,
@@ -178,7 +193,7 @@ def parse_hearing_aid_settings(data):
178193 logging .info (f"Parsing hearing aid settings, starting read at offset 4, value: { buffer [offset ]:02x} " )
179194
180195 left_eq = []
181- for _ in range (8 ):
196+ for i in range (8 ):
182197 val , = struct .unpack ('<f' , buffer [offset :offset + 4 ])
183198 left_eq .append (val )
184199 offset += 4
@@ -233,12 +248,16 @@ def send_hearing_aid_settings(att_manager, settings):
233248 buffer [2 ] = 0x64
234249
235250 # Left ear
251+ for i in range (8 ):
252+ struct .pack_into ('<f' , buffer , 4 + i * 4 , settings .left_eq [i ])
236253 struct .pack_into ('<f' , buffer , 36 , settings .left_amplification )
237254 struct .pack_into ('<f' , buffer , 40 , settings .left_tone )
238255 struct .pack_into ('<f' , buffer , 44 , 1.0 if settings .left_conversation_boost else 0.0 )
239256 struct .pack_into ('<f' , buffer , 48 , settings .left_ambient_noise_reduction )
240257
241258 # Right ear
259+ for i in range (8 ):
260+ struct .pack_into ('<f' , buffer , 52 + i * 4 , settings .right_eq [i ])
242261 struct .pack_into ('<f' , buffer , 84 , settings .right_amplification )
243262 struct .pack_into ('<f' , buffer , 88 , settings .right_tone )
244263 struct .pack_into ('<f' , buffer , 92 , 1.0 if settings .right_conversation_boost else 0.0 )
@@ -273,6 +292,32 @@ def init_ui(self):
273292 self .setWindowTitle ("Hearing Aid Adjustments" )
274293 layout = QVBoxLayout ()
275294
295+ # EQ Inputs
296+ eq_layout = QGridLayout ()
297+ self .left_eq_inputs = []
298+ self .right_eq_inputs = []
299+
300+ eq_labels = ["250Hz" , "500Hz" , "1kHz" , "2kHz" , "3kHz" , "4kHz" , "6kHz" , "8kHz" ]
301+ eq_layout .addWidget (QLabel ("Frequency" ), 0 , 0 )
302+ eq_layout .addWidget (QLabel ("Left" ), 0 , 1 )
303+ eq_layout .addWidget (QLabel ("Right" ), 0 , 2 )
304+
305+ for i , label in enumerate (eq_labels ):
306+ eq_layout .addWidget (QLabel (label ), i + 1 , 0 )
307+ left_input = QLineEdit ()
308+ right_input = QLineEdit ()
309+ left_input .setPlaceholderText ("Left" )
310+ right_input .setPlaceholderText ("Right" )
311+ self .left_eq_inputs .append (left_input )
312+ self .right_eq_inputs .append (right_input )
313+ eq_layout .addWidget (left_input , i + 1 , 1 )
314+ eq_layout .addWidget (right_input , i + 1 , 2 )
315+
316+ eq_group = QWidget ()
317+ eq_group .setLayout (eq_layout )
318+ layout .addWidget (QLabel ("Loss, in dBHL" ))
319+ layout .addWidget (eq_group )
320+
276321 # Amplification
277322 self .amp_slider = QSlider (Qt .Horizontal )
278323 self .amp_slider .setRange (- 100 , 100 )
@@ -306,6 +351,8 @@ def init_ui(self):
306351 layout .addWidget (self .conv_checkbox )
307352
308353 # Connect signals
354+ for input_box in self .left_eq_inputs + self .right_eq_inputs :
355+ input_box .textChanged .connect (self .on_value_changed )
309356 self .amp_slider .valueChanged .connect (self .on_value_changed )
310357 self .balance_slider .valueChanged .connect (self .on_value_changed )
311358 self .tone_slider .valueChanged .connect (self .on_value_changed )
@@ -328,7 +375,11 @@ def connect_att(self):
328375 self .emitter .update_ui .emit (settings )
329376 logging .info ("Initial settings loaded" )
330377 except Exception as e :
331- logging .error (f"Connection failed: { e } " )
378+ if e .errno == 111 :
379+ logging .error ("Connection refused. Try reconnecting your AirPods." )
380+ sys .exit (1 )
381+ else :
382+ logging .error (f"Connection failed: { e } " )
332383
333384 def on_notification (self , value ):
334385 logging .debug ("Notification received" )
@@ -344,6 +395,11 @@ def on_update_ui(self, settings):
344395 self .anr_slider .setValue (int (settings .left_ambient_noise_reduction * 100 ))
345396 self .conv_checkbox .setChecked (settings .left_conversation_boost )
346397
398+ for i , value in enumerate (settings .left_eq ):
399+ self .left_eq_inputs [i ].setText (f"{ value :.2f} " )
400+ for i , value in enumerate (settings .right_eq ):
401+ self .right_eq_inputs [i ].setText (f"{ value :.2f} " )
402+
347403 def on_value_changed (self ):
348404 logging .debug ("UI value changed, starting debounce" )
349405 self .debounce_timer .start (100 )
@@ -359,8 +415,11 @@ def send_settings(self):
359415 left_amp = amp + (0.5 - balance ) * amp * 2 if balance < 0 else amp
360416 right_amp = amp + (balance - 0.5 ) * amp * 2 if balance > 0 else amp
361417
418+ left_eq = [float (input_box .text () or 0 ) for input_box in self .left_eq_inputs ]
419+ right_eq = [float (input_box .text () or 0 ) for input_box in self .right_eq_inputs ]
420+
362421 settings = HearingAidSettings (
363- [ 0.5 ] * 8 , [ 0.5 ] * 8 , left_amp , right_amp , tone , tone ,
422+ left_eq , right_eq , left_amp , right_amp , tone , tone ,
364423 conv , conv , anr , anr , amp , balance , 0.5
365424 )
366425 threading .Thread (target = send_hearing_aid_settings , args = (self .att_manager , settings )).start ()
0 commit comments