Skip to content

Commit 802fa30

Browse files
Add alert surfacing workflow to @stamateviorel 's I2C self-healing fix
1 parent 4760ec8 commit 802fa30

2 files changed

Lines changed: 31 additions & 16 deletions

File tree

amplipi/rt.py

Lines changed: 28 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,13 @@
8282

8383
MAX_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

8693
class 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)

web/src/App.jsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -140,8 +140,8 @@ export const useStatusStore = create((set, get) => ({
140140
}
141141
});
142142
},
143-
setAlert: (text, onClose) => {
144-
set({alert: {"open": true, "text": text, "onClose": onClose}});
143+
setAlert: (text, onClose, severity = "error") => {
144+
set({alert: {"open": true, "text": text, "severity": severity, "onClose": onClose}});
145145
},
146146

147147
getSystemState: () => {
@@ -155,7 +155,7 @@ export const useStatusStore = create((set, get) => ({
155155
} else {
156156
set({ status: s, loaded: true, disconnected: false });
157157
if(s.info.version != import.meta.env.VITE_BACKEND_VERSION){
158-
set({alert: {"open": true, "text": "Your webapp is out of date, closing this message will refresh the page. If this message persists post-refresh, clear your browser cache and try again.", "onClose": () => {window.location.reload();}}});
158+
set({alert: {"open": true, "text": "Your webapp is out of date, closing this message will refresh the page. If this message persists post-refresh, clear your browser cache and try again.", "onClose": () => {window.location.reload();}, "severity": "warning"}});
159159
}
160160

161161
for(let i = 0; i < s.info.global_alerts.length; i++){

0 commit comments

Comments
 (0)