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:
2026-09-03 20:00:52 +02:00
co-authored by Claude Sonnet 5
parent 5f46486d46
commit e4ca925801
2 changed files with 90 additions and 0 deletions
@@ -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 WiFi 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",