8282
8383MAX_ZONES = 6 * len (_DEV_ADDRS )
8484
85+ _I2C_ALERT_MSG = (
86+ "Writing data to the I2C bus has failed and automatic recovery was unsuccessful. "
87+ "Please go to Settings -> Config -> Hardware Reset.\n "
88+ "If you see this message again in a short time period, "
89+ "contact AmpliPi Support at support@micro-nova.com"
90+ )
91+
8592
8693class FanCtrl (Enum ):
8794 MAX6644 = 0
@@ -250,22 +257,27 @@ def new_preamp(self, addr: int):
250257 def _recover_preamps (self ) -> bool :
251258 """ Recover a wedged/hung preamp IN-PLACE.
252259
253- The bare bus.write_byte_data retry in write_byte_data only reopens the
254- Linux SMBus handle — that recovers a transient bus glitch but NOT a hung
255- preamp microcontroller (which stops ACKing -> OSError 121 / EREMOTEIO).
256- The only thing that revives a hung preamp is pulsing its reset line,
257- which is exactly what a full reboot does. This does the same WITHOUT
258- rebooting: reset the preamp(s), re-assign I2C addresses, reopen the bus,
259- and re-flush every cached register so zone state (mute/source/vol)
260- survives the reset (self.preamps is the code's source of truth, updated
261- on every write).
262-
263- Rate-limited so a benign one-off glitch never resets audio. Returns True
264- if a recovery was performed (caller may retry the write).
260+ The bare bus.write_byte_data retry in write_byte_data only reopens the
261+ Linux SMBus handle — that recovers a transient bus glitch but NOT a hung
262+ preamp microcontroller (which stops ACKing -> OSError 121 / EREMOTEIO).
263+ The only thing that revives a hung preamp is pulsing its reset line,
264+ which is exactly what a full reboot does. This does the same WITHOUT
265+ rebooting: reset the preamp(s), re-assign I2C addresses, reopen the bus,
266+ and re-flush every cached register so zone state (mute/source/vol)
267+ survives the reset (self.preamps is the code's source of truth, updated
268+ on every write).
269+
270+ Rate-limited to once per _RECOVERY_COOLDOWN_S so a benign one-off glitch
271+ never resets audio. Returns True if a recovery was performed (caller may
272+ retry the write), False if on cooldown or if the recovery itself failed —
273+ in both cases an alert is surfaced to the user.
265274 """
266275 now = time .time ()
267276 if now - self ._last_recovery < self ._RECOVERY_COOLDOWN_S :
277+ logger .warning ('Preamp I2C write failed and recovery is still on cooldown — surfacing alert' )
278+ utils .add_alert (_I2C_ALERT_MSG )
268279 return False
280+
269281 self ._last_recovery = now
270282 logger .warning ('Preamp I2C wedged (EREMOTEIO) - attempting in-place recovery (reset + re-flush)' )
271283 try :
@@ -280,6 +292,7 @@ def _recover_preamps(self) -> bool:
280292 return True
281293 except Exception as exc :
282294 logger .error (f'Preamp in-place recovery failed: { exc } ' )
295+ utils .add_alert (_I2C_ALERT_MSG )
283296 return False
284297
285298 def write_byte_data (self , preamp_addr , reg , data ):
@@ -310,7 +323,9 @@ def write_byte_data(self, preamp_addr, reg, data):
310323 self .bus .write_byte_data (preamp_addr , reg , data )
311324 except Exception :
312325 # Fallback 2: a reopened fd can't revive a hung preamp MCU.
313- # Escalate to an in-place preamp reset + re-flush, then retry once more.
326+ # Escalate to an in-place preamp reset + re-flush, then retry once
327+ # more. If recovery is on cooldown or itself fails, an alert is
328+ # surfaced to the user inside _recover_preamps().
314329 if self ._recover_preamps ():
315330 time .sleep (0.001 )
316331 self .bus .write_byte_data (preamp_addr , reg , data )
0 commit comments