Skip to content

Commit c173a0d

Browse files
committed
linux: implement adding hearing aid test results
1 parent 0cd19d1 commit c173a0d

1 file changed

Lines changed: 78 additions & 19 deletions

File tree

linux/hearing-aid-adjustments.py

Lines changed: 78 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
# Configure logging
1010
logging.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
1313
from PyQt5.QtCore import Qt, QTimer, pyqtSignal, QObject
1414

1515
OPCODE_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

150165
class 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

Comments
 (0)