feat: support multiple simultaneous TCP interfaces#2
Merged
Conversation
Previously only the first enabled TCP client was connected; additional TCP interfaces were silently ignored. This caused custom local servers added after onboarding to be shadowed by the community server. - AppServices: replace single `tcpInterface` with `tcpInterfaces: [String: TCPInterface]` keyed by entity ID. Add `connectTCPInterface(entityId:host:port:)` and `stopTCPInterface(entityId:)` for per-interface control. `stopTCPInterface()` (no args) stops all. `startStateObserver` aggregates state across all TCP interfaces. - ColumbaApp startup: connect each enabled TCP entity independently via `connectTCPInterface(entityId: iface.id)` so each gets its own transport ID. - InterfaceManagementViewModel: loop all enabled TCP clients in `applyChanges()`, stop removed ones. Status observer tracks each entity's state independently. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
torlando-tech
pushed a commit
that referenced
this pull request
May 26, 2026
Closes the loop on the lxst-wiring batch. With this commit a call placed
from sim1 to sim2 fully establishes through the canonical Mark Qvist
Python RNS+LXST embedded in BeeWare — the full Reticulum LXST signal
exchange (AVAILABLE → LINKIDENTIFY → RINGING → CONNECTING → ESTABLISHED
+ PREFERRED_PROFILE) crosses the bridge.
Bridge wiring completed in this commit:
- Compat Link: identifyHook (parallels sendBytesHook), wired by
AppServices to PythonRNSBackend.linkIdentify(linkId:) so the
caller's `link.identify(identity:)` call propagates through Python
and the remote sees the proper IDENTIFY signal.
- Compat Link.identifyCallbacks: was weakly held, became strong.
Telephone.handleIncomingLink constructs a
`TelephoneIdentifyHandler(telephone: self)` inline with no caller-
held reference; under weak storage it deallocated immediately and
the inbound call silently stalled at AVAILABLE, never receiving
the identify-callback invocation when Python's link_identified
event arrived. Strong storage fixes it.
- app/rns_bridge.py: announce_telephony() function so the LXST
telephony destination announces alongside the LXMF delivery one;
sendAllAnnounces() invokes both. Without this, sim2 couldn't see
sim1's voice destination in its path table.
- URL test triggers: lxma://test-call?to=HEX[&profile=...] places
an outbound call through CallManager.initiateCall; lxma://test-answer
auto-accepts the ringing call. Mirrors the existing lxma://test-* set.
- CallManager auto-answer escape hatch keyed off env var
COLUMBA_AUTO_ANSWER=1. Reason: simctl openurl only delivers URLs
to the frontmost simulator window; sim2 stays backgrounded during
the sim1 → sim2 test so we can't drive the answer via URL. The
env var lets the smoke test reliably push sim2 past RINGING.
Smoke test (iPhone 17 Pro / 17 Pro Max sims, fresh boots):
sim1 → sim2: [TEST-CALL] to=e6abaf41... profile=qualityMedium
sim1: [TEL_BRIDGE] opened outbound link 1 → e6abaf41
sim2: [PY] link 1 state=established inbound=true
sim2: [CALL] handleIncomingLink
sim2: [TEL] sendData #1: 4B (AVAILABLE)
sim1: [TEL] handlePacket first4=81009103 (AVAILABLE)
sim1: (link.identify → linkIdentifyHook → Python linkIdentify)
sim2: [PY] link 1 identified=cb54d9a4
sim2: [CALL] Ringing from: cb54d9a4...
sim2: [TEL] sendData #2: 4B (RINGING)
sim2: [TEL] PREFERRED_PROFILE: Medium Quality
sim1: [CALL] Ringing from: 4e4f5bcb...
sim2: [CALL] COLUMBA_AUTO_ANSWER=1 — auto-answering
sim2: [TEL] answer(): sending CONNECTING, profile=Medium Quality
sim2: [TEL] sendData #3: 4B (CONNECTING)
sim2: [TEL] answer(): pipeline started, codec=opus
sim2: [TEL] answer(): sent ESTABLISHED
sim2: [CALL] establishedCallback fired
sim2: [AUDIO] startAudio() creating AudioManager
sim1: [TEL] handlePacket first4=81009105 (CONNECTING)
sim1: [TEL] handlePacket first4=81009106 (ESTABLISHED)
sim1: [AUDIO] startAudio() creating AudioManager
Audio frame round-trip (what we'd ideally see next, e.g. opus
"frame #1: hdr=0x01 audioBytes=N") doesn't fire on the simulator:
both sides reach [AUDIO] startAudio() but then log "deferring engine
start until didActivateAudioSession" because CallKit on the simulator
doesn't drive its audio-session-activation delegate the way a physical
device does (the sim has no real audio hardware to bind to). The
signaling path is end-to-end across the Python bridge; verifying
actual audio bytes traversing requires installing on Torlando's USB-
attached iPhone, which is a separate workstream from this 5-commit
batch.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
AppServices.tcpInterfacesis now a[String: TCPInterface]dict keyed by entity ID, supporting N concurrent TCP connections.Changes
AppServicestcpInterface: TCPInterface?(stored) →tcpInterfaces: [String: TCPInterface];tcpInterfacekept as computedvalues.firstfor backward compatconnectTCPInterface(entityId:host:port:)— connects or replaces a specific TCP interface by entity IDstopTCPInterface(entityId:)— stops one; existingstopTCPInterface()now stops allstartStateObserveraggregates.connected/.reconnectingacross all TCP interfacesColumbaApp._initializeServicesOnce""toinitialize()(skips built-in TCP setup)connectTCPInterface(entityId: iface.id, ...)InterfaceManagementViewModel.applyChangesfirstactiveInterfaceId)Test plan
10.x.x.xserver) — both should show Connected🤖 Generated with Claude Code