Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 10 additions & 4 deletions Switch2Bridge.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@
# ============================================================

APP_NAME = "Switch2 Bridge"
APP_VERSION = "1.2.2" # single source of truth — read by setup_app.py & build_dmg.sh
APP_VERSION = "1.2.3" # single source of truth — read by setup_app.py & build_dmg.sh
INPUT_CHAR_UUID = "7492866c-ec3e-4619-8258-32755ffcc0f9"

# Nintendo company identifiers seen in BLE advertisements:
Expand Down Expand Up @@ -273,8 +273,10 @@ def set_dsu_enabled(self, enabled):
with open(MAPPINGS_FILE) as f:
cfg = json.load(f)
except Exception:
log.exception("could not read mappings.json to persist DSU toggle")
cfg = json.loads(json.dumps(self.DEFAULT))
# Never clobber a corrupt (but user-authored) file with defaults —
# the toggle just won't persist until the file is fixed.
log.exception("mappings.json unreadable, DSU toggle not persisted")
return
dsu = cfg.get("dsu")
if not isinstance(dsu, dict):
dsu = cfg["dsu"] = {}
Expand Down Expand Up @@ -596,6 +598,10 @@ async def _connect_async(self):
streamed = await self._session(
address, name, reconnected=was_connected
)
if not streamed and was_connected:
# Still auto-retrying — keep the failure in the logs
# only; a notification per attempt would spam the user.
self.last_error = None

if self._stop_event.is_set():
return
Expand Down Expand Up @@ -791,7 +797,7 @@ def _apply_state(self, state):
elif state == 'connected':
self.title = "🟢"
self._status_item.title = f"✓ {self.bridge.controller_name or 'Controller'}"
self._detail_item.title = f" {self.bridge.packet_count} packets"
self._detail_item.title = f" {self.bridge.packet_count} pkts"
self._action_item.title = "Disconnect"
self._action_item.set_callback(self._on_action)
elif state == 'stopping':
Expand Down
34 changes: 34 additions & 0 deletions tests/test_bridge.py
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,40 @@ class Named:
a, n = asyncio.run(br7._find_controller())
check("unrelated device ignored", a is None)

# failing reconnect attempts must not spam last_error; only the final
# give-up message surfaces
print("== reconnect error spam ==")
S2B.INITIAL_SCAN_WINDOW = 10.0
S2B.RECONNECT_WINDOW = 2.0
MockScanner.delay = 0.05
MockScanner.queue = []
MockScanner.result = {"AA:BB": (Dev(), Adv())}
br8 = S2B.ControllerBridge(mm)
br8.connect()
time.sleep(0.6)
check("spam test: connected", br8.is_connected)

orig_connect = MockClient.connect
async def _failing_connect(self):
raise RuntimeError("nope")
MockClient.connect = _failing_connect
MockClient.instances[-1]._connected = False # unexpected drop
time.sleep(1.2) # several failing reconnect attempts happen here
check("no error surfaced while retrying", br8.last_error is None, br8.last_error)
time.sleep(2.5) # past the (shrunk) reconnect window
check("final give-up error surfaced",
br8.last_error and "reconnect" in br8.last_error.lower(), br8.last_error)
check("spam test: thread ended", not br8._thread.is_alive())
MockClient.connect = orig_connect

# a corrupt mappings.json must never be clobbered by the DSU toggle
print("== dsu toggle vs corrupt file ==")
S2B.MAPPINGS_FILE.write_text("{broken json")
m4 = S2B.Mappings()
m4.set_dsu_enabled(False)
check("corrupt file untouched", S2B.MAPPINGS_FILE.read_text() == "{broken json")
check("in-memory toggle still applied", m4.dsu_enabled is False)

print()
if FAILURES:
print(f"FAILED: {len(FAILURES)} -> {FAILURES}")
Expand Down
Loading