Fix igiRadio app discovery: publish mDNS service, scan real subnet
The Bonjour browse for _http._tcp in DigiRadioDiscoveryService never found the device because the firmware only called mdns_hostname_set() (answers direct A-record lookups) and never mdns_service_add() (needed for service browse to see it at all). The app's LAN-scan fallback also assumed a /24 subnet, so it silently missed devices on networks with a narrower mask (e.g. /22) when the phone and the device land in different third-octet blocks of the same network. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018iiTF1KGJBzY9zzpnz9Zr2
This commit is contained in:
@@ -0,0 +1,84 @@
|
|||||||
|
import Foundation
|
||||||
|
import Darwin
|
||||||
|
|
||||||
|
enum LocalNetworkScanner {
|
||||||
|
/// A contiguous IPv4 host range derived from the interface's actual
|
||||||
|
/// address + netmask — NOT assumed to be a /24 (see `hosts(in:)`).
|
||||||
|
struct Subnet {
|
||||||
|
var prefix: String
|
||||||
|
var networkAddress: UInt32
|
||||||
|
var hostCount: UInt32
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the Wi‑Fi IPv4 subnet, sized from the interface's real netmask.
|
||||||
|
///
|
||||||
|
/// Networks narrower than /24 (e.g. /22, common on larger home/office
|
||||||
|
/// routers) span multiple third-octet blocks; assuming /24 silently
|
||||||
|
/// drops hosts outside the phone's own block. Cap the scan at /20 (4094
|
||||||
|
/// hosts) so an unusually flat network doesn't turn this into an
|
||||||
|
/// unbounded sweep.
|
||||||
|
static func currentWiFiSubnet() -> Subnet? {
|
||||||
|
var candidates: [(ip: UInt32, mask: UInt32, display: String)] = []
|
||||||
|
var ifaddr: UnsafeMutablePointer<ifaddrs>?
|
||||||
|
guard getifaddrs(&ifaddr) == 0, let first = ifaddr else { return nil }
|
||||||
|
defer { freeifaddrs(ifaddr) }
|
||||||
|
|
||||||
|
var ptr: UnsafeMutablePointer<ifaddrs>? = first
|
||||||
|
while let interface = ptr {
|
||||||
|
let flags = Int32(interface.pointee.ifa_flags)
|
||||||
|
let isUp = (flags & IFF_UP) != 0
|
||||||
|
let isLoopback = (flags & IFF_LOOPBACK) != 0
|
||||||
|
let name = String(cString: interface.pointee.ifa_name)
|
||||||
|
|
||||||
|
if isUp, !isLoopback, name.hasPrefix("en"),
|
||||||
|
let addr = interface.pointee.ifa_addr,
|
||||||
|
addr.pointee.sa_family == UInt8(AF_INET),
|
||||||
|
let maskAddr = interface.pointee.ifa_netmask,
|
||||||
|
maskAddr.pointee.sa_family == UInt8(AF_INET) {
|
||||||
|
var host = [CChar](repeating: 0, count: Int(NI_MAXHOST))
|
||||||
|
let len = socklen_t(addr.pointee.sa_len)
|
||||||
|
if getnameinfo(addr, len, &host, socklen_t(host.count), nil, 0, NI_NUMERICHOST) == 0 {
|
||||||
|
let ip = String(cString: host)
|
||||||
|
if !ip.hasPrefix("127."), !ip.hasPrefix("169.254."),
|
||||||
|
let ipValue = ipv4Value(ip) {
|
||||||
|
let maskValue = maskAddr.withMemoryRebound(to: sockaddr_in.self, capacity: 1) {
|
||||||
|
UInt32(bigEndian: $0.pointee.sin_addr.s_addr)
|
||||||
|
}
|
||||||
|
candidates.append((ipValue, maskValue, ip))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ptr = interface.pointee.ifa_next
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let (ip, mask, display) = candidates.first else { return nil }
|
||||||
|
// Never scan a wider range than a /20 (4094 hosts), even if the
|
||||||
|
// reported mask is flatter than that.
|
||||||
|
let cappedMask = mask | 0xFFF0_0000
|
||||||
|
let networkAddress = ip & cappedMask
|
||||||
|
let hostCount = ~cappedMask
|
||||||
|
let parts = display.split(separator: ".").compactMap { Int($0) }
|
||||||
|
let prefix = parts.count == 4 ? "\(parts[0]).\(parts[1]).\(parts[2])" : display
|
||||||
|
return Subnet(prefix: prefix, networkAddress: networkAddress, hostCount: hostCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Every usable host address in `subnet` (network + broadcast excluded).
|
||||||
|
static func hosts(in subnet: Subnet) -> [String] {
|
||||||
|
guard subnet.hostCount > 1 else { return [] }
|
||||||
|
return (1 ..< subnet.hostCount).map { offset in
|
||||||
|
"http://\(ipv4String(subnet.networkAddress + offset))"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static let setupSoftAPHost = "http://192.168.4.1"
|
||||||
|
|
||||||
|
private static func ipv4Value(_ dotted: String) -> UInt32? {
|
||||||
|
let parts = dotted.split(separator: ".").compactMap { UInt32($0) }
|
||||||
|
guard parts.count == 4, parts.allSatisfy({ $0 <= 255 }) else { return nil }
|
||||||
|
return (parts[0] << 24) | (parts[1] << 16) | (parts[2] << 8) | parts[3]
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func ipv4String(_ value: UInt32) -> String {
|
||||||
|
"\((value >> 24) & 0xFF).\((value >> 16) & 0xFF).\((value >> 8) & 0xFF).\(value & 0xFF)"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -354,6 +354,12 @@ StaClient::connect(const core::WifiCredentials& creds,
|
|||||||
if (!hostLabel.empty()) {
|
if (!hostLabel.empty()) {
|
||||||
if (mdns_init() == ESP_OK) {
|
if (mdns_init() == ESP_OK) {
|
||||||
mdns_hostname_set(hostLabel.c_str());
|
mdns_hostname_set(hostLabel.c_str());
|
||||||
|
// Publishes the PTR/SRV/TXT records the app's Bonjour
|
||||||
|
// browse (_http._tcp) needs to discover this device.
|
||||||
|
// mdns_hostname_set() alone only answers direct A-record
|
||||||
|
// lookups of <hostLabel>.local, which a browse never sends.
|
||||||
|
mdns_service_add(hostLabel.c_str(), "_http", "_tcp", 80,
|
||||||
|
nullptr, 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ESP_LOGI(kTag, "connected to %.*s",
|
ESP_LOGI(kTag, "connected to %.*s",
|
||||||
|
|||||||
Reference in New Issue
Block a user