mirror of
https://github.com/meshtastic/Meshtastic-Apple.git
synced 2026-04-20 22:13:56 +00:00
* Initial implementation of transports * Initial LogRadio implementation * Fixes for Settings view (caused by debug commenting) * Refinement of the object and actor model * Connect view text and tab updates * Fix mac catalyst and tests * Warning and logging clean-up * In progress commit * Serial Transport and Reconnect draft work * Serial transport and reconnection draft work * Quick fix for BLE - still more work to do * interim commit * More in progress changes * Minor improvements * Pretty good initial implementation * Bump version beyond the app store * Fix for disconnection swipeAction * Tweaks to TCPConnection implementation * Retry for NONCE_ONLY_DB * Revert json string change * Simplified some of the API + "Anti-discovery" * Tweaks for devices leaving the discovery process * Bump version * iOS26 Tweaks * Tweaks and bug fixes * Add link with slash sf symbol * update symbol image on connect view * BLE disconnect handling * Log privacy attributes * Onboarding and minor fixes. * change database to nodes, add emoji to tcp logs * Error handling improvements * More logging emojis * Suppressed unnecessary errors on disconnect * Heartbeat emoji * Add bluetooth symbol * add privacy attributes to [TCP] logs, add custom bluetooth logo * Improve routing logs * Emoji for connect logs * Heartbeat emoji * Add CBCentralManagerScanOptionAllowDuplicatesKey options to central for bluetooth * fix nav errors by switching from observableobject to state * Update connection indicator icon * fix for BLE disconnects * Connection process fixes * More fixes/tweaks to connection process * Strict concurrency * Fix some warnings, remove wifi warning * delete stale keys * interim commit * Update privacy for log, fix wrong space * fix a couple of linting items * Switch to targeted * interim commit * BLE Signal strenth on connect view * Remove BLE RSSI from long press menu * Modem lights * minor spacing tweak * Additional BLE logging and a scanning fix. * Discovery and BLE RSSI improvements * Background suspension * Update isConnected to enable UI during db load * update protobufs * Replace config if statements with switches, Fix unknown module config logging, make dark mode modem circle stroke color white so they are visible * Additional logging cleanup * hast * Set unmessagable to true if the longname has the unmessagable emoji * Connect error handling improvements * Admin popup list icon and activity lights updates * Revert use of .toolbar back to .navigationBarItems * More public logging * Better BLE error handling * Node DB progress meter * minor tweak to activity light interaction timing * Fix comment linting, remove stale keys * Remove stale keys * Easy linting fixes * Two more simple linting fixes * clean up meshtasticapp * More public logging * Replay config * Logging * Fix for unselected node on Settings * Tweak to progress meter based on device idiom * Update protos * Session replay redaction of messages * Serial fix for old devices, and a let statement * Mask text too * Fix typo * BLE poweredOff is now an auto-reconnectable error * Update logging * Fix for peerRemovedPairingInformation * Logging for BLE peripheral:didUpdateValueFor errors. * Fix for inconsistent swipe disconnect behavior * periperal:didUpdateValueFor error handling * Fix for BLEConnection continuation guarding * BLEConnection actor deadlock on disconnect * Heartbeat nonce * Fix for swipe disconnect and task cancellation * Fix for swipe actions not honoring .disabled() * Tell BLETransport when BLEConnection is cancelled * Update navigation logging * Logging updates * Bump version to 2.7.0 * Organize into folders and heartbeat stuff * Minor improvements to manual TCP connection * Auto-connect toggle * Possible BLE bug, still waiting to see in logs * Concurrency tweaks * Concurrency improvements * requestDeviceMetadata fix. fixes remote admin * Minor typo fixes * "All" button for log filters: category and level * More robust continuation handling for BLE * @FetchRequest based ChannelMessageList * Update info.plist and device hardware file * Move auto connect toggle to app settings and debug mode, tint properly with the accent color * Add label to auto connect toggle * Update log for node info received from ourselves over the mesh * Remove unused scrollViewProxy * Update Meshtastic/Views/Onboarding/DeviceOnboarding.swift Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update target for connect view * Properly Set datadog environment * Comment out ble manager * Adjust cyclomatic complexity thresholds in .swiftlint.yml * Linting fixes, delete ble manager * Make session replay debug only --------- Co-authored-by: jake-b <jake-b@users.noreply.github.com> Co-authored-by: jake <jake@jakes-Mac-mini.local> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
227 lines
6.3 KiB
Swift
227 lines
6.3 KiB
Swift
//
|
|
// TCPConnection.swift
|
|
// Meshtastic
|
|
//
|
|
// Created by Jake Bordens on 7/19/25.
|
|
//
|
|
|
|
import Foundation
|
|
import Network
|
|
import OSLog
|
|
import MeshtasticProtobufs
|
|
|
|
actor TCPConnection: Connection {
|
|
let type = TransportType.tcp
|
|
|
|
private var connection: NWConnection?
|
|
private let queue = DispatchQueue(label: "tcp.connection")
|
|
private var readerTask: Task<Void, Never>?
|
|
private let nwHost: NWEndpoint.Host
|
|
private let nwPort: NWEndpoint.Port
|
|
|
|
private var connectionStreamContinuation: AsyncStream<ConnectionEvent>.Continuation?
|
|
|
|
var isConnected: Bool {
|
|
connection?.state == .ready
|
|
}
|
|
|
|
init(host: String, port: Int) async throws {
|
|
self.nwHost = NWEndpoint.Host(host)
|
|
self.nwPort = NWEndpoint.Port(integerLiteral: UInt16(port))
|
|
}
|
|
|
|
private func waitForMagicBytes() async throws -> Bool {
|
|
let startOfFrame: [UInt8] = [0x94, 0xc3]
|
|
var waitingOnByte = 0
|
|
while true {
|
|
let data = try await receiveData(min: 1, max: 1)
|
|
if data.count != 1 {
|
|
// End of stream
|
|
return false
|
|
}
|
|
|
|
if data[0] == startOfFrame[waitingOnByte] {
|
|
waitingOnByte += 1
|
|
} else {
|
|
waitingOnByte = 0
|
|
}
|
|
|
|
if waitingOnByte > 1 {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
|
|
private func readInteger() async throws -> UInt16? {
|
|
let data = try await receiveData(min: 2, max: 2)
|
|
if data.count == 2 {
|
|
let value = data.withUnsafeBytes { $0.load(as: UInt16.self).bigEndian }
|
|
return value
|
|
}
|
|
return nil
|
|
}
|
|
|
|
private func startReader() {
|
|
// TODO: @MainActor here because packets come into AccessoryManager out of order otherwise. Need to figure out the concurrency
|
|
readerTask = Task { @MainActor in
|
|
while await isConnected {
|
|
do {
|
|
if try await waitForMagicBytes() == false {
|
|
Logger.transport.debug("🌐 [TCP] startReader: EOF while waiting for magic bytes")
|
|
continue
|
|
}
|
|
// Logger.transport.debug("[TCP] startReader: Found magic byte, waiting for length")
|
|
|
|
if let length = try? await readInteger() {
|
|
let payload = try await receiveData(min: Int(length), max: Int(length))
|
|
if let fromRadio = try? FromRadio(serializedBytes: payload) {
|
|
await connectionStreamContinuation?.yield(.data(fromRadio))
|
|
} else {
|
|
try await self.disconnect(withError: AccessoryError.disconnected("Network connection dropped"), shouldReconnect: true)
|
|
}
|
|
} else {
|
|
Logger.transport.debug("🌐 [TCP] startReader: EOF while waiting for length")
|
|
}
|
|
} catch {
|
|
Logger.transport.error("🌐 [TCP] startReader: Error reading from TCP: \(error, privacy: .public)")
|
|
try? await self.disconnect(withError: error, shouldReconnect: true)
|
|
break
|
|
}
|
|
}
|
|
// Logger.services.error("End of TCP reading task: isConnected:\(self.isConnected)")
|
|
}
|
|
}
|
|
|
|
private func receiveData(min: Int, max: Int) async throws -> Data {
|
|
try await withCheckedThrowingContinuation { cont in
|
|
connection?.receive(minimumIncompleteLength: min, maximumLength: max) { content, _, isComplete, error in
|
|
if let error = error {
|
|
cont.resume(throwing: error)
|
|
return
|
|
}
|
|
if isComplete {
|
|
// cont.resume(returning: Data())
|
|
cont.resume(throwing: AccessoryError.disconnected("Error while receiving data"))
|
|
return
|
|
}
|
|
cont.resume(returning: content ?? Data())
|
|
}
|
|
}
|
|
}
|
|
|
|
func send(_ data: ToRadio) async throws {
|
|
let serialized = try data.serializedData()
|
|
var buffer = Data()
|
|
buffer.append(0x94)
|
|
buffer.append(0xc3)
|
|
var len = UInt16(serialized.count).bigEndian
|
|
withUnsafeBytes(of: &len) { buffer.append(contentsOf: $0) }
|
|
buffer.append(serialized)
|
|
|
|
try await withCheckedThrowingContinuation { (cont: CheckedContinuation<Void, Error>) in
|
|
connection?.send(content: buffer, completion: .contentProcessed { error in
|
|
if let error = error {
|
|
cont.resume(throwing: error)
|
|
} else {
|
|
cont.resume()
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func disconnect(withError error: Error? = nil, shouldReconnect: Bool) throws {
|
|
Logger.transport.debug("🌐 [TCP] Disconnecting from TCP connection")
|
|
readerTask?.cancel()
|
|
readerTask = nil
|
|
|
|
connection?.cancel()
|
|
connection = nil
|
|
|
|
if let error {
|
|
// Inform the AccessoryManager of the error and intent to reconnect
|
|
if shouldReconnect {
|
|
connectionStreamContinuation?.yield(.error(error))
|
|
} else {
|
|
connectionStreamContinuation?.yield(.errorWithoutReconnect(error))
|
|
}
|
|
} else {
|
|
connectionStreamContinuation?.yield(.disconnected(shouldReconnect: shouldReconnect))
|
|
}
|
|
|
|
connectionStreamContinuation?.finish()
|
|
connectionStreamContinuation = nil
|
|
}
|
|
|
|
func drainPendingPackets() async throws {
|
|
// For TCP, since reader is always running, no need to drain separately
|
|
}
|
|
|
|
func startDrainPendingPackets() throws {
|
|
// For TCP, reader is already started
|
|
}
|
|
|
|
private func getPacketStream() -> AsyncStream<ConnectionEvent> {
|
|
AsyncStream<ConnectionEvent> { continuation in
|
|
self.connectionStreamContinuation = continuation
|
|
continuation.onTermination = { _ in
|
|
Task { try await self.disconnect(withError: AccessoryError.eventStreamCancelled, shouldReconnect: true) }
|
|
}
|
|
}
|
|
}
|
|
|
|
func connect() async throws -> AsyncStream<ConnectionEvent> {
|
|
let newConnection = NWConnection(host: nwHost, port: nwPort, using: .tcp)
|
|
self.connection = newConnection
|
|
|
|
try await withTaskCancellationHandler {
|
|
try await withCheckedThrowingContinuation { cont in
|
|
newConnection.stateUpdateHandler = { state in
|
|
switch state {
|
|
case .ready:
|
|
cont.resume()
|
|
case .failed(let error):
|
|
cont.resume(throwing: error)
|
|
case .cancelled:
|
|
cont.resume(throwing: CancellationError())
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
newConnection.start(queue: queue)
|
|
}
|
|
} onCancel: {
|
|
newConnection.cancel()
|
|
}
|
|
|
|
// We've gotten here past the connection and since we haven't thrown, the
|
|
// connection is in the ready state.
|
|
|
|
// Update the state connection handler for in-progress monitoring of state
|
|
// changes while connected.
|
|
newConnection.stateUpdateHandler = { state in
|
|
switch state {
|
|
case .failed(let error):
|
|
Logger.transport.error("🌐 [TCP] Connection failed after ready: \(error, privacy: .public)")
|
|
Task {
|
|
try? await self.disconnect(withError: error, shouldReconnect: true)
|
|
}
|
|
case .cancelled:
|
|
Logger.transport.debug("🌐 [TCP] Connection cancelled")
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
|
|
startReader()
|
|
return getPacketStream()
|
|
|
|
}
|
|
|
|
func appDidEnterBackground() {
|
|
|
|
}
|
|
|
|
func appDidBecomeActive() {
|
|
|
|
}
|
|
}
|