Merge branch 'upstream/main' into noise-floor

Conflicts resolved:
- AccessoryManager+ToRadio.swift: Keep both sendLocalStatsRequest and exchangeUserInfo
- NodeDetail.swift: Keep both RequestLocalStatsButton and ExchangeUserInfoButton
- project.pbxproj: Add ExchangeUserInfoButton.swift to project
This commit is contained in:
Benjamin Faershtein 2026-01-17 19:38:55 -08:00
commit 9693625dcf
28 changed files with 769 additions and 196 deletions

View file

@ -13737,6 +13737,9 @@
}
}
}
},
"Exchange User Info" : {
},
"Exclamation" : {
"localizations" : {
@ -14217,6 +14220,9 @@
}
}
}
},
"Failed to exchange user info." : {
},
"Failed to get a valid position to exchange" : {
"localizations" : {
@ -14343,6 +14349,7 @@
}
},
"Favorited and ignored nodes are always retained. Nodes without PKC keys are cleared from the app database on the schedule set by the user, nodes with PKC keys are cleared only if the interval is set to 7 days or longer. This feature only purges nodes from the app that are not stored in the device node database." : {
"extractionState" : "stale",
"localizations" : {
"de" : {
"stringUnit" : {
@ -14363,6 +14370,9 @@
}
}
}
},
"Favorited and ignored nodes are always retained. Other nodes are cleared from the app database on the schedule set by the user. (Nodes with PKC keys are always retained for at least 7 days.) This feature only purges nodes from the app that are not stored in the device node database." : {
},
"Favorites" : {
"localizations" : {
@ -31701,6 +31711,9 @@
}
}
}
},
"Select an emoji" : {
},
"Select Channel" : {
"localizations" : {
@ -35631,6 +35644,9 @@
}
}
}
},
"Tap to enter emoji" : {
},
"Tapback" : {
"localizations" : {
@ -40311,6 +40327,12 @@
}
}
}
},
"User Info Exchange Failed" : {
},
"User Info Sent" : {
},
"User Privacy" : {
@ -42402,6 +42424,9 @@
}
}
}
},
"Your user info has been sent with a request for a response with their user info." : {
}
},
"version" : "1.1"

View file

@ -66,6 +66,7 @@
251926852C3BA97800249DF5 /* FavoriteNodeButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 251926842C3BA97800249DF5 /* FavoriteNodeButton.swift */; };
251926872C3BAE2200249DF5 /* NodeAlertsButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 251926862C3BAE2200249DF5 /* NodeAlertsButton.swift */; };
2519268A2C3BB1B200249DF5 /* ExchangePositionsButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 251926892C3BB1B200249DF5 /* ExchangePositionsButton.swift */; };
DD4074692F1233F400BCC22F /* ExchangeUserInfoButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD4074682F1233F400BCC22F /* ExchangeUserInfoButton.swift */; };
2519268C2C3BB52000249DF5 /* TraceRouteButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2519268B2C3BB52000249DF5 /* TraceRouteButton.swift */; };
251926902C3CB44900249DF5 /* ClientHistoryButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2519268F2C3CB44900249DF5 /* ClientHistoryButton.swift */; };
251926922C3CB52300249DF5 /* DeleteNodeButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 251926912C3CB52300249DF5 /* DeleteNodeButton.swift */; };
@ -385,6 +386,7 @@
251926842C3BA97800249DF5 /* FavoriteNodeButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoriteNodeButton.swift; sourceTree = "<group>"; };
251926862C3BAE2200249DF5 /* NodeAlertsButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeAlertsButton.swift; sourceTree = "<group>"; };
251926892C3BB1B200249DF5 /* ExchangePositionsButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExchangePositionsButton.swift; sourceTree = "<group>"; };
DD4074682F1233F400BCC22F /* ExchangeUserInfoButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExchangeUserInfoButton.swift; sourceTree = "<group>"; };
2519268B2C3BB52000249DF5 /* TraceRouteButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TraceRouteButton.swift; sourceTree = "<group>"; };
2519268F2C3CB44900249DF5 /* ClientHistoryButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientHistoryButton.swift; sourceTree = "<group>"; };
251926912C3CB52300249DF5 /* DeleteNodeButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeleteNodeButton.swift; sourceTree = "<group>"; };
@ -852,6 +854,7 @@
DDDFE73E2D0D48FF0044463C /* IgnoreNodeButton.swift */,
251926842C3BA97800249DF5 /* FavoriteNodeButton.swift */,
251926892C3BB1B200249DF5 /* ExchangePositionsButton.swift */,
DD4074682F1233F400BCC22F /* ExchangeUserInfoButton.swift */,
DDF82CBC2D5BC69200DC25EC /* NavigateToButton.swift */,
251926862C3BAE2200249DF5 /* NodeAlertsButton.swift */,
2519268B2C3BB52000249DF5 /* TraceRouteButton.swift */,
@ -1821,6 +1824,7 @@
DDF82CBD2D5BC69200DC25EC /* NavigateToButton.swift in Sources */,
8D3F8A3F2D44BB02009EAAA4 /* PowerMetrics.swift in Sources */,
2519268A2C3BB1B200249DF5 /* ExchangePositionsButton.swift in Sources */,
DD4074692F1233F400BCC22F /* ExchangeUserInfoButton.swift in Sources */,
DD86D40A287F04F100BAEB7A /* InvalidVersion.swift in Sources */,
233E99BE2D849D3200CC3A77 /* RadiationCompactWidget.swift in Sources */,
DDD94A502845C8F5004A87A0 /* DateTimeText.swift in Sources */,

View file

@ -48,7 +48,7 @@ extension AccessoryManager {
}
// Step 1: Setup the connection
Step(timeout: .seconds(2)) { @MainActor _ in
Step(timeout: .seconds(5)) { @MainActor _ in
Logger.transport.info("🔗👟[Connect] Step 1: connection to \(device.id, privacy: .public)")
do {
let connection: Connection
@ -352,7 +352,6 @@ actor SequentialSteps {
return
}
isRunning = false
return
throw AccessoryError.tooManyRetries
}

View file

@ -15,7 +15,7 @@ extension AccessoryManager {
let tasks = transports.map { transport in
Task {
Logger.transport.info("🔎 [Discovery] Discovery stream started for transport \(String(describing: transport.type), privacy: .public)")
for await event in transport.discoverDevices() {
for await event in await transport.discoverDevices() {
continuation.yield(event)
}
Logger.transport.info("🔎 [Discovery] Discovery stream closed for transport \(String(describing: transport.type), privacy: .public)")

View file

@ -2111,6 +2111,36 @@ extension AccessoryManager {
try await sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription)
}
public func exchangeUserInfo(fromUser: UserEntity, toUser: UserEntity) async throws -> Int64 {
let userProto = fromUser.toProto()
guard let userPayload: Data = try? userProto.serializedData() else {
throw AccessoryError.ioFailed("exchangeUserInfo: Unable to serialize User protobuf")
}
var dataMessage = DataMessage()
dataMessage.payload = userPayload
dataMessage.portnum = PortNum.nodeinfoApp
dataMessage.wantResponse = true
var meshPacket: MeshPacket = MeshPacket()
meshPacket.to = UInt32(toUser.num)
meshPacket.from = UInt32(fromUser.num)
meshPacket.id = UInt32.random(in: UInt32(UInt8.max)..<UInt32.max)
meshPacket.priority = MeshPacket.Priority.reliable
meshPacket.wantAck = true
meshPacket.channel = UInt32(toUser.userNode?.channel ?? 0)
meshPacket.decoded = dataMessage
var toRadio: ToRadio = ToRadio()
toRadio.packet = meshPacket
let logString = String.localizedStringWithFormat("Sent User Info Exchange request from %@ to %@".localized, fromUser.longName ?? "Unknown".localized, toUser.longName ?? "Unknown".localized)
try await send(toRadio, debugDescription: logString)
return Int64(meshPacket.id)
}
func sendLocalStatsRequest(destNum: Int64, wantResponse: Bool) async throws {
guard let fromNodeNum = self.activeConnection?.device.num else {
Logger.services.error("Error while sending local stats request. No active device.")

View file

@ -196,6 +196,8 @@ class AccessoryManager: ObservableObject, MqttClientProxyManagerDelegate {
Logger.transport.error("Unable to send wantConfig (config): No device connected")
return
}
_ = clearStaleNodes(nodeExpireDays: Int(UserDefaults.purgeStaleNodeDays), context: self.context)
try await withTaskCancellationHandler {
var toRadio: ToRadio = ToRadio()

View file

@ -14,7 +14,7 @@ protocol Connection: Actor {
var isConnected: Bool { get }
func send(_ data: ToRadio) async throws
func connect() async throws -> AsyncStream<ConnectionEvent>
func disconnect(withError: Error?, shouldReconnect: Bool) throws
func disconnect(withError: Error?, shouldReconnect: Bool) async throws
func drainPendingPackets() async throws
func startDrainPendingPackets() throws

View file

@ -42,10 +42,10 @@ enum DiscoveryEvent {
protocol Transport {
var type: TransportType { get }
var status: TransportStatus { get }
var status: TransportStatus { get async }
// Discovers devices asynchronously. For ongoing scans (e.g., BLE), this can yield via AsyncStream.
func discoverDevices() -> AsyncStream<DiscoveryEvent>
func discoverDevices() async -> AsyncStream<DiscoveryEvent>
// Connects to a device and returns a Connection.
func connect(to device: Device) async throws -> any Connection

View file

@ -69,7 +69,7 @@ actor BLEConnection: Connection {
self.delegate.setConnection(self)
}
func disconnect(withError error: Error? = nil, shouldReconnect: Bool) throws {
func disconnect(withError error: Error? = nil, shouldReconnect: Bool) async throws {
if peripheral.state == .connected {
if let characteristic = FROMRADIO_characteristic {
peripheral.setNotifyValue(false, for: characteristic)
@ -82,7 +82,7 @@ actor BLEConnection: Connection {
}
}
transport?.connectionDidDisconnect(fromPeripheral: peripheral)
await transport?.connectionDidDisconnect(fromPeripheral: peripheral)
central.cancelPeripheralConnection(peripheral)
peripheral.delegate = nil
@ -217,8 +217,8 @@ actor BLEConnection: Connection {
self.connectContinuation = nil
}
private func notifyTransportOfDisconnect() {
transport?.connectionDidDisconnect(fromPeripheral: peripheral)
private func notifyTransportOfDisconnect() async {
await transport?.connectionDidDisconnect(fromPeripheral: peripheral)
}
func startRSSITask() {
@ -450,7 +450,7 @@ actor BLEConnection: Connection {
}
// Inform the active connection that there was an error and it should disconnect
try self.disconnect(withError: error, shouldReconnect: shouldReconnect)
try await self.disconnect(withError: error, shouldReconnect: shouldReconnect)
}
func appDidEnterBackground() {

View file

@ -10,7 +10,7 @@ import Foundation
import SwiftUI
import OSLog
class BLETransport: Transport {
actor BLETransport: Transport {
let meshtasticServiceCBUUID = CBUUID(string: "0x6BA1B218-15A8-461F-9FA8-5DCAE273EAFD")
private let kCentralRestoreID = "com.meshtastic.central"
@ -31,7 +31,7 @@ class BLETransport: Transport {
private var cleanupTask: Task<Void, Never>?
// Transport properties
var supportsManualConnection: Bool = false
let supportsManualConnection: Bool = false
let requiresPeriodicHeartbeat = false
init() {
@ -46,19 +46,24 @@ class BLETransport: Transport {
self.delegate.setTransport(self)
}
nonisolated func discoverDevices() -> AsyncStream<DiscoveryEvent> {
private func setDiscoveredDeviceContinuation(_ cont: AsyncStream<DiscoveryEvent>.Continuation?) {
self.discoveredDeviceContinuation = cont
}
func discoverDevices() -> AsyncStream<DiscoveryEvent> {
AsyncStream { cont in
Task {
self.discoveredDeviceContinuation = cont
await self.setDiscoveredDeviceContinuation(cont)
// This gate is opened when the CBCentralManager is in poweredOn state.
// Its probably open already, but just to be sure in case we get here too quickly.
try await self.setupCompleteGate.wait()
if !restoreInProgress {
if await !self.restoreInProgress {
centralManager.scanForPeripherals(withServices: [meshtasticServiceCBUUID], options: [CBCentralManagerScanOptionAllowDuplicatesKey: true])
for alreadyDiscoveredPeripheral in self.discoveredPeripherals.values.map({$0.peripheral}) {
let peripherals = await self.discoveredPeripherals.values.map({$0.peripheral})
for alreadyDiscoveredPeripheral in peripherals {
let device = Device(id: alreadyDiscoveredPeripheral.identifier,
name: alreadyDiscoveredPeripheral.name ?? "Unknown",
transportType: .ble,
@ -66,11 +71,13 @@ class BLETransport: Transport {
cont.yield(.deviceFound(device))
}
}
setupCleanupTask()
await setupCleanupTask()
}
cont.onTermination = { _ in
Logger.transport.error("🛜 [BLE] Discovery event stream has been canecelled.")
self.stopScanning()
Task {
await self.stopScanning()
}
}
}
}
@ -188,6 +195,12 @@ class BLETransport: Transport {
}
}
private func cancelConnectContinuation(for peripheral: CBPeripheral) {
self.connectContinuation?.resume(throwing: CancellationError())
self.connectContinuation = nil
self.connectionDidDisconnect(fromPeripheral: peripheral)
}
func connect(to device: Device) async throws -> any Connection {
guard let peripheral = discoveredPeripherals[UUID(uuidString: device.identifier)!] else {
throw AccessoryError.connectionFailed("Peripheral not found")
@ -211,9 +224,9 @@ class BLETransport: Transport {
self.activeConnection = newConnection
return newConnection
} onCancel: {
self.connectContinuation?.resume(throwing: CancellationError())
self.connectContinuation = nil
self.connectionDidDisconnect(fromPeripheral: peripheral.peripheral)
Task {
await self.cancelConnectContinuation(for: peripheral.peripheral)
}
}
Logger.transport.debug("🛜 [BLE] Connect complete.")
return returnConnection
@ -226,7 +239,7 @@ class BLETransport: Transport {
Task {
if await connection.peripheral.identifier == peripheral.identifier {
try await connection.disconnect(withError: AccessoryError.disconnected("BLE connection lost"), shouldReconnect: true)
self.connectionDidDisconnect(fromPeripheral: peripheral)
await self.connectionDidDisconnect(fromPeripheral: peripheral)
}
}
}
@ -264,7 +277,7 @@ class BLETransport: Transport {
Logger.transport.debug("🛜 [BLETransport] Error while connecting. Disconnecting the active connection.")
Task {
try? await activeConnection.disconnect(withError: error, shouldReconnect: shouldReconnect)
self.connectionDidDisconnect(fromPeripheral: peripheral)
await self.connectionDidDisconnect(fromPeripheral: peripheral)
}
} else {
Logger.transport.error("🚨 [BLETransport] unhandled error. May be in an inconsistent state.")
@ -372,15 +385,20 @@ class BLETransport: Transport {
}
Logger.transport.error("🛜 [BLE] Restoring peripheral in connecting state. ✅ didConnect Received!")
Task { @MainActor in
// In this case we need a full reconnect, so do the wantConfig, wantDatabase, and versionCheck
try? await AccessoryManager.shared.connect(to: device, withConnection: restoredConnection, wantConfig: true, wantDatabase: true, versionCheck: true)
restoreInProgress = false
let connectTask = Task { @MainActor in
try await AccessoryManager.shared.connect(to: device, withConnection: restoredConnection, wantConfig: true, wantDatabase: true, versionCheck: true)
}
do {
try await connectTask.value
} catch {
Logger.transport.error("🛜 [BLE] Error connecting during state restoration: \(error, privacy: .public)")
}
self.restoreInProgress = false
} catch {
// We had a conneciton failure during restoration.
// We had a connection failure during restoration.
Logger.transport.error("🛜 [BLE] Error restoring peripheral in connecting state. \(error, privacy: .public)")
restoreInProgress = false
self.restoreInProgress = false
}
}
@ -388,22 +406,28 @@ class BLETransport: Transport {
let restoredConnection = BLEConnection(peripheral: peripheral, central: central, transport: self)
self.activeConnection = restoredConnection
Logger.transport.error("🛜 [BLE] Peripheral Connection found and state is connected setting this connection as the activeConnection.")
Task { @MainActor in
let connectTask = Task { @MainActor in
// In this case we need a full reconnect, so do the wantConfig, wantDatabase, and versionCheck
try? await AccessoryManager.shared.connect(to: device, withConnection: restoredConnection, wantConfig: false, wantDatabase: false, versionCheck: false)
restoreInProgress = false
try await AccessoryManager.shared.connect(to: device, withConnection: restoredConnection, wantConfig: false, wantDatabase: false, versionCheck: false)
}
do {
try await connectTask.value
} catch {
Logger.transport.error("🛜 [BLE] Error connecting during state restoration: \(error, privacy: .public)")
}
self.restoreInProgress = false
Logger.transport.error("🛜 [BLE] Connection state successfully restored in the background.")
default:
// Since we're not going to attempt to reconnect in then allow normal device discovery
Logger.transport.error("🛜 [BLE] Unhandled state restoration for state: \(cbPeripheralStateDescription(peripheral.state), privacy: .public).")
restoreInProgress = false
self.restoreInProgress = false
}
}
}
func device(forManualConnection: String) -> Device? {
nonisolated func device(forManualConnection: String) -> Device? {
return nil
}
@ -438,33 +462,33 @@ class BLEDelegate: NSObject, CBCentralManagerDelegate {
}
func centralManagerDidUpdateState(_ central: CBCentralManager) {
transport?.handleCentralState(central.state, central: central)
Task { await transport?.handleCentralState(central.state, central: central) }
}
func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String: Any], rssi RSSI: NSNumber) {
transport?.didDiscover(peripheral: peripheral, rssi: RSSI)
Task { await transport?.didDiscover(peripheral: peripheral, rssi: RSSI) }
}
func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
transport?.handleDidConnect(peripheral: peripheral, central: central)
Task { await transport?.handleDidConnect(peripheral: peripheral, central: central) }
}
func centralManager(_ central: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) {
transport?.handleDidFailToConnect(peripheral: peripheral, error: error)
Task { await transport?.handleDidFailToConnect(peripheral: peripheral, error: error) }
}
func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) {
if let error = error as? NSError {
Logger.transport.error("🛜 [BLETransport] Error while disconnecting peripheral: \(peripheral.name ?? ""): \(error)")
transport?.handlePeripheralDisconnectError(peripheral: peripheral, error: error)
Task { await transport?.handlePeripheralDisconnectError(peripheral: peripheral, error: error) }
} else {
Logger.transport.error("🛜 [BLETransport] Did succesfully disconnect peripheral: \(peripheral.name ?? "")")
transport?.handlePeripheralDisconnect(peripheral: peripheral)
Task { await transport?.handlePeripheralDisconnect(peripheral: peripheral) }
}
}
func centralManager(_ central: CBCentralManager, willRestoreState dict: [String: Any]) {
self.transport?.handleWillRestoreState(dict: dict, central: central)
Task { await self.transport?.handleWillRestoreState(dict: dict, central: central) }
}
}

View file

@ -0,0 +1,13 @@
//
// UIKeyboard.swift
// Meshtastic
//
// Copyright(c) Garth Vander Houwen 1/7/26.
//
import UIKit
extension UIKeyboardType {
static var emoji: UIKeyboardType {
return UIKeyboardType(rawValue: 124) ?? .default
}
}

View file

@ -80,6 +80,7 @@ extension UserDefaults {
case showDeviceOnboarding
case usageDataAndCrashReporting
case autoconnectOnDiscovery
case purgeStaleNodeDays
case manualConnections
case testIntEnum
}
@ -178,6 +179,9 @@ extension UserDefaults {
@UserDefault(.autoconnectOnDiscovery, defaultValue: true)
static var autoconnectOnDiscovery: Bool
@UserDefault(.purgeStaleNodeDays, defaultValue: 0)
static var purgeStaleNodeDays: Double
@UserDefault(.testIntEnum, defaultValue: .one)
static var testIntEnum: TestIntEnum

View file

@ -7,6 +7,7 @@
import SwiftUI
class SwiftUIEmojiTextField: UITextField {
var shouldBecomeFirstResponderOnAppear = false
func setEmoji() {
_ = self.textInputMode
@ -23,22 +24,39 @@ class SwiftUIEmojiTextField: UITextField {
}
return nil
}
override func didMoveToWindow() {
super.didMoveToWindow()
if shouldBecomeFirstResponderOnAppear && window != nil {
DispatchQueue.main.async { [weak self] in
self?.becomeFirstResponder()
}
}
}
}
struct EmojiOnlyTextField: UIViewRepresentable {
@Binding var text: String
var placeholder: String = ""
var onBecomeFirstResponder: (() -> Void)?
var onKeyboardTypeChanged: ((Bool) -> Void)? // true if NOT emoji (should dismiss), false if emoji
var onKeyboardDismissed: (() -> Void)? // Called when keyboard is dismissed
func makeUIView(context: Context) -> SwiftUIEmojiTextField {
let emojiTextField = SwiftUIEmojiTextField()
emojiTextField.placeholder = placeholder
emojiTextField.text = text
emojiTextField.delegate = context.coordinator
emojiTextField.shouldBecomeFirstResponderOnAppear = true
context.coordinator.textField = emojiTextField
return emojiTextField
}
func updateUIView(_ uiView: SwiftUIEmojiTextField, context: Context) {
uiView.text = text
context.coordinator.onBecomeFirstResponder = onBecomeFirstResponder
context.coordinator.onKeyboardTypeChanged = onKeyboardTypeChanged
context.coordinator.onKeyboardDismissed = onKeyboardDismissed
}
func makeCoordinator() -> Coordinator {
@ -47,13 +65,41 @@ struct EmojiOnlyTextField: UIViewRepresentable {
class Coordinator: NSObject, UITextFieldDelegate {
var parent: EmojiOnlyTextField
var textField: SwiftUIEmojiTextField?
var onBecomeFirstResponder: (() -> Void)?
var onKeyboardTypeChanged: ((Bool) -> Void)?
var onKeyboardDismissed: (() -> Void)?
var previousInputMode: String?
init(parent: EmojiOnlyTextField) {
self.parent = parent
}
func textFieldDidBeginEditing(_ textField: UITextField) {
onBecomeFirstResponder?()
checkInputMode(textField)
}
func textFieldDidEndEditing(_ textField: UITextField) {
// Keyboard was dismissed
onKeyboardDismissed?()
}
func textFieldDidChangeSelection(_ textField: UITextField) {
DispatchQueue.main.async { [weak self] in
self?.parent.text = textField.text ?? ""
}
checkInputMode(textField)
}
private func checkInputMode(_ textField: UITextField) {
if let inputMode = textField.textInputMode {
let isEmoji = inputMode.primaryLanguage == "emoji"
if previousInputMode != inputMode.primaryLanguage {
previousInputMode = inputMode.primaryLanguage
onKeyboardTypeChanged?(!isEmoji) // true if NOT emoji (should dismiss)
}
}
}
}
}

View file

@ -882,8 +882,8 @@ func telemetryPacket(packet: MeshPacket, connectedNode: Int64, context: NSManage
let meshActivity = Activity<MeshActivityAttributes>.activities.first(where: { $0.attributes.nodeNum == connectedNode })
if meshActivity != nil {
Task {
await meshActivity?.update(updatedContent, alertConfiguration: alertConfiguration)
// await meshActivity?.update(updatedContent)
// await meshActivity?.update(updatedContent, alertConfiguration: alertConfiguration)
await meshActivity?.update(updatedContent)
Logger.services.debug("Updated live activity.")
}
}

View file

@ -10,6 +10,7 @@ struct MessageContextMenuItems: View {
let tapBackDestination: MessageDestination
let isCurrentUser: Bool
@Binding var isShowingDeleteConfirmation: Bool
@Binding var isShowingTapbackInput: Bool
let onReply: () -> Void
@State var relayDisplay: String? = nil
@ -29,29 +30,10 @@ struct MessageContextMenuItems: View {
}
}
Menu("Tapback") {
ForEach(Tapbacks.allCases) { tb in
Button {
Task {
do {
try await accessoryManager.sendMessage(
message: tb.emojiString,
toUserNum: tapBackDestination.userNum,
channel: tapBackDestination.channelNum,
isEmoji: true,
replyID: message.messageId
)
Task { @MainActor in
self.context.refresh(tapBackDestination.managedObject, mergeChanges: true)
}
} catch {
Logger.services.warning("Failed to send tapback.")
}
}
} label: {
Text(tb.description)
Image(uiImage: tb.emojiString.image()!)
}
Button("Tapback") {
// The context menu needs a moment to dismiss before the focus state can be changed.
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
isShowingTapbackInput = true
}
}

View file

@ -28,101 +28,15 @@ struct MessageText: View {
@State private var saveChannelLink: SaveChannelLinkData?
@State private var isShowingDeleteConfirmation = false
@FocusState private var isTapbackInputFocused: Bool
@State private var tapbackText = ""
var body: some View {
SessionReplayPrivacyView(textAndInputPrivacy: .maskAll) {
let markdownText = LocalizedStringKey(message.messagePayloadMarkdown ?? (message.messagePayload ?? "EMPTY MESSAGE"))
return Text(markdownText)
.tint(Self.linkBlue)
.padding(.vertical, 10)
.padding(.horizontal, 8)
.foregroundColor(.white)
.background(isCurrentUser ? .accentColor : Color(.gray))
.cornerRadius(15)
.overlay {
/// Show the lock if the message is pki encrypted and has a real ack if sent by the current user, or is pki encrypted for incoming messages
if message.pkiEncrypted && message.realACK || !isCurrentUser && message.pkiEncrypted {
VStack(alignment: .trailing) {
Spacer()
HStack {
Spacer()
Image(systemName: "lock.circle.fill")
.symbolRenderingMode(.palette)
.foregroundStyle(.white, .green)
.font(.system(size: 20))
.offset(x: 8, y: 8)
}
}
}
let isStoreAndForward = message.portNum == Int32(PortNum.storeForwardApp.rawValue)
let isDetectionSensorMessage = message.portNum == Int32(PortNum.detectionSensorApp.rawValue)
if isStoreAndForward {
VStack(alignment: .trailing) {
Spacer()
HStack {
Spacer()
Image(systemName: "envelope.circle.fill")
.symbolRenderingMode(.palette)
.foregroundStyle(.white, .gray)
.font(.system(size: 20))
.offset(x: 8, y: 8)
}
}
}
if tapBackDestination.overlaySensorMessage {
VStack {
isDetectionSensorMessage ? Image(systemName: "sensor.fill")
.padding()
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topTrailing)
.foregroundStyle(Color.orange)
.symbolRenderingMode(.multicolor)
.symbolEffect(.variableColor.reversing.cumulative, options: .repeat(20).speed(3))
.offset(x: 20, y: -20)
: nil
}
} else {
EmptyView()
}
}
.contextMenu {
MessageContextMenuItems(
message: message,
tapBackDestination: tapBackDestination,
isCurrentUser: isCurrentUser,
isShowingDeleteConfirmation: $isShowingDeleteConfirmation,
onReply: onReply
)
}
messageContent
.environment(\.openURL, OpenURLAction { url in
saveChannelLink = nil
var addChannels = false
if url.absoluteString.lowercased().contains("meshtastic.org/v/#") {
// Handle contact URL
ContactURLHandler.handleContactUrl(url: url, accessoryManager: AccessoryManager.shared)
return .handled // Prevent default browser opening
} else if url.absoluteString.lowercased().contains("meshtastic.org/e/") {
// Handle channel URL
let components = url.absoluteString.components(separatedBy: "#")
guard !components.isEmpty, let lastComponent = components.last else {
Logger.services.error("No valid components found in channel URL: \(url.absoluteString, privacy: .public)")
return .discarded
}
addChannels = Bool(url.query?.contains("add=true") ?? false)
guard let lastComponent = components.last else {
Logger.services.error("Channel URL missing fragment component: \(url.absoluteString, privacy: .public)")
self.saveChannelLink = nil
return .discarded
}
let cs = lastComponent.components(separatedBy: "?").first ?? ""
self.saveChannelLink = SaveChannelLinkData(data: cs, add: addChannels)
Logger.services.debug("Add Channel: \(addChannels, privacy: .public)")
Logger.mesh.debug("Opening Channel Settings URL: \(url.absoluteString, privacy: .public)")
return .handled // Prevent default browser opening
}
return .systemAction // Open other URLs in browser
handleURL(url)
})
// Display sheet for channel settings
.sheet(item: $saveChannelLink) { link in
SaveChannelQRCode(
channelSetLink: link.data,
@ -138,17 +52,155 @@ struct MessageText: View {
titleVisibility: .visible
) {
Button("Delete Message", role: .destructive) {
context.delete(message)
do {
try context.save()
} catch {
Logger.data.error("Failed to delete message \(message.messageId, privacy: .public): \(error.localizedDescription, privacy: .public)")
}
deleteMessage()
}
Button("Cancel", role: .cancel) {}
}
}
}
private var messageContent: some View {
let markdownText = LocalizedStringKey(message.messagePayloadMarkdown ?? (message.messagePayload ?? "EMPTY MESSAGE"))
return Text(markdownText)
.tint(Self.linkBlue)
.padding(.vertical, 10)
.padding(.horizontal, 8)
.foregroundColor(.white)
.background(isCurrentUser ? .accentColor : Color(.gray))
.cornerRadius(15)
.background {
TextField("", text: $tapbackText)
.keyboardType(.emoji)
.scrollDismissesKeyboard(.immediately)
.focused($isTapbackInputFocused)
.frame(width: 0, height: 0)
.opacity(0)
.onChange(of: tapbackText) {
processTapback()
}
}
.overlay(messageOverlays)
.contextMenu {
MessageContextMenuItems(
message: message,
tapBackDestination: tapBackDestination,
isCurrentUser: isCurrentUser,
isShowingDeleteConfirmation: $isShowingDeleteConfirmation,
isShowingTapbackInput: Binding(
get: { isTapbackInputFocused },
set: { isTapbackInputFocused = $0 }
),
onReply: onReply
)
}
}
@ViewBuilder
private var messageOverlays: some View {
if message.pkiEncrypted && message.realACK || !isCurrentUser && message.pkiEncrypted {
VStack(alignment: .trailing) {
Spacer()
HStack {
Spacer()
Image(systemName: "lock.circle.fill")
.symbolRenderingMode(.palette)
.foregroundStyle(.white, .green)
.font(.system(size: 20))
.offset(x: 8, y: 8)
}
}
}
if message.portNum == Int32(PortNum.storeForwardApp.rawValue) {
VStack(alignment: .trailing) {
Spacer()
HStack {
Spacer()
Image(systemName: "envelope.circle.fill")
.symbolRenderingMode(.palette)
.foregroundStyle(.white, .gray)
.font(.system(size: 20))
.offset(x: 8, y: 8)
}
}
}
if tapBackDestination.overlaySensorMessage && message.portNum == Int32(PortNum.detectionSensorApp.rawValue) {
Image(systemName: "sensor.fill")
.padding()
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topTrailing)
.foregroundStyle(Color.orange)
.symbolRenderingMode(.multicolor)
.symbolEffect(.variableColor.reversing.cumulative, options: .repeat(20).speed(3))
.offset(x: 20, y: -20)
}
}
private func handleURL(_ url: URL) -> OpenURLAction.Result {
saveChannelLink = nil
var addChannels = false
if url.absoluteString.lowercased().contains("meshtastic.org/v/#") {
// Handle contact URL
ContactURLHandler.handleContactUrl(url: url, accessoryManager: AccessoryManager.shared)
return .handled // Prevent default browser opening
} else if url.absoluteString.lowercased().contains("meshtastic.org/e/") {
// Handle channel URL
let components = url.absoluteString.components(separatedBy: "#")
guard !components.isEmpty, let lastComponent = components.last else {
Logger.services.error("No valid components found in channel URL: \(url.absoluteString, privacy: .public)")
return .discarded
}
addChannels = Bool(url.query?.contains("add=true") ?? false)
guard let lastComponent = components.last else {
Logger.services.error("Channel URL missing fragment component: \(url.absoluteString, privacy: .public)")
self.saveChannelLink = nil
return .discarded
}
let cs = lastComponent.components(separatedBy: "?").first ?? ""
self.saveChannelLink = SaveChannelLinkData(data: cs, add: addChannels)
Logger.services.debug("Add Channel: \(addChannels, privacy: .public)")
Logger.mesh.debug("Opening Channel Settings URL: \(url.absoluteString, privacy: .public)")
return .handled // Prevent default browser opening
}
return .systemAction // Open other URLs in browser
}
private func deleteMessage() {
context.delete(message)
do {
try context.save()
} catch {
Logger.data.error("Failed to delete message \(message.messageId, privacy: .public): \(error.localizedDescription, privacy: .public)")
}
}
private func processTapback() {
guard !tapbackText.isEmpty else { return }
let emojiToSend = tapbackText
Task {
do {
try await accessoryManager.sendMessage(
message: emojiToSend,
toUserNum: tapBackDestination.userNum,
channel: tapBackDestination.channelNum,
isEmoji: true,
replyID: message.messageId
)
await MainActor.run {
switch tapBackDestination {
case let .channel(channel):
context.refresh(channel, mergeChanges: true)
case let .user(user):
context.refresh(user, mergeChanges: true)
}
}
} catch {
Logger.services.warning("Failed to send tapback.")
}
}
tapbackText = ""
isTapbackInputFocused = false
}
}
private extension MessageDestination {

View file

@ -0,0 +1,80 @@
import SwiftUI
import UIKit
struct TapbackInputView: View {
@Binding var text: String
@Binding var isPresented: Bool
let onEmojiSelected: (String) -> Void
var body: some View {
NavigationView {
VStack(spacing: 0) {
TextField("Tap to enter emoji", text: $text)
.keyboardType(.emoji)
.frame(height: 50)
.padding(.horizontal)
.background(
RoundedRectangle(cornerRadius: 10)
.strokeBorder(.tertiary, lineWidth: 1)
.background(RoundedRectangle(cornerRadius: 10).fill(Color(.systemBackground)))
)
.padding(.horizontal)
.padding(.top, 8)
.onChange(of: text) { oldValue, newValue in
// Extract first emoji character and send it
if !newValue.isEmpty, let firstEmoji = extractFirstEmoji(from: newValue) {
onEmojiSelected(firstEmoji)
// Clear the text box after getting the emoji
text = ""
}
}
}
.navigationTitle("Tapback")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button("Cancel") {
isPresented = false
}
}
}
}
.presentationDetents([.height(120)])
}
private func extractFirstEmoji(from string: String) -> String? {
// Extract the first emoji character(s) - handle both single and multi-scalar emojis
guard !string.isEmpty else { return nil }
// Try to get the first character
let firstChar = string[string.startIndex]
// Check if it's an emoji using the existing extension
if firstChar.isEmoji {
// For multi-scalar emojis (like emojis with skin tones), we need to find the full emoji sequence
var emojiEnd = string.index(after: string.startIndex)
// Check if there are continuation scalars (for emojis with skin tones, variation selectors, etc.)
while emojiEnd < string.endIndex {
let nextChar = string[emojiEnd]
// Check if this is a continuation (variation selector, skin tone modifier, zero-width joiner, etc.)
if let scalar = nextChar.unicodeScalars.first,
(scalar.properties.isVariationSelector ||
scalar.value == 0xFE0F || // Variation selector
(scalar.value >= 0x1F3FB && scalar.value <= 0x1F3FF) || // Skin tone modifiers
scalar.value == 0x200D) { // Zero-width joiner
emojiEnd = string.index(after: emojiEnd)
} else if nextChar.isEmoji {
// If it's another emoji, include it (for compound emojis like flags)
emojiEnd = string.index(after: emojiEnd)
} else {
break
}
}
return String(string[string.startIndex..<emojiEnd])
}
return nil
}
}

View file

@ -0,0 +1,61 @@
import CoreData
import SwiftUI
import OSLog
struct ExchangeUserInfoButton: View {
var node: NodeInfoEntity
var connectedNode: NodeInfoEntity
@EnvironmentObject var accessoryManager: AccessoryManager
@State private var isPresentingUserInfoSentAlert: Bool = false
@State private var isPresentingUserInfoFailedAlert: Bool = false
var body: some View {
Button {
Task {
if let fromUser = connectedNode.user, let toUser = node.user {
do {
_ = try await accessoryManager.exchangeUserInfo(fromUser: fromUser, toUser: toUser)
Task { @MainActor in
isPresentingUserInfoSentAlert = true
DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
isPresentingUserInfoSentAlert = false
}
}
} catch {
Logger.mesh.warning("Failed to exchange user info")
Task { @MainActor in
isPresentingUserInfoFailedAlert = true
DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
isPresentingUserInfoFailedAlert = false
}
}
}
}
}
} label: {
Label {
Text("Exchange User Info")
} icon: {
Image(systemName: "person.2.badge.gearshape")
.symbolRenderingMode(.hierarchical)
}
}.alert(
"User Info Sent",
isPresented: $isPresentingUserInfoSentAlert
) {
Button("OK") { }.keyboardShortcut(.defaultAction)
} message: {
Text("Your user info has been sent with a request for a response with their user info.")
}.alert(
"User Info Exchange Failed",
isPresented: $isPresentingUserInfoFailedAlert
) {
Button("OK") { }.keyboardShortcut(.defaultAction)
} message: {
Text("Failed to exchange user info.")
}
}
}

View file

@ -29,7 +29,6 @@ struct WaypointForm: View {
@State private var expire: Date = Date.now.addingTimeInterval(60 * 480) // 1 minute * 480 = 8 Hours
@State private var locked: Bool = false
@State private var lockedTo: Int64 = 0
@State private var detents: Set<PresentationDetent> = [.medium, .fraction(0.85)]
@State private var selectedDetent: PresentationDetent = .medium
@State private var waypointFailedAlert: Bool = false
@ -111,26 +110,19 @@ struct WaypointForm: View {
HStack {
Text("Icon")
Spacer()
EmojiOnlyTextField(text: $icon, placeholder: "Select an emoji")
TextField("Select an emoji", text: $icon)
.keyboardType(.emoji)
.font(.title)
.focused($iconIsFocused)
.onChange(of: icon) { _, value in
// If you have anything other than emojis in your string make it empty
if !value.onlyEmojis() {
icon = ""
}
// If a second emoji is entered delete the first one
if value.count >= 1 {
if value.count > 1 {
let index = value.index(value.startIndex, offsetBy: 1)
icon = String(value[index])
}
iconIsFocused = false
}
}
}
Toggle(isOn: $expires) {
Label("Expires", systemImage: "clock.badge.xmark")
@ -458,7 +450,6 @@ struct WaypointForm: View {
longitude = waypoint.coordinate.longitude
}
}
.presentationDetents(detents, selection: $selectedDetent)
.presentationBackgroundInteraction(.enabled(upThrough: .fraction(0.85)))
.presentationDragIndicator(.visible)
}

View file

@ -476,6 +476,10 @@ struct NodeDetail: View {
connectedNode: connectedNode
)
RequestLocalStatsButton(node: node)
ExchangeUserInfoButton(
node: node,
connectedNode: connectedNode
)
TraceRouteButton(
node: node
)

View file

@ -120,16 +120,14 @@ struct MeshMap: View {
}
.sheet(item: $selectedWaypoint) { selection in
WaypointForm(waypoint: selection)
.padding()
.presentationDetents([.large])
}
.sheet(item: $editingWaypoint) { selection in
WaypointForm(waypoint: selection, editMode: true)
.padding()
.presentationDetents([.large])
}
.sheet(isPresented: $editingSettings) {
MapSettingsForm(traffic: $showTraffic, pointsOfInterest: $showPointsOfInterest, mapLayer: $selectedMapLayer, meshMap: $isMeshMap, enabledOverlayConfigs: $enabledOverlayConfigs)
.presentationDetents([.large])
}
.onChange(of: router.navigationState) {
guard case .map = router.navigationState.selectedTab else { return }

View file

@ -253,6 +253,19 @@ fileprivate struct FilteredNodeList: View {
} label: {
Label("Exchange Positions", systemImage: "arrow.triangle.2.circlepath")
}
Button {
Task {
if let fromUser = connectedNode.user, let toUser = node.user {
do {
_ = try await accessoryManager.exchangeUserInfo(fromUser: fromUser, toUser: toUser)
} catch {
Logger.mesh.warning("Failed to exchange user info")
}
}
}
} label: {
Label("Exchange User Info", systemImage: "person.2.badge.gearshape")
}
TraceRouteButton(
node: node
)

View file

@ -120,7 +120,7 @@ struct AppSettings: View {
Text("180")
}
}
Text("Favorited and ignored nodes are always retained. Nodes without PKC keys are cleared from the app database on the schedule set by the user, nodes with PKC keys are cleared only if the interval is set to 7 days or longer. This feature only purges nodes from the app that are not stored in the device node database.")
Text("Favorited and ignored nodes are always retained. Other nodes are cleared from the app database on the schedule set by the user. (Nodes with PKC keys are always retained for at least 7 days.) This feature only purges nodes from the app that are not stored in the device node database.")
.foregroundStyle(.secondary)
.font(idiom == .phone ? .caption : .callout)
}

View file

@ -21,6 +21,55 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP
typealias Version = _2
}
///
/// Firmware update mode for OTA updates
public enum OTAMode: SwiftProtobuf.Enum, Swift.CaseIterable {
public typealias RawValue = Int
///
/// Do not reboot into OTA mode
case noRebootOta // = 0
///
/// Reboot into OTA mode for BLE firmware update
case otaBle // = 1
///
/// Reboot into OTA mode for WiFi firmware update
case otaWifi // = 2
case UNRECOGNIZED(Int)
public init() {
self = .noRebootOta
}
public init?(rawValue: Int) {
switch rawValue {
case 0: self = .noRebootOta
case 1: self = .otaBle
case 2: self = .otaWifi
default: self = .UNRECOGNIZED(rawValue)
}
}
public var rawValue: Int {
switch self {
case .noRebootOta: return 0
case .otaBle: return 1
case .otaWifi: return 2
case .UNRECOGNIZED(let i): return i
}
}
// The compiler won't synthesize support with the UNRECOGNIZED case.
public static let allCases: [OTAMode] = [
.noRebootOta,
.otaBle,
.otaWifi,
]
}
///
/// This message is handled by the Admin module and is responsible for all settings/channel read/write operations.
/// This message is used to do settings operations to both remote AND local nodes.
@ -478,6 +527,16 @@ public struct AdminMessage: Sendable {
set {payloadVariant = .removeIgnoredNode(newValue)}
}
///
/// Set specified node-num to be muted
public var toggleMutedNode: UInt32 {
get {
if case .toggleMutedNode(let v)? = payloadVariant {return v}
return 0
}
set {payloadVariant = .toggleMutedNode(newValue)}
}
///
/// Begins an edit transaction for config, module config, owner, and channel settings changes
/// This will delay the standard *implicit* save to the file system and subsequent reboot behavior until committed (commit_edit_settings)
@ -532,6 +591,9 @@ public struct AdminMessage: Sendable {
///
/// Tell the node to reboot into the OTA Firmware in this many seconds (or <0 to cancel reboot)
/// Only Implemented for ESP32 Devices. This needs to be issued to send a new main firmware via bluetooth.
/// Deprecated in favor of reboot_ota_mode in 2.7.17
///
/// NOTE: This field was marked as deprecated in the .proto file.
public var rebootOtaSeconds: Int32 {
get {
if case .rebootOtaSeconds(let v)? = payloadVariant {return v}
@ -592,6 +654,16 @@ public struct AdminMessage: Sendable {
set {payloadVariant = .nodedbReset(newValue)}
}
///
/// Tell the node to reset into the OTA Loader
public var otaRequest: AdminMessage.OTAEvent {
get {
if case .otaRequest(let v)? = payloadVariant {return v}
return AdminMessage.OTAEvent()
}
set {payloadVariant = .otaRequest(newValue)}
}
public var unknownFields = SwiftProtobuf.UnknownStorage()
///
@ -735,6 +807,9 @@ public struct AdminMessage: Sendable {
/// Set specified node-num to be un-ignored on the NodeDB on the device
case removeIgnoredNode(UInt32)
///
/// Set specified node-num to be muted
case toggleMutedNode(UInt32)
///
/// Begins an edit transaction for config, module config, owner, and channel settings changes
/// This will delay the standard *implicit* save to the file system and subsequent reboot behavior until committed (commit_edit_settings)
case beginEditSettings(Bool)
@ -753,6 +828,9 @@ public struct AdminMessage: Sendable {
///
/// Tell the node to reboot into the OTA Firmware in this many seconds (or <0 to cancel reboot)
/// Only Implemented for ESP32 Devices. This needs to be issued to send a new main firmware via bluetooth.
/// Deprecated in favor of reboot_ota_mode in 2.7.17
///
/// NOTE: This field was marked as deprecated in the .proto file.
case rebootOtaSeconds(Int32)
///
/// This message is only supported for the simulator Portduino build.
@ -771,6 +849,9 @@ public struct AdminMessage: Sendable {
/// Tell the node to reset the nodedb.
/// When true, favorites are preserved through reset.
case nodedbReset(Bool)
///
/// Tell the node to reset into the OTA Loader
case otaRequest(AdminMessage.OTAEvent)
}
@ -1059,6 +1140,28 @@ public struct AdminMessage: Sendable {
public init() {}
}
///
/// User is requesting an over the air update.
/// Node will reboot into the OTA loader
public struct OTAEvent: Sendable {
// SwiftProtobuf.Message conformance is added in an extension below. See the
// `Message` and `Message+*Additions` files in the SwiftProtobuf library for
// methods supported on all messages.
///
/// Tell the node to reboot into OTA mode for firmware update via BLE or WiFi (ESP32 only for now)
public var rebootOtaMode: OTAMode = .noRebootOta
///
/// A 32 byte hash of the OTA firmware.
/// Used to verify the integrity of the firmware before applying an update.
public var otaHash: Data = Data()
public var unknownFields = SwiftProtobuf.UnknownStorage()
public init() {}
}
public init() {}
}
@ -1239,9 +1342,13 @@ public struct KeyVerificationAdmin: Sendable {
fileprivate let _protobuf_package = "meshtastic"
extension OTAMode: SwiftProtobuf._ProtoNameProviding {
public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{2}\0NO_REBOOT_OTA\0\u{1}OTA_BLE\0\u{1}OTA_WIFI\0")
}
extension AdminMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
public static let protoMessageName: String = _protobuf_package + ".AdminMessage"
public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}get_channel_request\0\u{3}get_channel_response\0\u{3}get_owner_request\0\u{3}get_owner_response\0\u{3}get_config_request\0\u{3}get_config_response\0\u{3}get_module_config_request\0\u{3}get_module_config_response\0\u{4}\u{2}get_canned_message_module_messages_request\0\u{3}get_canned_message_module_messages_response\0\u{3}get_device_metadata_request\0\u{3}get_device_metadata_response\0\u{3}get_ringtone_request\0\u{3}get_ringtone_response\0\u{3}get_device_connection_status_request\0\u{3}get_device_connection_status_response\0\u{3}set_ham_mode\0\u{3}get_node_remote_hardware_pins_request\0\u{3}get_node_remote_hardware_pins_response\0\u{3}enter_dfu_mode_request\0\u{3}delete_file_request\0\u{3}set_scale\0\u{3}backup_preferences\0\u{3}restore_preferences\0\u{3}remove_backup_preferences\0\u{3}send_input_event\0\u{4}\u{5}set_owner\0\u{3}set_channel\0\u{3}set_config\0\u{3}set_module_config\0\u{3}set_canned_message_module_messages\0\u{3}set_ringtone_message\0\u{3}remove_by_nodenum\0\u{3}set_favorite_node\0\u{3}remove_favorite_node\0\u{3}set_fixed_position\0\u{3}remove_fixed_position\0\u{3}set_time_only\0\u{3}get_ui_config_request\0\u{3}get_ui_config_response\0\u{3}store_ui_config\0\u{3}set_ignored_node\0\u{3}remove_ignored_node\0\u{4}\u{10}begin_edit_settings\0\u{3}commit_edit_settings\0\u{3}add_contact\0\u{3}key_verification\0\u{4}\u{1b}factory_reset_device\0\u{3}reboot_ota_seconds\0\u{3}exit_simulator\0\u{3}reboot_seconds\0\u{3}shutdown_seconds\0\u{3}factory_reset_config\0\u{3}nodedb_reset\0\u{3}session_passkey\0")
public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}get_channel_request\0\u{3}get_channel_response\0\u{3}get_owner_request\0\u{3}get_owner_response\0\u{3}get_config_request\0\u{3}get_config_response\0\u{3}get_module_config_request\0\u{3}get_module_config_response\0\u{4}\u{2}get_canned_message_module_messages_request\0\u{3}get_canned_message_module_messages_response\0\u{3}get_device_metadata_request\0\u{3}get_device_metadata_response\0\u{3}get_ringtone_request\0\u{3}get_ringtone_response\0\u{3}get_device_connection_status_request\0\u{3}get_device_connection_status_response\0\u{3}set_ham_mode\0\u{3}get_node_remote_hardware_pins_request\0\u{3}get_node_remote_hardware_pins_response\0\u{3}enter_dfu_mode_request\0\u{3}delete_file_request\0\u{3}set_scale\0\u{3}backup_preferences\0\u{3}restore_preferences\0\u{3}remove_backup_preferences\0\u{3}send_input_event\0\u{4}\u{5}set_owner\0\u{3}set_channel\0\u{3}set_config\0\u{3}set_module_config\0\u{3}set_canned_message_module_messages\0\u{3}set_ringtone_message\0\u{3}remove_by_nodenum\0\u{3}set_favorite_node\0\u{3}remove_favorite_node\0\u{3}set_fixed_position\0\u{3}remove_fixed_position\0\u{3}set_time_only\0\u{3}get_ui_config_request\0\u{3}get_ui_config_response\0\u{3}store_ui_config\0\u{3}set_ignored_node\0\u{3}remove_ignored_node\0\u{3}toggle_muted_node\0\u{4}\u{f}begin_edit_settings\0\u{3}commit_edit_settings\0\u{3}add_contact\0\u{3}key_verification\0\u{4}\u{1b}factory_reset_device\0\u{3}reboot_ota_seconds\0\u{3}exit_simulator\0\u{3}reboot_seconds\0\u{3}shutdown_seconds\0\u{3}factory_reset_config\0\u{3}nodedb_reset\0\u{3}session_passkey\0\u{3}ota_request\0")
public mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
while let fieldNumber = try decoder.nextFieldNumber() {
@ -1673,6 +1780,14 @@ extension AdminMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat
self.payloadVariant = .removeIgnoredNode(v)
}
}()
case 49: try {
var v: UInt32?
try decoder.decodeSingularUInt32Field(value: &v)
if let v = v {
if self.payloadVariant != nil {try decoder.handleConflictingOneOf()}
self.payloadVariant = .toggleMutedNode(v)
}
}()
case 64: try {
var v: Bool?
try decoder.decodeSingularBoolField(value: &v)
@ -1772,6 +1887,19 @@ extension AdminMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat
}
}()
case 101: try { try decoder.decodeSingularBytesField(value: &self.sessionPasskey) }()
case 102: try {
var v: AdminMessage.OTAEvent?
var hadOneofValue = false
if let current = self.payloadVariant {
hadOneofValue = true
if case .otaRequest(let m) = current {v = m}
}
try decoder.decodeSingularMessageField(value: &v)
if let v = v {
if hadOneofValue {try decoder.handleConflictingOneOf()}
self.payloadVariant = .otaRequest(v)
}
}()
default: break
}
}
@ -1955,6 +2083,10 @@ extension AdminMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat
guard case .removeIgnoredNode(let v)? = self.payloadVariant else { preconditionFailure() }
try visitor.visitSingularUInt32Field(value: v, fieldNumber: 48)
}()
case .toggleMutedNode?: try {
guard case .toggleMutedNode(let v)? = self.payloadVariant else { preconditionFailure() }
try visitor.visitSingularUInt32Field(value: v, fieldNumber: 49)
}()
case .beginEditSettings?: try {
guard case .beginEditSettings(let v)? = self.payloadVariant else { preconditionFailure() }
try visitor.visitSingularBoolField(value: v, fieldNumber: 64)
@ -1999,11 +2131,14 @@ extension AdminMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat
guard case .nodedbReset(let v)? = self.payloadVariant else { preconditionFailure() }
try visitor.visitSingularBoolField(value: v, fieldNumber: 100)
}()
case nil: break
default: break
}
if !self.sessionPasskey.isEmpty {
try visitor.visitSingularBytesField(value: self.sessionPasskey, fieldNumber: 101)
}
try { if case .otaRequest(let v)? = self.payloadVariant {
try visitor.visitSingularMessageField(value: v, fieldNumber: 102)
} }()
try unknownFields.traverse(visitor: &visitor)
}
@ -2072,6 +2207,41 @@ extension AdminMessage.InputEvent: SwiftProtobuf.Message, SwiftProtobuf._Message
}
}
extension AdminMessage.OTAEvent: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
public static let protoMessageName: String = AdminMessage.protoMessageName + ".OTAEvent"
public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}reboot_ota_mode\0\u{3}ota_hash\0")
public mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
while let fieldNumber = try decoder.nextFieldNumber() {
// The use of inline closures is to circumvent an issue where the compiler
// allocates stack space for every case branch when no optimizations are
// enabled. https://github.com/apple/swift-protobuf/issues/1034
switch fieldNumber {
case 1: try { try decoder.decodeSingularEnumField(value: &self.rebootOtaMode) }()
case 2: try { try decoder.decodeSingularBytesField(value: &self.otaHash) }()
default: break
}
}
}
public func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
if self.rebootOtaMode != .noRebootOta {
try visitor.visitSingularEnumField(value: self.rebootOtaMode, fieldNumber: 1)
}
if !self.otaHash.isEmpty {
try visitor.visitSingularBytesField(value: self.otaHash, fieldNumber: 2)
}
try unknownFields.traverse(visitor: &visitor)
}
public static func ==(lhs: AdminMessage.OTAEvent, rhs: AdminMessage.OTAEvent) -> Bool {
if lhs.rebootOtaMode != rhs.rebootOtaMode {return false}
if lhs.otaHash != rhs.otaHash {return false}
if lhs.unknownFields != rhs.unknownFields {return false}
return true
}
}
extension HamParameters: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
public static let protoMessageName: String = _protobuf_package + ".HamParameters"
public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}call_sign\0\u{3}tx_power\0\u{1}frequency\0\u{3}short_name\0")

View file

@ -281,7 +281,7 @@ public struct Config: Sendable {
case routerLate // = 11
///
/// Description: Treats packets from or to favorited nodes as ROUTER, and all other packets as CLIENT.
/// Description: Treats packets from or to favorited nodes as ROUTER_LATE, and all other packets as CLIENT.
/// Technical Details: Used for stronger attic/roof nodes to distribute messages more widely
/// from weaker, indoor, or less-well-positioned nodes. Recommended for users with multiple nodes
/// where one CLIENT_BASE acts as a more powerful base station, such as an attic/roof node.

View file

@ -230,6 +230,7 @@ public struct NodeInfoLite: @unchecked Sendable {
///
/// Bitfield for storing booleans.
/// LSB 0 is_key_manually_verified
/// LSB 1 is_muted
public var bitfield: UInt32 {
get {return _storage._bitfield}
set {_uniqueStorage()._bitfield = newValue}

View file

@ -166,8 +166,8 @@ public enum HardwareModel: SwiftProtobuf.Enum, Swift.CaseIterable {
case loraRelayV1 // = 32
///
/// TODO: REPLACE
case nrf52840Dk // = 33
/// T-Echo Plus device from LilyGo
case tEchoPlus // = 33
///
/// TODO: REPLACE
@ -535,6 +535,18 @@ public enum HardwareModel: SwiftProtobuf.Enum, Swift.CaseIterable {
/// Elecrow ThinkNode M6
case thinknodeM6 // = 120
///
/// Elecrow Meshstick 1262
case meshstick1262 // = 121
///
/// LilyGo T-Beam 1W
case tbeam1Watt // = 122
///
/// LilyGo T5 S3 ePaper Pro (V1 and V2)
case t5S3EpaperPro // = 123
///
/// ------------------------------------------------------------------------------------------------------------------------------------------
/// Reserved ID For developing private Ports. These will show up in live traffic sparsely, so we can use a high number. Keep it within 8 bits.
@ -581,7 +593,7 @@ public enum HardwareModel: SwiftProtobuf.Enum, Swift.CaseIterable {
case 30: self = .rp2040Lora
case 31: self = .stationG2
case 32: self = .loraRelayV1
case 33: self = .nrf52840Dk
case 33: self = .tEchoPlus
case 34: self = .ppr
case 35: self = .genieblocks
case 36: self = .nrf52Unknown
@ -669,6 +681,9 @@ public enum HardwareModel: SwiftProtobuf.Enum, Swift.CaseIterable {
case 118: self = .rak6421
case 119: self = .thinknodeM4
case 120: self = .thinknodeM6
case 121: self = .meshstick1262
case 122: self = .tbeam1Watt
case 123: self = .t5S3EpaperPro
case 255: self = .privateHw
default: self = .UNRECOGNIZED(rawValue)
}
@ -709,7 +724,7 @@ public enum HardwareModel: SwiftProtobuf.Enum, Swift.CaseIterable {
case .rp2040Lora: return 30
case .stationG2: return 31
case .loraRelayV1: return 32
case .nrf52840Dk: return 33
case .tEchoPlus: return 33
case .ppr: return 34
case .genieblocks: return 35
case .nrf52Unknown: return 36
@ -797,6 +812,9 @@ public enum HardwareModel: SwiftProtobuf.Enum, Swift.CaseIterable {
case .rak6421: return 118
case .thinknodeM4: return 119
case .thinknodeM6: return 120
case .meshstick1262: return 121
case .tbeam1Watt: return 122
case .t5S3EpaperPro: return 123
case .privateHw: return 255
case .UNRECOGNIZED(let i): return i
}
@ -837,7 +855,7 @@ public enum HardwareModel: SwiftProtobuf.Enum, Swift.CaseIterable {
.rp2040Lora,
.stationG2,
.loraRelayV1,
.nrf52840Dk,
.tEchoPlus,
.ppr,
.genieblocks,
.nrf52Unknown,
@ -925,6 +943,9 @@ public enum HardwareModel: SwiftProtobuf.Enum, Swift.CaseIterable {
.rak6421,
.thinknodeM4,
.thinknodeM6,
.meshstick1262,
.tbeam1Watt,
.t5S3EpaperPro,
.privateHw,
]
@ -1921,6 +1942,11 @@ public struct Routing: Sendable {
/// Airtime fairness rate limit exceeded for a packet
/// This typically enforced per portnum and is used to prevent a single node from monopolizing airtime
case rateLimitExceeded // = 38
///
/// PKI encryption failed, due to no public key for the remote node
/// This is different from PKI_UNKNOWN_PUBKEY which indicates a failure upon receiving a packet
case pkiSendFailPublicKey // = 39
case UNRECOGNIZED(Int)
public init() {
@ -1946,6 +1972,7 @@ public struct Routing: Sendable {
case 36: self = .adminBadSessionKey
case 37: self = .adminPublicKeyUnauthorized
case 38: self = .rateLimitExceeded
case 39: self = .pkiSendFailPublicKey
default: self = .UNRECOGNIZED(rawValue)
}
}
@ -1969,6 +1996,7 @@ public struct Routing: Sendable {
case .adminBadSessionKey: return 36
case .adminPublicKeyUnauthorized: return 37
case .rateLimitExceeded: return 38
case .pkiSendFailPublicKey: return 39
case .UNRECOGNIZED(let i): return i
}
}
@ -1992,6 +2020,7 @@ public struct Routing: Sendable {
.adminBadSessionKey,
.adminPublicKeyUnauthorized,
.rateLimitExceeded,
.pkiSendFailPublicKey,
]
}
@ -2136,6 +2165,10 @@ public struct StoreForwardPlusPlus: Sendable {
/// The receive time of the message in question
public var encapsulatedRxtime: UInt32 = 0
///
/// Used in a LINK_REQUEST to specify the message X spots back from head
public var chainCount: UInt32 = 0
public var unknownFields = SwiftProtobuf.UnknownStorage()
///
@ -2936,6 +2969,14 @@ public struct NodeInfo: @unchecked Sendable {
set {_uniqueStorage()._isKeyManuallyVerified = newValue}
}
///
/// True if node has been muted
/// Persistes between NodeDB internal clean ups
public var isMuted: Bool {
get {return _storage._isMuted}
set {_uniqueStorage()._isMuted = newValue}
}
public var unknownFields = SwiftProtobuf.UnknownStorage()
public init() {}
@ -3959,7 +4000,7 @@ public struct ChunkedPayloadResponse: Sendable {
fileprivate let _protobuf_package = "meshtastic"
extension HardwareModel: SwiftProtobuf._ProtoNameProviding {
public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{2}\0UNSET\0\u{1}TLORA_V2\0\u{1}TLORA_V1\0\u{1}TLORA_V2_1_1P6\0\u{1}TBEAM\0\u{1}HELTEC_V2_0\0\u{1}TBEAM_V0P7\0\u{1}T_ECHO\0\u{1}TLORA_V1_1P3\0\u{1}RAK4631\0\u{1}HELTEC_V2_1\0\u{1}HELTEC_V1\0\u{1}LILYGO_TBEAM_S3_CORE\0\u{1}RAK11200\0\u{1}NANO_G1\0\u{1}TLORA_V2_1_1P8\0\u{1}TLORA_T3_S3\0\u{1}NANO_G1_EXPLORER\0\u{1}NANO_G2_ULTRA\0\u{1}LORA_TYPE\0\u{1}WIPHONE\0\u{1}WIO_WM1110\0\u{1}RAK2560\0\u{1}HELTEC_HRU_3601\0\u{1}HELTEC_WIRELESS_BRIDGE\0\u{1}STATION_G1\0\u{1}RAK11310\0\u{1}SENSELORA_RP2040\0\u{1}SENSELORA_S3\0\u{1}CANARYONE\0\u{1}RP2040_LORA\0\u{1}STATION_G2\0\u{1}LORA_RELAY_V1\0\u{1}NRF52840DK\0\u{1}PPR\0\u{1}GENIEBLOCKS\0\u{1}NRF52_UNKNOWN\0\u{1}PORTDUINO\0\u{1}ANDROID_SIM\0\u{1}DIY_V1\0\u{1}NRF52840_PCA10059\0\u{1}DR_DEV\0\u{1}M5STACK\0\u{1}HELTEC_V3\0\u{1}HELTEC_WSL_V3\0\u{1}BETAFPV_2400_TX\0\u{1}BETAFPV_900_NANO_TX\0\u{1}RPI_PICO\0\u{1}HELTEC_WIRELESS_TRACKER\0\u{1}HELTEC_WIRELESS_PAPER\0\u{1}T_DECK\0\u{1}T_WATCH_S3\0\u{1}PICOMPUTER_S3\0\u{1}HELTEC_HT62\0\u{1}EBYTE_ESP32_S3\0\u{1}ESP32_S3_PICO\0\u{1}CHATTER_2\0\u{1}HELTEC_WIRELESS_PAPER_V1_0\0\u{1}HELTEC_WIRELESS_TRACKER_V1_0\0\u{1}UNPHONE\0\u{1}TD_LORAC\0\u{1}CDEBYTE_EORA_S3\0\u{1}TWC_MESH_V4\0\u{1}NRF52_PROMICRO_DIY\0\u{1}RADIOMASTER_900_BANDIT_NANO\0\u{1}HELTEC_CAPSULE_SENSOR_V3\0\u{1}HELTEC_VISION_MASTER_T190\0\u{1}HELTEC_VISION_MASTER_E213\0\u{1}HELTEC_VISION_MASTER_E290\0\u{1}HELTEC_MESH_NODE_T114\0\u{1}SENSECAP_INDICATOR\0\u{1}TRACKER_T1000_E\0\u{1}RAK3172\0\u{1}WIO_E5\0\u{1}RADIOMASTER_900_BANDIT\0\u{1}ME25LS01_4Y10TD\0\u{1}RP2040_FEATHER_RFM95\0\u{1}M5STACK_COREBASIC\0\u{1}M5STACK_CORE2\0\u{1}RPI_PICO2\0\u{1}M5STACK_CORES3\0\u{1}SEEED_XIAO_S3\0\u{1}MS24SF1\0\u{1}TLORA_C6\0\u{1}WISMESH_TAP\0\u{1}ROUTASTIC\0\u{1}MESH_TAB\0\u{1}MESHLINK\0\u{1}XIAO_NRF52_KIT\0\u{1}THINKNODE_M1\0\u{1}THINKNODE_M2\0\u{1}T_ETH_ELITE\0\u{1}HELTEC_SENSOR_HUB\0\u{1}MUZI_BASE\0\u{1}HELTEC_MESH_POCKET\0\u{1}SEEED_SOLAR_NODE\0\u{1}NOMADSTAR_METEOR_PRO\0\u{1}CROWPANEL\0\u{1}LINK_32\0\u{1}SEEED_WIO_TRACKER_L1\0\u{1}SEEED_WIO_TRACKER_L1_EINK\0\u{1}MUZI_R1_NEO\0\u{1}T_DECK_PRO\0\u{1}T_LORA_PAGER\0\u{1}M5STACK_RESERVED\0\u{1}WISMESH_TAG\0\u{1}RAK3312\0\u{1}THINKNODE_M5\0\u{1}HELTEC_MESH_SOLAR\0\u{1}T_ECHO_LITE\0\u{1}HELTEC_V4\0\u{1}M5STACK_C6L\0\u{1}M5STACK_CARDPUTER_ADV\0\u{1}HELTEC_WIRELESS_TRACKER_V2\0\u{1}T_WATCH_ULTRA\0\u{1}THINKNODE_M3\0\u{1}WISMESH_TAP_V2\0\u{1}RAK3401\0\u{1}RAK6421\0\u{1}THINKNODE_M4\0\u{1}THINKNODE_M6\0\u{2}G\u{2}PRIVATE_HW\0")
public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{2}\0UNSET\0\u{1}TLORA_V2\0\u{1}TLORA_V1\0\u{1}TLORA_V2_1_1P6\0\u{1}TBEAM\0\u{1}HELTEC_V2_0\0\u{1}TBEAM_V0P7\0\u{1}T_ECHO\0\u{1}TLORA_V1_1P3\0\u{1}RAK4631\0\u{1}HELTEC_V2_1\0\u{1}HELTEC_V1\0\u{1}LILYGO_TBEAM_S3_CORE\0\u{1}RAK11200\0\u{1}NANO_G1\0\u{1}TLORA_V2_1_1P8\0\u{1}TLORA_T3_S3\0\u{1}NANO_G1_EXPLORER\0\u{1}NANO_G2_ULTRA\0\u{1}LORA_TYPE\0\u{1}WIPHONE\0\u{1}WIO_WM1110\0\u{1}RAK2560\0\u{1}HELTEC_HRU_3601\0\u{1}HELTEC_WIRELESS_BRIDGE\0\u{1}STATION_G1\0\u{1}RAK11310\0\u{1}SENSELORA_RP2040\0\u{1}SENSELORA_S3\0\u{1}CANARYONE\0\u{1}RP2040_LORA\0\u{1}STATION_G2\0\u{1}LORA_RELAY_V1\0\u{1}T_ECHO_PLUS\0\u{1}PPR\0\u{1}GENIEBLOCKS\0\u{1}NRF52_UNKNOWN\0\u{1}PORTDUINO\0\u{1}ANDROID_SIM\0\u{1}DIY_V1\0\u{1}NRF52840_PCA10059\0\u{1}DR_DEV\0\u{1}M5STACK\0\u{1}HELTEC_V3\0\u{1}HELTEC_WSL_V3\0\u{1}BETAFPV_2400_TX\0\u{1}BETAFPV_900_NANO_TX\0\u{1}RPI_PICO\0\u{1}HELTEC_WIRELESS_TRACKER\0\u{1}HELTEC_WIRELESS_PAPER\0\u{1}T_DECK\0\u{1}T_WATCH_S3\0\u{1}PICOMPUTER_S3\0\u{1}HELTEC_HT62\0\u{1}EBYTE_ESP32_S3\0\u{1}ESP32_S3_PICO\0\u{1}CHATTER_2\0\u{1}HELTEC_WIRELESS_PAPER_V1_0\0\u{1}HELTEC_WIRELESS_TRACKER_V1_0\0\u{1}UNPHONE\0\u{1}TD_LORAC\0\u{1}CDEBYTE_EORA_S3\0\u{1}TWC_MESH_V4\0\u{1}NRF52_PROMICRO_DIY\0\u{1}RADIOMASTER_900_BANDIT_NANO\0\u{1}HELTEC_CAPSULE_SENSOR_V3\0\u{1}HELTEC_VISION_MASTER_T190\0\u{1}HELTEC_VISION_MASTER_E213\0\u{1}HELTEC_VISION_MASTER_E290\0\u{1}HELTEC_MESH_NODE_T114\0\u{1}SENSECAP_INDICATOR\0\u{1}TRACKER_T1000_E\0\u{1}RAK3172\0\u{1}WIO_E5\0\u{1}RADIOMASTER_900_BANDIT\0\u{1}ME25LS01_4Y10TD\0\u{1}RP2040_FEATHER_RFM95\0\u{1}M5STACK_COREBASIC\0\u{1}M5STACK_CORE2\0\u{1}RPI_PICO2\0\u{1}M5STACK_CORES3\0\u{1}SEEED_XIAO_S3\0\u{1}MS24SF1\0\u{1}TLORA_C6\0\u{1}WISMESH_TAP\0\u{1}ROUTASTIC\0\u{1}MESH_TAB\0\u{1}MESHLINK\0\u{1}XIAO_NRF52_KIT\0\u{1}THINKNODE_M1\0\u{1}THINKNODE_M2\0\u{1}T_ETH_ELITE\0\u{1}HELTEC_SENSOR_HUB\0\u{1}MUZI_BASE\0\u{1}HELTEC_MESH_POCKET\0\u{1}SEEED_SOLAR_NODE\0\u{1}NOMADSTAR_METEOR_PRO\0\u{1}CROWPANEL\0\u{1}LINK_32\0\u{1}SEEED_WIO_TRACKER_L1\0\u{1}SEEED_WIO_TRACKER_L1_EINK\0\u{1}MUZI_R1_NEO\0\u{1}T_DECK_PRO\0\u{1}T_LORA_PAGER\0\u{1}M5STACK_RESERVED\0\u{1}WISMESH_TAG\0\u{1}RAK3312\0\u{1}THINKNODE_M5\0\u{1}HELTEC_MESH_SOLAR\0\u{1}T_ECHO_LITE\0\u{1}HELTEC_V4\0\u{1}M5STACK_C6L\0\u{1}M5STACK_CARDPUTER_ADV\0\u{1}HELTEC_WIRELESS_TRACKER_V2\0\u{1}T_WATCH_ULTRA\0\u{1}THINKNODE_M3\0\u{1}WISMESH_TAP_V2\0\u{1}RAK3401\0\u{1}RAK6421\0\u{1}THINKNODE_M4\0\u{1}THINKNODE_M6\0\u{1}MESHSTICK_1262\0\u{1}TBEAM_1_WATT\0\u{1}T5_S3_EPAPER_PRO\0\u{2}D\u{2}PRIVATE_HW\0")
}
extension Constants: SwiftProtobuf._ProtoNameProviding {
@ -4409,7 +4450,7 @@ extension Routing: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBa
}
extension Routing.Error: SwiftProtobuf._ProtoNameProviding {
public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{2}\0NONE\0\u{1}NO_ROUTE\0\u{1}GOT_NAK\0\u{1}TIMEOUT\0\u{1}NO_INTERFACE\0\u{1}MAX_RETRANSMIT\0\u{1}NO_CHANNEL\0\u{1}TOO_LARGE\0\u{1}NO_RESPONSE\0\u{1}DUTY_CYCLE_LIMIT\0\u{2}\u{17}BAD_REQUEST\0\u{1}NOT_AUTHORIZED\0\u{1}PKI_FAILED\0\u{1}PKI_UNKNOWN_PUBKEY\0\u{1}ADMIN_BAD_SESSION_KEY\0\u{1}ADMIN_PUBLIC_KEY_UNAUTHORIZED\0\u{1}RATE_LIMIT_EXCEEDED\0")
public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{2}\0NONE\0\u{1}NO_ROUTE\0\u{1}GOT_NAK\0\u{1}TIMEOUT\0\u{1}NO_INTERFACE\0\u{1}MAX_RETRANSMIT\0\u{1}NO_CHANNEL\0\u{1}TOO_LARGE\0\u{1}NO_RESPONSE\0\u{1}DUTY_CYCLE_LIMIT\0\u{2}\u{17}BAD_REQUEST\0\u{1}NOT_AUTHORIZED\0\u{1}PKI_FAILED\0\u{1}PKI_UNKNOWN_PUBKEY\0\u{1}ADMIN_BAD_SESSION_KEY\0\u{1}ADMIN_PUBLIC_KEY_UNAUTHORIZED\0\u{1}RATE_LIMIT_EXCEEDED\0\u{1}PKI_SEND_FAIL_PUBLIC_KEY\0")
}
extension DataMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
@ -4528,7 +4569,7 @@ extension KeyVerification: SwiftProtobuf.Message, SwiftProtobuf._MessageImplemen
extension StoreForwardPlusPlus: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
public static let protoMessageName: String = _protobuf_package + ".StoreForwardPlusPlus"
public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}sfpp_message_type\0\u{3}message_hash\0\u{3}commit_hash\0\u{3}root_hash\0\u{1}message\0\u{3}encapsulated_id\0\u{3}encapsulated_to\0\u{3}encapsulated_from\0\u{3}encapsulated_rxtime\0")
public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}sfpp_message_type\0\u{3}message_hash\0\u{3}commit_hash\0\u{3}root_hash\0\u{1}message\0\u{3}encapsulated_id\0\u{3}encapsulated_to\0\u{3}encapsulated_from\0\u{3}encapsulated_rxtime\0\u{3}chain_count\0")
public mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
while let fieldNumber = try decoder.nextFieldNumber() {
@ -4545,6 +4586,7 @@ extension StoreForwardPlusPlus: SwiftProtobuf.Message, SwiftProtobuf._MessageImp
case 7: try { try decoder.decodeSingularUInt32Field(value: &self.encapsulatedTo) }()
case 8: try { try decoder.decodeSingularUInt32Field(value: &self.encapsulatedFrom) }()
case 9: try { try decoder.decodeSingularUInt32Field(value: &self.encapsulatedRxtime) }()
case 10: try { try decoder.decodeSingularUInt32Field(value: &self.chainCount) }()
default: break
}
}
@ -4578,6 +4620,9 @@ extension StoreForwardPlusPlus: SwiftProtobuf.Message, SwiftProtobuf._MessageImp
if self.encapsulatedRxtime != 0 {
try visitor.visitSingularUInt32Field(value: self.encapsulatedRxtime, fieldNumber: 9)
}
if self.chainCount != 0 {
try visitor.visitSingularUInt32Field(value: self.chainCount, fieldNumber: 10)
}
try unknownFields.traverse(visitor: &visitor)
}
@ -4591,6 +4636,7 @@ extension StoreForwardPlusPlus: SwiftProtobuf.Message, SwiftProtobuf._MessageImp
if lhs.encapsulatedTo != rhs.encapsulatedTo {return false}
if lhs.encapsulatedFrom != rhs.encapsulatedFrom {return false}
if lhs.encapsulatedRxtime != rhs.encapsulatedRxtime {return false}
if lhs.chainCount != rhs.chainCount {return false}
if lhs.unknownFields != rhs.unknownFields {return false}
return true
}
@ -4981,7 +5027,7 @@ extension MeshPacket.TransportMechanism: SwiftProtobuf._ProtoNameProviding {
extension NodeInfo: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
public static let protoMessageName: String = _protobuf_package + ".NodeInfo"
public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}num\0\u{1}user\0\u{1}position\0\u{1}snr\0\u{3}last_heard\0\u{3}device_metrics\0\u{1}channel\0\u{3}via_mqtt\0\u{3}hops_away\0\u{3}is_favorite\0\u{3}is_ignored\0\u{3}is_key_manually_verified\0")
public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}num\0\u{1}user\0\u{1}position\0\u{1}snr\0\u{3}last_heard\0\u{3}device_metrics\0\u{1}channel\0\u{3}via_mqtt\0\u{3}hops_away\0\u{3}is_favorite\0\u{3}is_ignored\0\u{3}is_key_manually_verified\0\u{3}is_muted\0")
fileprivate class _StorageClass {
var _num: UInt32 = 0
@ -4996,6 +5042,7 @@ extension NodeInfo: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB
var _isFavorite: Bool = false
var _isIgnored: Bool = false
var _isKeyManuallyVerified: Bool = false
var _isMuted: Bool = false
// This property is used as the initial default value for new instances of the type.
// The type itself is protecting the reference to its storage via CoW semantics.
@ -5018,6 +5065,7 @@ extension NodeInfo: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB
_isFavorite = source._isFavorite
_isIgnored = source._isIgnored
_isKeyManuallyVerified = source._isKeyManuallyVerified
_isMuted = source._isMuted
}
}
@ -5048,6 +5096,7 @@ extension NodeInfo: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB
case 10: try { try decoder.decodeSingularBoolField(value: &_storage._isFavorite) }()
case 11: try { try decoder.decodeSingularBoolField(value: &_storage._isIgnored) }()
case 12: try { try decoder.decodeSingularBoolField(value: &_storage._isKeyManuallyVerified) }()
case 13: try { try decoder.decodeSingularBoolField(value: &_storage._isMuted) }()
default: break
}
}
@ -5096,6 +5145,9 @@ extension NodeInfo: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB
if _storage._isKeyManuallyVerified != false {
try visitor.visitSingularBoolField(value: _storage._isKeyManuallyVerified, fieldNumber: 12)
}
if _storage._isMuted != false {
try visitor.visitSingularBoolField(value: _storage._isMuted, fieldNumber: 13)
}
}
try unknownFields.traverse(visitor: &visitor)
}
@ -5117,6 +5169,7 @@ extension NodeInfo: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB
if _storage._isFavorite != rhs_storage._isFavorite {return false}
if _storage._isIgnored != rhs_storage._isIgnored {return false}
if _storage._isKeyManuallyVerified != rhs_storage._isKeyManuallyVerified {return false}
if _storage._isMuted != rhs_storage._isMuted {return false}
return true
}
if !storagesAreEqual {return false}

View file

@ -805,9 +805,15 @@ public struct ModuleConfig: Sendable {
/// https://beta.ivc.no/wiki/index.php/Victron_VE_Direct_DIY_Cable
case veDirect // = 7
///Used to configure and view some parameters of MeshSolar.
///https://heltec.org/project/meshsolar/
/// Used to configure and view some parameters of MeshSolar.
/// https://heltec.org/project/meshsolar/
case msConfig // = 8
/// Logs mesh traffic to the serial pins, ideal for logging via openLog or similar.
case log // = 9
/// only text (channel & DM)
case logtext // = 10
case UNRECOGNIZED(Int)
public init() {
@ -825,6 +831,8 @@ public struct ModuleConfig: Sendable {
case 6: self = .ws85
case 7: self = .veDirect
case 8: self = .msConfig
case 9: self = .log
case 10: self = .logtext
default: self = .UNRECOGNIZED(rawValue)
}
}
@ -840,6 +848,8 @@ public struct ModuleConfig: Sendable {
case .ws85: return 6
case .veDirect: return 7
case .msConfig: return 8
case .log: return 9
case .logtext: return 10
case .UNRECOGNIZED(let i): return i
}
}
@ -855,6 +865,8 @@ public struct ModuleConfig: Sendable {
.ws85,
.veDirect,
.msConfig,
.log,
.logtext,
]
}
@ -1080,6 +1092,10 @@ public struct ModuleConfig: Sendable {
/// Note: We will still send telemtry to the connected phone / client every minute over the API
public var deviceTelemetryEnabled: Bool = false
///
/// Enable/Disable the air quality telemetry measurement module on-device display
public var airQualityScreenEnabled: Bool = false
public var unknownFields = SwiftProtobuf.UnknownStorage()
public init() {}
@ -2005,7 +2021,7 @@ extension ModuleConfig.SerialConfig.Serial_Baud: SwiftProtobuf._ProtoNameProvidi
}
extension ModuleConfig.SerialConfig.Serial_Mode: SwiftProtobuf._ProtoNameProviding {
public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{2}\0DEFAULT\0\u{1}SIMPLE\0\u{1}PROTO\0\u{1}TEXTMSG\0\u{1}NMEA\0\u{1}CALTOPO\0\u{1}WS85\0\u{1}VE_DIRECT\0\u{1}MS_CONFIG\0")
public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{2}\0DEFAULT\0\u{1}SIMPLE\0\u{1}PROTO\0\u{1}TEXTMSG\0\u{1}NMEA\0\u{1}CALTOPO\0\u{1}WS85\0\u{1}VE_DIRECT\0\u{1}MS_CONFIG\0\u{1}LOG\0\u{1}LOGTEXT\0")
}
extension ModuleConfig.ExternalNotificationConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
@ -2210,7 +2226,7 @@ extension ModuleConfig.RangeTestConfig: SwiftProtobuf.Message, SwiftProtobuf._Me
extension ModuleConfig.TelemetryConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
public static let protoMessageName: String = ModuleConfig.protoMessageName + ".TelemetryConfig"
public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}device_update_interval\0\u{3}environment_update_interval\0\u{3}environment_measurement_enabled\0\u{3}environment_screen_enabled\0\u{3}environment_display_fahrenheit\0\u{3}air_quality_enabled\0\u{3}air_quality_interval\0\u{3}power_measurement_enabled\0\u{3}power_update_interval\0\u{3}power_screen_enabled\0\u{3}health_measurement_enabled\0\u{3}health_update_interval\0\u{3}health_screen_enabled\0\u{3}device_telemetry_enabled\0")
public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}device_update_interval\0\u{3}environment_update_interval\0\u{3}environment_measurement_enabled\0\u{3}environment_screen_enabled\0\u{3}environment_display_fahrenheit\0\u{3}air_quality_enabled\0\u{3}air_quality_interval\0\u{3}power_measurement_enabled\0\u{3}power_update_interval\0\u{3}power_screen_enabled\0\u{3}health_measurement_enabled\0\u{3}health_update_interval\0\u{3}health_screen_enabled\0\u{3}device_telemetry_enabled\0\u{3}air_quality_screen_enabled\0")
public mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
while let fieldNumber = try decoder.nextFieldNumber() {
@ -2232,6 +2248,7 @@ extension ModuleConfig.TelemetryConfig: SwiftProtobuf.Message, SwiftProtobuf._Me
case 12: try { try decoder.decodeSingularUInt32Field(value: &self.healthUpdateInterval) }()
case 13: try { try decoder.decodeSingularBoolField(value: &self.healthScreenEnabled) }()
case 14: try { try decoder.decodeSingularBoolField(value: &self.deviceTelemetryEnabled) }()
case 15: try { try decoder.decodeSingularBoolField(value: &self.airQualityScreenEnabled) }()
default: break
}
}
@ -2280,6 +2297,9 @@ extension ModuleConfig.TelemetryConfig: SwiftProtobuf.Message, SwiftProtobuf._Me
if self.deviceTelemetryEnabled != false {
try visitor.visitSingularBoolField(value: self.deviceTelemetryEnabled, fieldNumber: 14)
}
if self.airQualityScreenEnabled != false {
try visitor.visitSingularBoolField(value: self.airQualityScreenEnabled, fieldNumber: 15)
}
try unknownFields.traverse(visitor: &visitor)
}
@ -2298,6 +2318,7 @@ extension ModuleConfig.TelemetryConfig: SwiftProtobuf.Message, SwiftProtobuf._Me
if lhs.healthUpdateInterval != rhs.healthUpdateInterval {return false}
if lhs.healthScreenEnabled != rhs.healthScreenEnabled {return false}
if lhs.deviceTelemetryEnabled != rhs.deviceTelemetryEnabled {return false}
if lhs.airQualityScreenEnabled != rhs.airQualityScreenEnabled {return false}
if lhs.unknownFields != rhs.unknownFields {return false}
return true
}