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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ public actor BinaryProtocolService {
// MARK: - Status Request

/// Request status from a remote node (blocking, waits for response)
/// - Parameter publicKey: The remote node's public key (full or prefix)
/// - Parameter publicKey: The remote node's full 32-byte public key
/// - Returns: StatusResponse with device stats
public func requestStatus(from publicKey: Data) async throws -> StatusResponse {
do {
Expand All @@ -129,6 +129,22 @@ public actor BinaryProtocolService {
}
}

/// Request status from a remote node (blocking, waits for response)
/// - Parameters:
/// - publicKey: The remote node's full 32-byte public key
/// - type: The target node type used to select the correct firmware status layout
/// - Returns: StatusResponse with device stats
public func requestStatus(
from publicKey: Data,
type: ContactType
) async throws -> StatusResponse {
do {
return try await session.requestStatus(from: publicKey, type: type)
} catch let error as MeshCoreError {
throw BinaryProtocolError.sessionError(error)
}
}

// MARK: - Telemetry Request

/// Request telemetry from a remote node (blocking, waits for response)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -727,7 +727,8 @@ public actor RemoteNodeService {

// Request status (which triggers sync)
do {
_ = try await session.requestStatus(from: remoteSession.publicKey)
let contactType: ContactType = remoteSession.isRoom ? .room : .repeater
_ = try await session.requestStatus(from: remoteSession.publicKey, type: contactType)
} catch let error as MeshCoreError {
throw RemoteNodeError.sessionError(error)
}
Expand Down Expand Up @@ -781,7 +782,8 @@ public actor RemoteNodeService {
do {
let effectiveTimeout = timeout ?? RemoteOperationTimeoutPolicy.binaryMaximum
return try await withTimeout(effectiveTimeout, operationName: "remoteStatus") {
try await self.session.requestStatus(from: remoteSession.publicKey)
let contactType: ContactType = remoteSession.isRoom ? .room : .repeater
return try await self.session.requestStatus(from: remoteSession.publicKey, type: contactType)
}
} catch is TimeoutError {
throw RemoteNodeError.timeout
Expand Down
3 changes: 3 additions & 0 deletions MeshCore/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,9 @@ for await state in await session.connectionState {
let status = try await session.requestStatus(from: publicKey)
print("Remote battery: \(status.battery) mV, uptime: \(status.uptime)s")

// For room servers, use a typed request so the correct status layout is decoded.
let roomStatus = try await session.requestStatus(from: roomContact)

// Request telemetry
let telemetry = try await session.requestTelemetry(from: publicKey)

Expand Down
22 changes: 21 additions & 1 deletion MeshCore/Sources/MeshCore/Events/MeshEvent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -508,6 +508,16 @@ public struct ChannelInfo: Sendable, Equatable {
/// - Push notification responses: offset=8, pubkey_prefix at bytes 2-8, fields follow
/// The parser must handle both cases based on whether this is a solicited vs unsolicited response
public struct StatusResponse: Sendable, Equatable {
/// Describes which firmware status layout was used to decode the payload.
public enum Layout: Sendable, Equatable {
/// Standard repeater / legacy status layout.
case repeater
/// Room server layout used by room-server firmware.
case roomServer
}

/// The decoded status layout.
public let layout: Layout
/// The public key prefix of the responding node.
public let publicKeyPrefix: Data
/// The battery level in millivolts.
Expand Down Expand Up @@ -546,9 +556,14 @@ public struct StatusResponse: Sendable, Equatable {
public let rxAirtime: UInt32
/// Total receive errors (v1.12+, 0 for older firmware).
public let receiveErrors: UInt32
/// Total messages posted to the room server.
public let roomServerPostedCount: UInt16?
/// Total room-server post push attempts.
public let roomServerPostPushCount: UInt16?

/// Initializes a new status response object.
public init(
layout: Layout = .repeater,
publicKeyPrefix: Data,
battery: Int,
txQueueLength: Int,
Expand All @@ -567,8 +582,11 @@ public struct StatusResponse: Sendable, Equatable {
directDuplicates: Int,
floodDuplicates: Int,
rxAirtime: UInt32,
receiveErrors: UInt32 = 0
receiveErrors: UInt32 = 0,
roomServerPostedCount: UInt16? = nil,
roomServerPostPushCount: UInt16? = nil
) {
self.layout = layout
self.publicKeyPrefix = publicKeyPrefix
self.battery = battery
self.txQueueLength = txQueueLength
Expand All @@ -588,6 +606,8 @@ public struct StatusResponse: Sendable, Equatable {
self.floodDuplicates = floodDuplicates
self.rxAirtime = rxAirtime
self.receiveErrors = receiveErrors
self.roomServerPostedCount = roomServerPostedCount
self.roomServerPostPushCount = roomServerPostPushCount
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ The binary protocol provides efficient data transfer for complex requests like s
Query a remote node's status:

```swift
let status = try await session.requestStatus(from: contact.publicKey)
let status = try await session.requestStatus(from: contact)

print("Battery: \(status.battery)mV")
print("Uptime: \(status.uptime)s")
Expand Down Expand Up @@ -124,3 +124,6 @@ do {
print("Unexpected response: expected \(expected), got \(got)")
}
```

When querying a room server, use ``requestStatus(from: MeshContact)`` or
``requestStatus(from:type:)`` so the session can select the room-server status layout.
93 changes: 66 additions & 27 deletions MeshCore/Sources/MeshCore/Protocol/Parsers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -712,7 +712,11 @@ public enum Parsers {
/// - data: Raw binary response payload (without the 4-byte tag).
/// - publicKeyPrefix: The 6-byte public key prefix from the pending request context.
/// - Returns: A `StatusResponse` if parsing succeeds, `nil` otherwise.
static func parseFromBinaryResponse(_ data: Data, publicKeyPrefix: Data) -> MeshCore.StatusResponse? {
static func parseFromBinaryResponse(
_ data: Data,
publicKeyPrefix: Data,
layout: MeshCore.StatusResponse.Layout = .repeater
) -> MeshCore.StatusResponse? {
// Accept exactly 48 (no rxAirtime), 52 (with rxAirtime), or 56+ (with receiveErrors).
// Reject malformed payloads with incomplete fields (49-51, 53-55).
guard data.count == PacketSize.binaryResponseStatusBase ||
Expand All @@ -736,33 +740,68 @@ public enum Parsers {
let lastSNR = Double(data.readInt16LE(at: offset)) / 4.0; offset += 2
let directDups = Int(data.readUInt16LE(at: offset)); offset += 2
let floodDups = Int(data.readUInt16LE(at: offset)); offset += 2
let rxAirtime: UInt32 = data.count >= PacketSize.binaryResponseStatusWithRxAirtime
? data.readUInt32LE(at: offset) : 0
offset += 4
let receiveErrors: UInt32 = data.count >= PacketSize.binaryResponseStatusWithReceiveErrors
? data.readUInt32LE(at: offset) : 0
switch layout {
case .repeater:
let rxAirtime: UInt32 = data.count >= PacketSize.binaryResponseStatusWithRxAirtime
? data.readUInt32LE(at: offset) : 0
offset += 4
let receiveErrors: UInt32 = data.count >= PacketSize.binaryResponseStatusWithReceiveErrors
? data.readUInt32LE(at: offset) : 0

return MeshCore.StatusResponse(
layout: .repeater,
publicKeyPrefix: publicKeyPrefix,
battery: battery,
txQueueLength: txQueueLen,
noiseFloor: noiseFloor,
lastRSSI: lastRSSI,
packetsReceived: packetsRecv,
packetsSent: packetsSent,
airtime: airtime,
uptime: uptime,
sentFlood: sentFlood,
sentDirect: sentDirect,
receivedFlood: recvFlood,
receivedDirect: recvDirect,
fullEvents: fullEvents,
lastSNR: lastSNR,
directDuplicates: directDups,
floodDuplicates: floodDups,
rxAirtime: rxAirtime,
receiveErrors: receiveErrors
)

return MeshCore.StatusResponse(
publicKeyPrefix: publicKeyPrefix,
battery: battery,
txQueueLength: txQueueLen,
noiseFloor: noiseFloor,
lastRSSI: lastRSSI,
packetsReceived: packetsRecv,
packetsSent: packetsSent,
airtime: airtime,
uptime: uptime,
sentFlood: sentFlood,
sentDirect: sentDirect,
receivedFlood: recvFlood,
receivedDirect: recvDirect,
fullEvents: fullEvents,
lastSNR: lastSNR,
directDuplicates: directDups,
floodDuplicates: floodDups,
rxAirtime: rxAirtime,
receiveErrors: receiveErrors
)
case .roomServer:
let postedCount: UInt16? = data.count >= PacketSize.binaryResponseStatusWithRxAirtime
? data.readUInt16LE(at: offset) : nil
let postPushCount: UInt16? = data.count >= PacketSize.binaryResponseStatusWithRxAirtime
? data.readUInt16LE(at: offset + 2) : nil

return MeshCore.StatusResponse(
layout: .roomServer,
publicKeyPrefix: publicKeyPrefix,
battery: battery,
txQueueLength: txQueueLen,
noiseFloor: noiseFloor,
lastRSSI: lastRSSI,
packetsReceived: packetsRecv,
packetsSent: packetsSent,
airtime: airtime,
uptime: uptime,
sentFlood: sentFlood,
sentDirect: sentDirect,
receivedFlood: recvFlood,
receivedDirect: recvDirect,
fullEvents: fullEvents,
lastSNR: lastSNR,
directDuplicates: directDups,
floodDuplicates: floodDups,
rxAirtime: 0,
receiveErrors: 0,
roomServerPostedCount: postedCount,
roomServerPostPushCount: postPushCount
)
}
}
}

Expand Down
54 changes: 48 additions & 6 deletions MeshCore/Sources/MeshCore/Session/MeshCoreSession.swift
Original file line number Diff line number Diff line change
Expand Up @@ -883,21 +883,57 @@ public actor MeshCoreSession: MeshCoreSessionProtocol {

/// Requests status information from a remote node using the binary protocol.
///
/// Raw public-key status requests use the repeater status layout.
/// For room servers, prefer ``requestStatus(from: MeshContact)`` or
/// ``requestStatus(from:type:)`` so the correct status layout is selected.
///
/// - Parameter publicKey: The full 32-byte public key of the remote node.
/// - Returns: A status response containing battery, uptime, and other metrics.
/// - Throws: ``MeshCoreError/timeout`` if no response within the timeout period.
/// ``MeshCoreError/deviceError(code:)`` if the device rejects the request.
/// ``MeshCoreError/invalidResponse`` if an unexpected response is received.
public func requestStatus(from publicKey: Data) async throws -> StatusResponse {
try requireFullPublicKey(publicKey, operation: "requestStatus")
// Serialize binary requests to prevent messageSent race conditions
return try await binaryRequestSerializer.withSerialization { [self] in
try await performStatusRequest(from: publicKey)
try await performStatusRequest(from: publicKey, layout: .repeater)
}
}

/// Requests status information from a remote node using the binary protocol.
///
/// - Parameters:
/// - publicKey: The full 32-byte public key of the remote node.
/// - type: The target node type used to choose the correct firmware status layout.
/// - Returns: A status response containing battery, uptime, and other metrics.
/// - Throws: ``MeshCoreError/timeout`` if no response within the timeout period.
/// ``MeshCoreError/deviceError(code:)`` if the device rejects the request.
/// ``MeshCoreError/invalidResponse`` if an unexpected response is received.
public func requestStatus(
from publicKey: Data,
type: ContactType
) async throws -> StatusResponse {
try requireFullPublicKey(publicKey, operation: "requestStatus")
let layout: StatusResponse.Layout = type == .room ? .roomServer : .repeater
return try await binaryRequestSerializer.withSerialization { [self] in
try await performStatusRequest(from: publicKey, layout: layout)
}
}

/// Requests status information from a remote contact using its contact type to
/// select the correct firmware status layout.
///
/// - Parameter contact: The remote contact to query.
/// - Returns: A status response containing battery, uptime, and other metrics.
/// - Throws: ``MeshCoreError`` if the request fails.
public func requestStatus(from contact: MeshContact) async throws -> StatusResponse {
try await requestStatus(from: contact.publicKey, type: contact.type)
}

/// Internal implementation of status request, called within serialization.
private func performStatusRequest(from publicKey: Data) async throws -> StatusResponse {
private func performStatusRequest(
from publicKey: Data,
layout: StatusResponse.Layout
) async throws -> StatusResponse {
let data = PacketBuilder.binaryRequest(to: publicKey, type: .status)
let publicKeyPrefix = Data(publicKey.prefix(6))
let prefixHex = publicKeyPrefix.map { String(format: "%02x", $0) }.joined()
Expand Down Expand Up @@ -942,7 +978,8 @@ public actor MeshCoreSession: MeshCoreSessionProtocol {

guard let response = Parsers.StatusResponse.parseFromBinaryResponse(
responseData,
publicKeyPrefix: publicKeyPrefix
publicKeyPrefix: publicKeyPrefix,
layout: layout
) else {
return nil
}
Expand Down Expand Up @@ -996,8 +1033,13 @@ public actor MeshCoreSession: MeshCoreSessionProtocol {
/// - Returns: Status response from the remote node.
/// - Throws: ``MeshCoreError`` on failure.
public func requestStatus(from destination: Destination) async throws -> StatusResponse {
let publicKey = try destination.fullPublicKey()
return try await requestStatus(from: publicKey)
switch destination {
case .contact(let contact):
return try await requestStatus(from: contact)
case .data, .hexString:
let publicKey = try destination.fullPublicKey()
return try await requestStatus(from: publicKey)
}
}

// MARK: - Keep-Alive
Expand Down
Loading