diff --git a/OpenAudioNetwork b/OpenAudioNetwork index bafa755..1b824ac 160000 --- a/OpenAudioNetwork +++ b/OpenAudioNetwork @@ -1 +1 @@ -Subproject commit bafa7554b8070c22cc64820c87293c4aa1a00b69 +Subproject commit 1b824ac0ba0f40132f85c38b8061231aa7e9c3af diff --git a/coreui/core/NetworkConfig.h b/coreui/core/NetworkConfig.h index d5f9347..29caee5 100644 --- a/coreui/core/NetworkConfig.h +++ b/coreui/core/NetworkConfig.h @@ -13,7 +13,16 @@ struct NetworkConfig { std::string eth_interface; - uint16_t uid; + // hint_uid: 0 = no hint, static-range value = pin (autoconfig skipped), + // dynamic-range value = ignored with a warning (per design §2.5). + uint16_t hint_uid = 0; + // persisted_uid: the autoconfigurator's last-committed value, fed back + // into the configurator as a "try this first" hint. Optional. + uint16_t persisted_uid = 0; + + // After init_console runs, this is the committed UID (autoconfigured + // or static-pinned). + uint16_t uid = 0; QJsonObject serialize(); }; diff --git a/coreui/core/ShowManager.cpp b/coreui/core/ShowManager.cpp index 39b586d..04da5e3 100644 --- a/coreui/core/ShowManager.cpp +++ b/coreui/core/ShowManager.cpp @@ -5,8 +5,96 @@ #include "ShowManager.h" +#include "OpenAudioNetwork/common/UidStore.h" + +#include +#include + #include +namespace { + +// Persist the autoconfigured UID into a top-level field of an existing +// QJson document. Atomic via QSaveFile (write-tmp-then-rename). +class QJsonFieldUidStore : public IUidStore { +public: + QJsonFieldUidStore(QString path, QString field) + : m_path(std::move(path)), m_field(std::move(field)) {} + + std::optional load() override { + QFile f(m_path); + if (!f.open(QIODevice::ReadOnly)) return std::nullopt; + QJsonParseError err{}; + auto doc = QJsonDocument::fromJson(f.readAll(), &err); + if (err.error != QJsonParseError::NoError || !doc.isObject()) { + std::cerr << "QJsonFieldUidStore: parse '" << m_path.toStdString() + << "' failed: " << err.errorString().toStdString() << std::endl; + return std::nullopt; + } + auto root = doc.object(); + auto net = root.value("network").toObject(); + auto v = net.value(m_field); + if (!v.isDouble()) return std::nullopt; + int i = v.toInt(-1); + if (i < 0 || i > 0xFFFF) return std::nullopt; + return static_cast(i); + } + + void save(uint16_t uid) override { + QJsonObject root; + { + QFile f(m_path); + if (f.open(QIODevice::ReadOnly)) { + auto doc = QJsonDocument::fromJson(f.readAll()); + if (doc.isObject()) root = doc.object(); + } + } + QJsonObject net = root.value("network").toObject(); + net.insert(m_field, static_cast(uid)); + root.insert("network", net); + + QSaveFile out(m_path); + if (!out.open(QIODevice::WriteOnly | QIODevice::Truncate)) { + std::cerr << "QJsonFieldUidStore: open '" << m_path.toStdString() + << "' for write failed." << std::endl; + return; + } + out.write(QJsonDocument(root).toJson(QJsonDocument::Indented)); + if (!out.commit()) { + std::cerr << "QJsonFieldUidStore: commit '" << m_path.toStdString() + << "' failed." << std::endl; + } + } + + void clear() override { + QFile f(m_path); + if (!f.open(QIODevice::ReadOnly)) return; + auto doc = QJsonDocument::fromJson(f.readAll()); + f.close(); + if (!doc.isObject()) return; + auto root = doc.object(); + auto net = root.value("network").toObject(); + if (!net.contains(m_field)) return; + net.remove(m_field); + root.insert("network", net); + + QSaveFile out(m_path); + if (!out.open(QIODevice::WriteOnly | QIODevice::Truncate)) return; + out.write(QJsonDocument(root).toJson(QJsonDocument::Indented)); + out.commit(); + } + +private: + QString m_path; + QString m_field; +}; + +bool is_static_range(uint16_t uid) { + return uid >= 0xF000 && uid <= 0xFFFE; +} + +} // namespace + ShowManager::ShowManager() : QObject(nullptr) { m_netconfig = NetworkConfig{}; } @@ -22,7 +110,9 @@ bool ShowManager::init_console(SignalWindow* sw) { infos.dev_type = DeviceType::CONTROL_SURFACE; infos.iface = m_netconfig.eth_interface; infos.sample_rate = SamplingRate::SAMPLING_96K; - infos.uid = m_netconfig.uid; + // Hint = static-range pin only; dynamic-range hints are ignored + // by the configurator (per design §2.5). + infos.uid = is_static_range(m_netconfig.hint_uid) ? m_netconfig.hint_uid : 0; infos.topo.phy_in_count = 0; infos.topo.phy_out_count = 0; infos.topo.pipes_count = 0; @@ -34,6 +124,29 @@ bool ShowManager::init_console(SignalWindow* sw) { return false; } + if (!is_static_range(m_netconfig.hint_uid)) { + // No static pin — run autoconfig, persist to surface.json. + auto backing = std::make_unique( + QStringLiteral("surface_config/surface.json"), + QStringLiteral("persisted_uid")); + if (m_renumber) { + std::cout << "--renumber: clearing persisted UID in surface.json" << std::endl; + backing->clear(); + } + EnvOverrideUidStore store{"OAN_PERSISTED_UID", std::move(backing)}; + uint16_t committed = m_nmapper->autoconfigure_uid(store); + if (committed == 0) { + std::cerr << "ShowManager: UID autoconfiguration failed." << std::endl; + return false; + } + m_netconfig.uid = committed; + } else { + m_netconfig.uid = m_netconfig.hint_uid; + std::cout << "ShowManager: static-range UID 0x" << std::hex + << m_netconfig.hint_uid << std::dec + << " pinned; autoconfig skipped." << std::endl; + } + std::cout << "Starting netmapper and router processes on interface " << infos.iface << std::endl; m_nmapper->set_peer_change_callback([this](PeerInfos& infos, bool peer_state) { @@ -155,13 +268,14 @@ void ShowManager::load_console_config() { if (!config_file.open(QIODeviceBase::ReadOnly)) { std::cerr << "Failed to open surface.json config file" << std::endl; - std::cerr << "Using default config (iface = lo, uid = 200)" << std::endl; + std::cerr << "Using default config (iface = lo, no UID hint)" << std::endl; NetworkConfig netcfg{}; netcfg.eth_interface = "lo"; - netcfg.uid = 200; - + netcfg.hint_uid = 0; + netcfg.persisted_uid = 0; m_netconfig = std::move(netcfg); + return; } auto doc = QJsonDocument::fromJson(config_file.readAll()); @@ -169,7 +283,8 @@ void ShowManager::load_console_config() { NetworkConfig netcfg{}; netcfg.eth_interface = net_root["eth_interface"].toString("lo").toStdString(); - netcfg.uid = net_root["uid"].toInt(200); + netcfg.hint_uid = static_cast(net_root["uid"].toInt(0)); + netcfg.persisted_uid = static_cast(net_root["persisted_uid"].toInt(0)); m_netconfig = std::move(netcfg); } diff --git a/coreui/core/ShowManager.h b/coreui/core/ShowManager.h index e1e643e..9139c3f 100644 --- a/coreui/core/ShowManager.h +++ b/coreui/core/ShowManager.h @@ -44,6 +44,10 @@ class ShowManager : public QObject { bool init_console(SignalWindow* sw); + // Set before init_console: if true, the autoconfigurator clears any + // persisted UID before deriving a fresh one. + void set_renumber(bool r) { m_renumber = r; } + void add_pipe(PipeDesc *pipe_desc, QString pipe_name, uint8_t channel, uint16_t host, uint16_t pid, bool unsynced = false); void update_page(SignalWindow* swin); @@ -74,6 +78,7 @@ class ShowManager : public QObject { std::shared_ptr m_nmapper; NetworkConfig m_netconfig; + bool m_renumber = false; DSPManager* m_dsp_manager; std::shared_ptr m_plugin_loader; diff --git a/coreui/main.cpp b/coreui/main.cpp index b47aeec..842bb0b 100644 --- a/coreui/main.cpp +++ b/coreui/main.cpp @@ -7,6 +7,8 @@ #include #include +#include + #include "ui/SignalWindow.h" #include "ui/SetupWindow.h" @@ -19,9 +21,17 @@ #endif int main(int argc, char* argv[]) { + bool renumber = false; + for (int i = 1; i < argc; ++i) { + if (std::string(argv[i]) == "--renumber") { + renumber = true; + } + } + QApplication qapp {argc, argv}; auto* sm = new ShowManager{}; + sm->set_renumber(renumber); // Software initialization // Load stored console config diff --git a/coreui/surface_config/surface.json b/coreui/surface_config/surface.json index bb63556..747e20d 100644 --- a/coreui/surface_config/surface.json +++ b/coreui/surface_config/surface.json @@ -1,10 +1,11 @@ { - "network": { - "eth_interface": "sim:default", - "uid": 200 - }, - - "plugins": { - "search_paths": ["~/osst/plugins/"] - } -} \ No newline at end of file + "network": { + "eth_interface": "sim:default", + "persisted_uid": 21190 + }, + "plugins": { + "search_paths": [ + "~/osst/plugins/" + ] + } +} diff --git a/engine/NetMan.cpp b/engine/NetMan.cpp index 354114e..39527b5 100644 --- a/engine/NetMan.cpp +++ b/engine/NetMan.cpp @@ -15,12 +15,12 @@ NetMan::~NetMan() { } -bool NetMan::init_netman(const std::string& iface) { +bool NetMan::init_netman(const std::string& iface, IUidStore* uid_store) { m_pconf = PeerConf{}; m_pconf.dev_type = DeviceType::AUDIO_DSP; m_pconf.sample_rate = SamplingRate::SAMPLING_96K; m_pconf.topo = NodeTopology{0, 0, 64, 0xFFFFFFFFFFFFFFFF}; - m_pconf.uid = 100; + m_pconf.uid = 0; // 0 = "no hint", let the configurator pick. m_pconf.iface = iface; m_pconf.ck_type = CKTYPE_MASTER; @@ -33,6 +33,19 @@ bool NetMan::init_netman(const std::string& iface) { return false; } + if (uid_store) { + uint16_t committed = m_nmapper->autoconfigure_uid(*uid_store); + if (committed == 0) { + std::cerr << LOG_PREFIX << "UID autoconfiguration failed." << std::endl; + return false; + } + m_pconf.uid = committed; + } else { + // No store: caller (e.g. tests) accepts whatever PeerConf::uid was + // pre-seeded with. Mirror it back out of the mapper for consistency. + m_pconf.uid = m_nmapper->committed_uid(); + } + m_dsp_control = std::make_unique(m_pconf.uid, m_nmapper); if (!m_dsp_control->init_socket(m_pconf.iface, EthProtocol::ETH_PROTO_OANCONTROL)) { std::cerr << LOG_PREFIX << "Failed to init DSP Control socket." << std::endl; diff --git a/engine/NetMan.h b/engine/NetMan.h index 9df9596..4efc438 100644 --- a/engine/NetMan.h +++ b/engine/NetMan.h @@ -15,6 +15,8 @@ #include "piping/AudioPlumber.h" #include "log.h" +#include "OpenAudioNetwork/common/UidStore.h" + #include @@ -23,7 +25,13 @@ class NetMan { NetMan(AudioPlumber* plumber); ~NetMan(); - bool init_netman(const std::string& iface); + // Init the network manager and run UID autoconfiguration against the + // given store. Store may be null to skip autoconfig (e.g. for tests + // that want a deterministic UID via PeerConf). + bool init_netman(const std::string& iface, IUidStore* uid_store); + + uint16_t committed_uid() const { return m_pconf.uid; } + void update_netman(); void start_mapping(); diff --git a/engine/main.cpp b/engine/main.cpp index 8493f44..dad2f82 100644 --- a/engine/main.cpp +++ b/engine/main.cpp @@ -6,6 +6,8 @@ #include #include #include +#include +#include #ifdef OAN_HOST_BACKENDS #include // pause() @@ -22,9 +24,33 @@ #include "OpenAudioNetwork/common/AudioRouter.h" #include "OpenAudioNetwork/common/ClockMaster.h" +#include "OpenAudioNetwork/common/UidStore.h" #include "OpenAudioNetwork/netutils/platform/rt.h" +namespace { + +std::string sanitise_iface(const std::string& iface) { + std::string out; + out.reserve(iface.size()); + for (char c : iface) { + out.push_back((std::isalnum(static_cast(c)) || c == '-' || c == '_') ? c : '_'); + } + return out; +} + +std::string engine_uid_path(const std::string& iface) { +#ifdef OAN_HOST_BACKENDS + const char* home = std::getenv("HOME"); + std::string base = (home && *home) ? std::string(home) + "/.local/state/oals" : "/tmp/oals"; +#else + std::string base = "/var/lib/oals"; +#endif + return base + "/engine-" + sanitise_iface(iface) + ".uid"; +} + +} // namespace + static void print_usage() { std::cout << "OALSEngine — Open Audio Live System DSP engine\n" @@ -40,7 +66,9 @@ static void print_usage() { " raw: (Mac BPF — not yet implemented)\n" " Defaults to \"lo\" when omitted.\n" "\n" - " --help Show this message.\n" + " --renumber Clear persisted UID before boot, then autoconfigure\n" + " from scratch. Useful after a hardware swap.\n" + " --help Show this message.\n" "\n" "The engine runs four detached RT threads (audio recv, control recv,\n" "pipe updater, clock syncer) plus a main thread that parks. It must\n" @@ -50,12 +78,17 @@ static void print_usage() { int main(int argc, char* argv[]) { std::string eth_interface = "lo"; - if (argc > 1) { - std::string arg = argv[1]; + bool renumber = false; + for (int i = 1; i < argc; ++i) { + std::string arg = argv[i]; if (arg == "--help" || arg == "-h") { print_usage(); return 0; } + if (arg == "--renumber") { + renumber = true; + continue; + } eth_interface = std::move(arg); } @@ -63,12 +96,21 @@ int main(int argc, char* argv[]) { AudioEngine audio_engine{}; NetMan nman{&plumber}; - AudioRouter router{100}; + auto file_store = std::make_unique(engine_uid_path(eth_interface)); + if (renumber) { + std::cout << LOG_PREFIX << "--renumber: clearing persisted UID at " + << engine_uid_path(eth_interface) << std::endl; + file_store->clear(); + } + EnvOverrideUidStore uid_store{"OAN_PERSISTED_UID", std::move(file_store)}; - if (!nman.init_netman(eth_interface)) { + if (!nman.init_netman(eth_interface, &uid_store)) { std::cerr << LOG_PREFIX << "Failed to initialize network manager." << std::endl; + return -1; } + AudioRouter router{nman.committed_uid()}; + if (!router.init_router(eth_interface, nman.get_net_mapper())) { std::cerr << LOG_PREFIX << "Failed to initialize audio router." << std::endl; exit(-2); diff --git a/io_sim/io_sim.example.json b/io_sim/io_sim.example.json index 8a1077a..8a69214 100644 --- a/io_sim/io_sim.example.json +++ b/io_sim/io_sim.example.json @@ -1,5 +1,5 @@ { - "uid": 1, + "_comment_uid": "UID is autoconfigured at first boot and written back as 'persisted_uid'. To pin a static UID, set a top-level 'uid' to a value in 0xF000-0xFFFE (dynamic-range values are ignored). To re-derive, run with --renumber.", "tracks": [ { "channel": 0, "tone": { "freq": 440.0, "gain": 0.3 } }, { "channel": 1, "tone": { "freq": 880.0, "gain": 0.3 } }, diff --git a/io_sim/main.cpp b/io_sim/main.cpp index 3f06f92..32e648e 100644 --- a/io_sim/main.cpp +++ b/io_sim/main.cpp @@ -12,6 +12,8 @@ #include #include #include +#include +#include #include #include @@ -19,6 +21,7 @@ #include #include #include +#include #include @@ -132,6 +135,103 @@ std::vector gen_packet_strm_tone(float freq_hz, float gain, int cha return stream_packets; } +// Persists the autoconfigured UID into the io_sim.json config itself +// under the configured field name. Read-modify-write keeps any other +// fields the user has in the file (tracks etc.) intact. Atomic via +// temp+rename. Errors logged, never thrown. +class JsonFieldUidStore : public IUidStore { +public: + JsonFieldUidStore(std::string path, std::string field) + : m_path(std::move(path)), m_field(std::move(field)) {} + + std::optional load() override { + std::ifstream f(m_path); + if (!f) return std::nullopt; + try { + nlohmann::json doc; + f >> doc; + if (!doc.contains(m_field)) return std::nullopt; + const auto& v = doc.at(m_field); + if (!v.is_number_integer()) return std::nullopt; + int64_t i = v.get(); + if (i < 0 || i > 0xFFFF) return std::nullopt; + return static_cast(i); + } catch (const std::exception& e) { + std::cerr << "JsonFieldUidStore: load from '" << m_path + << "' failed: " << e.what() << std::endl; + return std::nullopt; + } + } + + void save(uint16_t uid) override { + nlohmann::json doc; + { + std::ifstream f(m_path); + if (f) { + try { + f >> doc; + } catch (const std::exception& e) { + std::cerr << "JsonFieldUidStore: parse of '" << m_path + << "' failed, will overwrite: " << e.what() << std::endl; + doc = nlohmann::json::object(); + } + } else { + doc = nlohmann::json::object(); + } + } + doc[m_field] = uid; + + std::string tmp = m_path + ".tmp"; + { + std::ofstream f(tmp, std::ios::trunc); + if (!f) { + std::cerr << "JsonFieldUidStore: open temp '" << tmp + << "' for write failed." << std::endl; + return; + } + f << doc.dump(2) << '\n'; + } + std::error_code ec; + std::filesystem::rename(tmp, m_path, ec); + if (ec) { + std::cerr << "JsonFieldUidStore: rename to '" << m_path + << "' failed: " << ec.message() << std::endl; + std::error_code ec2; + std::filesystem::remove(tmp, ec2); + } + } + + void clear() override { + std::ifstream f(m_path); + if (!f) return; + nlohmann::json doc; + try { + f >> doc; + } catch (...) { + return; + } + if (!doc.contains(m_field)) return; + doc.erase(m_field); + + std::string tmp = m_path + ".tmp"; + { + std::ofstream of(tmp, std::ios::trunc); + if (!of) return; + of << doc.dump(2) << '\n'; + } + std::error_code ec; + std::filesystem::rename(tmp, m_path, ec); + if (ec) { + std::error_code ec2; + std::filesystem::remove(tmp, ec2); + } + } + +private: + std::string m_path; + std::string m_field; +}; + static void print_usage() { std::cout << "io_simulator — looping audio source for the OALS dev stack\n" @@ -145,26 +245,37 @@ static void print_usage() { " Defaults to ./io_sim.json. Example template in\n" " io_sim/io_sim.example.json.\n" "\n" + " --renumber Clear persisted UID before boot, then autoconfigure\n" + " from scratch.\n" " --help Show this message.\n" "\n" "Loops the configured tracks (tone or .wav stems) onto the OAN audio\n" - "EtherType at 96 kHz, advertising itself as an AUDIO_IO_INTERFACE\n" - "with uid from the config (default 1) and acting as a ClockSlave to\n" - "whatever ClockMaster is on the segment.\n"; + "EtherType at 96 kHz, advertising itself as an AUDIO_IO_INTERFACE.\n" + "UID is autoconfigured at first boot and persisted into the config\n" + "file as 'persisted_uid'. Set 'uid' to a static-range value (0xF000-\n" + "0xFFFE) to pin manually. Acts as a ClockSlave to whatever ClockMaster\n" + "is on the segment.\n"; } int main(int argc, char* argv[]) { - if (argc > 1) { - std::string a = argv[1]; + bool renumber = false; + std::vector positional; + for (int i = 1; i < argc; ++i) { + std::string a = argv[i]; if (a == "--help" || a == "-h") { print_usage(); return 0; } + if (a == "--renumber") { + renumber = true; + continue; + } + positional.push_back(std::move(a)); } std::cout << "OpenAudioLive IO Emulator" << std::endl; - const std::string config_path = (argc > 2) ? argv[2] : "io_sim.json"; + const std::string config_path = (positional.size() > 1) ? positional[1] : "io_sim.json"; nlohmann::json cfg; { @@ -184,14 +295,16 @@ int main(int argc, char* argv[]) { } PeerConf conf{}; - conf.iface = (argc > 1) ? argv[1] : "virbr0"; + conf.iface = (!positional.empty()) ? positional[0] : "virbr0"; const char name[32] = "IOSIM"; memcpy(&conf.dev_name, name, strlen(name)); conf.sample_rate = SamplingRate::SAMPLING_96K; conf.dev_type = DeviceType::AUDIO_IO_INTERFACE; - conf.uid = cfg.value("uid", 1); + // Static-range hints in the config are honoured by the autoconfigurator; + // dynamic-range hints are ignored with a warning. 0 = no hint. + conf.uid = cfg.value("uid", 0); conf.topo.phy_in_count = 4; conf.topo.phy_out_count = 4; conf.topo.pipes_count = 1; @@ -201,20 +314,35 @@ int main(int argc, char* argv[]) { std::shared_ptr nmapper = std::make_shared(conf); std::cout << "Initializing on " << conf.iface << std::endl; - if(nmapper->init_mapper(conf.iface)) { - nmapper->launch_mapping_process(); - } else { + if(!nmapper->init_mapper(conf.iface)) { std::cerr << "Failed to init mapper" << std::endl; exit(-1); } + JsonFieldUidStore file_store{config_path, "persisted_uid"}; + if (renumber) { + std::cout << "--renumber: clearing persisted UID in " + << config_path << std::endl; + file_store.clear(); + } + EnvOverrideUidStore uid_store{"OAN_PERSISTED_UID", + std::make_unique(config_path, "persisted_uid")}; + uint16_t committed = nmapper->autoconfigure_uid(uid_store); + if (committed == 0) { + std::cerr << "io_sim: UID autoconfiguration failed." << std::endl; + return -1; + } + conf.uid = committed; + + nmapper->launch_mapping_process(); + LowLatSocket audio_iface(conf.uid, nmapper); audio_iface.init_socket(conf.iface, EthProtocol::ETH_PROTO_OANAUDIO); LowLatSocket control_iface(conf.uid, nmapper); control_iface.init_socket(conf.iface, EthProtocol::ETH_PROTO_OANCONTROL); - ClockSlave cs{1, conf.iface, nmapper}; + ClockSlave cs{conf.uid, conf.iface, nmapper}; oals::rt::set_process_scheduler_rr(99); diff --git a/tools/sim_switch/Switch.cpp b/tools/sim_switch/Switch.cpp index 476c0b5..755a26f 100644 --- a/tools/sim_switch/Switch.cpp +++ b/tools/sim_switch/Switch.cpp @@ -9,11 +9,31 @@ #include #include +#include "common/packet_structs.h" + // Per-conn rx_buf must fit at least one max-size frame. AudioPacket framing on // the wire is ~360 bytes; budget generously for future EtherTypes too. constexpr size_t MAX_FRAME_PAYLOAD = 8192; constexpr size_t RX_READ_CHUNK = 4096; +// Layout the disco peer puts on the wire (matches DiscoveryPeek): [eth 14] +// [LowLatHeader 6][OANPacket]. self_uid lives inside the +// MappingData payload. +namespace { +constexpr size_t DISCO_LL_PREFIX = 14 + 6; + +// Extract self_uid from a disco frame payload if it parses as a MAPPING +// packet; returns 0 otherwise (also for genuinely uid=0 packets, which is +// fine — uid=0 means "unknown" in our adoption flow). +uint16_t parse_mapping_self_uid(const uint8_t* payload, size_t len) { + if (len < DISCO_LL_PREFIX + sizeof(OANPacket)) return 0; + OANPacket pck{}; + std::memcpy(&pck, payload + DISCO_LL_PREFIX, sizeof(pck)); + if (pck.header.type != PacketType::MAPPING) return 0; + return pck.packet_data.self_uid; +} +} // namespace + Switch::EtypeIdx Switch::etype_index(uint16_t e) { switch (e) { case ETH_PROTO_OANAUDIO: return EtypeIdx::AUDIO; @@ -149,6 +169,30 @@ int Switch::consume_frame(Conn& c) { if (c.rx_buf.size() < sizeof(SimFrame) + hdr.payload_len) return 0; + const uint8_t* payload = c.rx_buf.data() + sizeof(SimFrame); + + // Conn-uid adoption: NetworkMapper creates its discovery socket *before* + // UID autoconfig runs, so the hello on that conn carries uid=0. The + // committed UID first shows up in the MAPPING packet payload. Adopt it + // here so per-peer stats, src_uid stamping, and the route table all see + // the right identity for the rest of the session. + if (!c.promiscuous && c.self_uid == 0 + && hdr.ethertype == ETH_PROTO_OANDISCO) { + uint16_t learned = parse_mapping_self_uid(payload, hdr.payload_len); + if (learned != 0) { + // Drop the bogus (disco, 0) route only if it still points to us. + // Another zero-uid conn may have overwritten it at hello time. + uint32_t old_key = (uint32_t(ETH_PROTO_OANDISCO) << 16) | 0u; + auto old_it = m_route_table.find(old_key); + if (old_it != m_route_table.end() && old_it->second == c.fd) { + m_route_table.erase(old_it); + } + c.self_uid = learned; + uint32_t new_key = (uint32_t(ETH_PROTO_OANDISCO) << 16) | learned; + m_route_table[new_key] = c.fd; + } + } + // The switch owns src_uid attribution — sender's value is overwritten // with the conn's registered uid so observers can trust it. hdr.src_uid = c.self_uid; @@ -158,8 +202,6 @@ int Switch::consume_frame(Conn& c) { // defensive in case someone later refactors to splat raw bytes. std::memcpy(c.rx_buf.data(), &hdr, sizeof(hdr)); - const uint8_t* payload = c.rx_buf.data() + sizeof(SimFrame); - // Stats int idx = (int)etype_index(hdr.ethertype); m_stats.frames_in[idx]++; diff --git a/tools/sim_switch/test/test_sim_switch.cpp b/tools/sim_switch/test/test_sim_switch.cpp index 90d8928..6fa67df 100644 --- a/tools/sim_switch/test/test_sim_switch.cpp +++ b/tools/sim_switch/test/test_sim_switch.cpp @@ -555,6 +555,55 @@ TEST(SimSwitch, V1HelloRejected) { EXPECT_EQ(::read(a.fd(), &buf, 1), 0); // EOF from server } +// 16. Disco conn UID adoption. NetworkMapper creates its discovery socket +// before UID autoconfig runs, so the hello on that conn carries uid=0. Once +// the autoconfigured UID first appears in a MAPPING packet, the switch must +// adopt it as the conn's identity — otherwise unicast disco to that peer +// goes nowhere and the TUI shows every engine aggregated as uid=0. +#include "common/packet_structs.h" +TEST(SimSwitch, DiscoConnAdoptsUidFromMappingPayload) { + auto path = make_test_socket_path(); + DaemonProc d(path); + ASSERT_TRUE(d.ready()); + RawClient a, b, insp; + ASSERT_TRUE(a.connect(path)); + ASSERT_TRUE(b.connect(path)); + ASSERT_TRUE(insp.connect(path)); + + // A hellos with uid=0 (the bootstrap-conn case). B is a normal peer. + a.send_hello(SIM_MAGIC, ETH_PROTO_OANDISCO, 0); + b.send_hello(SIM_MAGIC, ETH_PROTO_OANDISCO, 99); + insp.send_hello(SIM_MAGIC, 0, 0xFFFE, SIM_HELLO_PROMISCUOUS); + std::this_thread::sleep_for(50ms); + + // A broadcasts a MAPPING packet announcing committed_uid=42. Match the + // wire layout DiscoveryPeek parses: [eth 14][LowLatHeader 6][OANPacket]. + constexpr uint16_t COMMITTED_UID = 42; + constexpr size_t PREFIX = 14 + 6; + std::vector payload(PREFIX + sizeof(OANPacket), 0); + OANPacket pck{}; + pck.header.type = PacketType::MAPPING; + pck.packet_data.self_uid = COMMITTED_UID; + std::memcpy(payload.data() + PREFIX, &pck, sizeof(pck)); + ASSERT_TRUE(a.send_frame(ETH_PROTO_OANDISCO, /*dest=*/0, payload)); + + // Inspector confirms the broadcast went out with src_uid=42, proving + // the conn's identity flipped from 0 to 42 in the switch's bookkeeping. + auto got = insp.read_frame_full(500); + ASSERT_TRUE(got.ok); + EXPECT_EQ(got.hdr.src_uid, COMMITTED_UID); + + // B unicasts disco to dest=42. This only routes if the switch added a + // (disco, 42) → A entry to the route table — the second half of adoption. + std::vector hello_a(8, 0xAB); + ASSERT_TRUE(b.send_frame(ETH_PROTO_OANDISCO, COMMITTED_UID, hello_a)); + auto from_b = a.read_frame_full(500); + ASSERT_TRUE(from_b.ok); + EXPECT_EQ(from_b.hdr.src_uid, 99); + EXPECT_EQ(from_b.hdr.dest_uid, COMMITTED_UID); + EXPECT_EQ(from_b.body, hello_a); +} + // ------ Filter parser unit tests ------------------------------------------ // These exercise the oaninspect filter expression parser as a pure unit, // no daemon/process involved.