mirror of
https://github.com/meshtastic/Meshtastic-Apple.git
synced 2026-04-20 22:13:56 +00:00
* Update messaging list separator insets
* Dont show unread messages or notifications for emoji reactions matching iMessage.
* Restore ble state method (#1416)
* Restore BLE State
* Log privacy
* AccessoryManager to handle restored connection
* Comment task out
* Update restore state function based on conversation with jake
* Update Meshtastic/Accessory/Transports/Bluetooth Low Energy/BLETransport.swift
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
* Update Meshtastic/Accessory/Transports/Bluetooth Low Energy/BLETransport.swift
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
---------
Co-authored-by: Jake-B <jake-b@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
* Two Column Node List (#1425)
* Restore BLE State
* Log privacy
* AccessoryManager to handle restored connection
* Comment task out
* Switch the node list to a two column layout
* Keep asian translations of channel details string
* Update restore state function based on conversation with jake
* Update Meshtastic/Accessory/Transports/Bluetooth Low Energy/BLETransport.swift
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
* Update Meshtastic/Accessory/Transports/Bluetooth Low Energy/BLETransport.swift
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
* always show node list search bar
* Update auto correct modifier
* Dont show online animations for ios 17, remove online animation from node map, remove online circle from position popover
* Work in progress.
* Update detents
* Gate the discovery process while restoring
* Use geometry reader to size weather tiles on node details
* Update BLE Transport
* Update location weather condistion styles
* Log privacy in didReceive
* Remove extra dividers from admin key config, fix onboarding typo
* Bump minimum catalyst target
* Bump mac target version
* Use @FetchRequest for UserList to try and use less memory on ios 17
* Revert change to @fetchrequest
* Stab in the dark for Devices crash
* Updated UserList (back?) to @FetchRequest
* Set mac minimum to 15
* Nil out continuation after use
* Use @FetchRequest for the node list to stop crashes on iOS 17
* Handle failed connections during restoration
---------
Co-authored-by: Jake-B <jake-b@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
* Update protos
* Update protos
* Remove stale keys
* Serbian translations update (#1422)
* Log privacy
* Add Serbian translations
---------
Co-authored-by: Garth Vander Houwen <garthvh@yahoo.com>
* Clarify public key sub-text in security settings (#1412)
* Clarify public key sub-text in settings
* Trigger lint
* freq slot num pad (#1410)
* kill keyboard toolbar on lora config
* delete extranious scrollDismissesKeyboard
* Properly set catalyst target
* Update Meshtastic/Views/Onboarding/DeviceOnboarding.swift
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
* Update Meshtastic/Views/Settings/Config/SecurityConfig.swift
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
* Update Meshtastic/Enums/DeviceEnums.swift
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
* Make current location nilable, remove log spam
* clean up toUser logic
* Fix telemetry entity not added in nodeInfoPacket
* fix typo: powerMetrics.hasChXCurrent mismatch
* Duplicate decoding of telemetry.current removed
* Clean up mesh map fetch request and distance filter logic
* Revert attempt to fix message logic
* Bump datadog version
* Missing message fix, attempt #2 (#1431)
Co-authored-by: Jake-B <jake-b@users.noreply.github.com>
* Retry fewer times for longer
* Revert "Missing message fix, attempt #2 (#1431)" (#1432)
This reverts commit a96d318adb.
* Make retry 2 seconds
* Add back link to node details from position popover without navigation stack and link, clear notifications when deleting database
* Add clear notifications function
* Link from channel messages to node info
* Link to node details
* Discovery on retry fix
* Discovery on retry fix fix
* Add contact to device node db if you get an encrypted send faild routing error
* Seperate channel message view into two views for better performance.
* Refactor User Message List
* Update device hardware
Add liquid glass to config save button
* Save button cleanup
* Update button structure on users view
* Move encrypted send logic out of the router. Update protos
* Restore node long- and short- names (#1442)
Co-authored-by: Jake-B <jake-b@users.noreply.github.com>
* Update Meshtastic/Accessory/Transports/Bluetooth Low Energy/BLEConnection.swift
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
* Revert routing error
* Toggle for enabling device telemetry broadcast enable
* Update
* Enhancements for interval dropdowns (#1445)
* Cleanup
* Fix core data version
* Add never to update interval
* Device telemetry Enabled Boolean (#1446)
* Update core data and interval picker
* Move formatter
* Rework to nest options under enabled
* Clearer names
* Safer devicehardware api call, remove node history filter from mesh map
* Fix build
* Simplify mesh map filter
* Remove stale translation keys
---------
Co-authored-by: Jake-B <jake-b@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Nikola Dašić <dasic.nikola@yandex.com>
Co-authored-by: Spencer Smith <dontaskspencer@gmail.com>
Co-authored-by: Benjamin Faershtein <119711889+RCGV1@users.noreply.github.com>
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
239 lines
6.8 KiB
Swift
239 lines
6.8 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 {
|
||
let capturedConnection = connection
|
||
return try await withTaskCancellationHandler {
|
||
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
|
||
}
|
||
if let content {
|
||
cont.resume(returning: content)
|
||
} else {
|
||
cont.resume(returning: Data())
|
||
}
|
||
}
|
||
}
|
||
} onCancel: {
|
||
// ✨ onCancel cannot directly resume the continuation (it doesn’t know if it’s already been resumed).
|
||
// A safe pattern is to cancel the underlying NWConnection. That forces the receive completion
|
||
// handler to fire with an error, where you can safely resume the continuation.
|
||
capturedConnection?.cancel()
|
||
}
|
||
}
|
||
|
||
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() {
|
||
|
||
}
|
||
}
|