diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..67ecad0 --- /dev/null +++ b/.clang-format @@ -0,0 +1,38 @@ +# Pinned style for OALS. Tuned to match what's already in the tree +# (4-space indent, K&R braces, ref/pointer attached to the variable, +# no hard column cap). Apply to new code via editor-on-save; do NOT +# mass-format existing files in a single sweep. +--- +BasedOnStyle: LLVM +Language: Cpp +Standard: c++20 + +IndentWidth: 4 +TabWidth: 4 +UseTab: Never +ColumnLimit: 100 + +BreakBeforeBraces: Attach +AllowShortFunctionsOnASingleLine: Empty +AllowShortIfStatementsOnASingleLine: Never +AllowShortLoopsOnASingleLine: false +SpaceAfterCStyleCast: false +PointerAlignment: Left +ReferenceAlignment: Left +DerivePointerAlignment: false + +AccessModifierOffset: -4 +NamespaceIndentation: None +FixNamespaceComments: true + +IncludeBlocks: Preserve +SortIncludes: false + +AlignAfterOpenBracket: Align +BinPackParameters: false +BinPackArguments: true +AllowAllParametersOfDeclarationOnNextLine: true +ConstructorInitializerAllOnOneLineOrOnePerLine: true + +EmptyLineBeforeAccessModifier: LogicalBlock +SeparateDefinitionBlocks: Leave diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..fd91b63 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,47 @@ +name: CI + +on: + push: + branches: ['**'] + pull_request: + branches: ['**'] + +jobs: + build: + name: ${{ matrix.os }} (host=${{ matrix.host_backends }}) + strategy: + fail-fast: false + matrix: + include: + # Linux: default Zephyr-firmware-shaped build (no host backends) is + # the path the firmware repos consume — keep it green. + - os: ubuntu-latest + host_backends: OFF + # Host-dev build path used by OALSEngine + sim_switch. + - os: ubuntu-latest + host_backends: ON + - os: macos-latest + host_backends: ON + runs-on: ${{ matrix.os }} + + steps: + - uses: actions/checkout@v4 + + - name: Install deps (Linux) + if: runner.os == 'Linux' + run: | + sudo apt-get update + sudo apt-get install -y --no-install-recommends cmake ninja-build + + - name: Install deps (macOS) + if: runner.os == 'macOS' + run: brew install cmake ninja + + - name: Configure + run: | + cmake -G Ninja -B build \ + -DCMAKE_BUILD_TYPE=RelWithDebInfo \ + -DOAN_HOST_BACKENDS=${{ matrix.host_backends }} + + - name: Build + run: cmake --build build diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..f3dc516 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,19 @@ +# pre-commit hooks for OpenAudioNetwork. Install once per clone: +# pip install pre-commit && pre-commit install +repos: + - repo: https://github.com/pre-commit/mirrors-clang-format + rev: v17.0.6 + hooks: + - id: clang-format + types_or: [c++, c] + exclude: '^build/' + + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-merge-conflict + - id: check-yaml + - id: check-added-large-files + args: ['--maxkb=512'] diff --git a/CMakeLists.txt b/CMakeLists.txt index 6d59585..5d7c8cd 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -7,5 +7,18 @@ if(NO_THREADS) add_compile_definitions(NO_THREADS) endif (NO_THREADS) +# OAN_HOST_BACKENDS: switches in the host-side transport seam (SimTransport, +# RawLinuxTransport, RawMacTransport). The define is normally injected by +# the parent OALS build; mirror the logic here so OAN is self-buildable +# (e.g. for CI of OAN-only changes). Forced ON on macOS — there is no +# other working transport on Apple at the moment. +option(OAN_HOST_BACKENDS "Build host dev transports + RT shim" OFF) +if(APPLE) + set(OAN_HOST_BACKENDS ON CACHE BOOL "Build host dev transports + RT shim" FORCE) +endif() +if(OAN_HOST_BACKENDS) + add_compile_definitions(OAN_HOST_BACKENDS) +endif() + add_subdirectory(common) add_subdirectory(netutils) \ No newline at end of file diff --git a/common/ClockMaster.h b/common/ClockMaster.h index 8dda503..482f24c 100644 --- a/common/ClockMaster.h +++ b/common/ClockMaster.h @@ -20,6 +20,12 @@ class ClockMaster { void begin_sync_process(); void sync_process(); + // Block until a sync packet arrives or timeout_ms elapses. + // Lets the clock thread on host backends stop busy-polling the async recv. + int wait_sync_readable(int timeout_ms) const { + return m_sync_socket->wait_readable(timeout_ms); + } + private: void start_clock_sync(PeerInfos& slave); void process_packet(ClockSyncPacket& csp, uint16_t originator); diff --git a/common/ClockSlave.h b/common/ClockSlave.h index da548f6..0b1fa23 100644 --- a/common/ClockSlave.h +++ b/common/ClockSlave.h @@ -18,6 +18,12 @@ class ClockSlave { void sync_process(); int64_t get_ck_offset(); + // Block until a sync packet arrives or timeout_ms elapses. Lets the + // slave's clock thread on host backends stop busy-polling the async recv. + int wait_sync_readable(int timeout_ms) const { + return m_sync_socket->wait_readable(timeout_ms); + } + private: void send_delay_req(uint16_t dest); void calc_ck_offset(); diff --git a/common/NetworkMapper.cpp b/common/NetworkMapper.cpp index fa91128..89d0e73 100644 --- a/common/NetworkMapper.cpp +++ b/common/NetworkMapper.cpp @@ -5,12 +5,15 @@ #include "NetworkMapper.h" -#ifndef __linux__ -extern void _delay(uint32_t ms); +#if defined(__linux__) || defined(OAN_HOST_BACKENDS) +#include +#endif + +#if !defined(__linux__) && !defined(OAN_HOST_BACKENDS) extern uint32_t _now_ms(); extern uint64_t _now_us(); extern "C" IfaceMeta _fetch_iface_meta(const std::string&); -#endif // __linux__ +#endif NetworkMapper::NetworkMapper(const PeerConf& pconf) { m_peer_change_callback = [](PeerInfos&, bool) {}; @@ -24,16 +27,36 @@ NetworkMapper::~NetworkMapper() { bool NetworkMapper::init_mapper(const std::string& iface) { m_map_socket = std::make_unique(m_packet.packet_data.self_uid, std::shared_ptr{}); bool res = m_map_socket->init_socket(iface, EthProtocol::ETH_PROTO_OANDISCO); + if (!res) return false; + +#ifdef OAN_HOST_BACKENDS + // On the host-backend path, the transport (sim/raw-mac) supplies the + // device MAC — there is no real interface to query. The MAC in + // m_packet was zero-filled by update_packet() at construction; fill it + // now from the socket whose transport just learned it. + m_packet.packet_data.self_address = 0; + memcpy(&m_packet.packet_data.self_address, m_map_socket->get_self_mac(), 6); +#endif - return res; + return true; } void NetworkMapper::update_packet(const PeerConf &pconf) { -#ifdef __linux__ +#if defined(__linux__) auto iface_meta = get_iface_meta(pconf.iface); +#elif defined(OAN_HOST_BACKENDS) + // Skip the iface lookup entirely: pconf.iface may be a transport prefix + // (sim:default, raw:en0) for which no real OS-level lookup makes sense. + IfaceMeta iface_meta{}; + // If we already have a socket (i.e. update_packet is being re-run after + // init_mapper), preserve the MAC the transport learned. Otherwise leave + // zero and let init_mapper fill it in once the socket is bound. + if (m_map_socket) { + memcpy(iface_meta.mac, m_map_socket->get_self_mac(), 6); + } #else auto iface_meta = _fetch_iface_meta(pconf.iface); -#endif // __linux__ +#endif m_packet.header.type = PacketType::MAPPING; m_packet.packet_data.topo = pconf.topo; @@ -113,11 +136,9 @@ void NetworkMapper::packet_send_update() { void NetworkMapper::packet_sender() { while(true) { packet_send_update(); -#ifdef __linux__ +#if defined(__linux__) || defined(OAN_HOST_BACKENDS) usleep(5000000); -#else - -#endif // __linux__ +#endif } } @@ -136,9 +157,9 @@ void NetworkMapper::mapper_process() { mapper_update(); } -#ifdef __linux__ +#if defined(__linux__) || defined(OAN_HOST_BACKENDS) usleep(1000000); -#endif // __linux__ +#endif } } @@ -244,7 +265,7 @@ std::optional NetworkMapper::first_free_processing_channel(uint16_t uid } uint64_t NetworkMapper::local_now() { -#if defined(__linux__) || defined(__ZEPHYR__) +#if defined(__linux__) || defined(__ZEPHYR__) || defined(OAN_HOST_BACKENDS) return std::chrono::duration_cast( std::chrono::high_resolution_clock::now().time_since_epoch() ).count(); @@ -254,7 +275,7 @@ uint64_t NetworkMapper::local_now() { } uint64_t NetworkMapper::local_now_us() { -#if defined(__linux__) || defined(__ZEPHYR__) +#if defined(__linux__) || defined(__ZEPHYR__) || defined(OAN_HOST_BACKENDS) return std::chrono::duration_cast( std::chrono::high_resolution_clock::now().time_since_epoch() ).count(); diff --git a/netutils/CMakeLists.txt b/netutils/CMakeLists.txt index 2837448..67524e2 100644 --- a/netutils/CMakeLists.txt +++ b/netutils/CMakeLists.txt @@ -5,10 +5,47 @@ set(OAN_UTILS_SOURCES LowLatSocket.h ) +if(NOT EMBEDDED_BUILD) + list(APPEND OAN_UTILS_SOURCES + platform/rt.h + platform/rt.cpp + ) +endif() + +if(OAN_HOST_BACKENDS) + list(APPEND OAN_UTILS_SOURCES + transport/ITransport.h + transport/ITransport.cpp + transport/SimTransport.h + transport/SimTransport.cpp + transport/RawMacTransport.h + transport/RawMacTransport.cpp + ) + if(NOT APPLE) + list(APPEND OAN_UTILS_SOURCES + transport/RawLinuxTransport.h + transport/RawLinuxTransport.cpp + ) + endif() +endif() + if(EMBEDDED_BUILD) add_library(oannetutils STATIC ${OAN_UTILS_SOURCES}) else () add_library(oannetutils SHARED ${OAN_UTILS_SOURCES}) endif (EMBEDDED_BUILD) -target_include_directories(oannetutils PUBLIC ${PROJECT_SOURCE_DIR}) \ No newline at end of file +target_include_directories(oannetutils PUBLIC ${PROJECT_SOURCE_DIR}) + +# oannetutils and oancommon have a mutual symbol dependency: +# LowLatSocket (in netutils) calls NetworkMapper::get_mac_by_uid +# (in common), and common's ClockMaster/ClockSlave/AudioRouter all +# instantiate LowLatSocket. CMake forbids cyclic target_link_libraries +# on shared libs, so we relax each library's own link step (the symbol +# is unresolved in the .so but always resolved at exe link time when +# both .so's appear on the line — see executables that link them). +if(APPLE) + target_link_options(oannetutils PRIVATE LINKER:-undefined,dynamic_lookup) +elseif(UNIX) + target_link_options(oannetutils PRIVATE LINKER:-z,undefs) +endif() \ No newline at end of file diff --git a/netutils/LowLatSocket.cpp b/netutils/LowLatSocket.cpp index 8e8a5a6..dc1f8b6 100644 --- a/netutils/LowLatSocket.cpp +++ b/netutils/LowLatSocket.cpp @@ -6,10 +6,18 @@ #include "LowLatSocket.h" #include "common/NetworkMapper.h" -#ifndef __linux__ +#include + +#ifdef OAN_HOST_BACKENDS +#include "transport/ITransport.h" +#endif + +#if !defined(__linux__) && !defined(OAN_HOST_BACKENDS) +// Zephyr-firmware-only hooks. Host builds use ITransport instead. extern "C" int _send_data(uint8_t* data, size_t data_len); extern "C" int _recv_data(uint8_t* data_out, size_t data_size, EthProtocol filt_proto); -#endif // __linux__ +extern "C" IfaceMeta _fetch_iface_meta(const std::string& name); +#endif std::optional LowLatSocket::get_mac(uint16_t id) { return m_mapper->get_mac_by_uid(id); @@ -25,10 +33,29 @@ LowLatSocket::LowLatSocket(uint16_t self_uid, std::shared_ptr map } LowLatSocket::~LowLatSocket() { - close(m_socket); + if (m_socket > 0) { + close(m_socket); + } } bool LowLatSocket::init_socket(std::string interface, EthProtocol proto) { +#ifdef OAN_HOST_BACKENDS + m_transport = parse_transport(interface); + if (m_transport) { + IfaceMeta meta{}; + if (!m_transport->open(interface, proto, m_self_uid, meta)) { + return false; + } + + memset(m_hdr.h_dest, 0xFF, 6); + memcpy(m_hdr.h_source, meta.mac, 6); + m_hdr.h_proto = htons(proto); + m_self_proto = proto; + return true; + } +#endif + + // Default Linux path: direct AF_PACKET (byte-for-byte unchanged from pre-M3). m_socket = socket(AF_PACKET, SOCK_RAW, htons(proto)); if (m_socket < 0) { std::cerr << "LLS Failed to open low level socket. Err = " << errno << std::endl; @@ -50,8 +77,7 @@ bool LowLatSocket::init_socket(std::string interface, EthProtocol proto) { return true; } - -IfaceMeta get_iface_meta(const std::string &name) { +IfaceMeta get_iface_meta(const std::string& name) { // Overflow check assert(name.size() <= 16); @@ -81,11 +107,98 @@ IfaceMeta get_iface_meta(const std::string &name) { return meta; } +int LowLatSocket::backend_send(const uint8_t* data, size_t size, uint16_t dest_uid) { +#ifdef OAN_HOST_BACKENDS + if (m_transport) { + return m_transport->send(data, size, dest_uid); + } #else -// Embeddable interface that must be private -extern "C" IfaceMeta _fetch_iface_meta(std::string& name); + (void)dest_uid; +#endif + + return sendto( + m_socket, + data, size, + MSG_DONTWAIT, + reinterpret_cast(&m_iface_addr), + sizeof(m_iface_addr) + ); +} + +int LowLatSocket::backend_recv(uint8_t* data, size_t size, bool async) const { +#ifdef OAN_HOST_BACKENDS + if (m_transport) { + return m_transport->recv(data, size, async); + } +#endif + + return recv(m_socket, data, size, async ? MSG_DONTWAIT : 0); +} + +int LowLatSocket::wait_readable(int timeout_ms) const { +#ifdef OAN_HOST_BACKENDS + if (m_transport) { + return m_transport->wait_readable(timeout_ms); + } +#endif + // Linux AF_PACKET path: keep tight-loop semantics from M3, no-op. + (void)timeout_ms; + return 1; +} + +#elif defined(OAN_HOST_BACKENDS) + +LowLatSocket::LowLatSocket(uint16_t self_uid, std::shared_ptr mapper) { + m_socket = 0; + memset(m_iface_addr, 0, sizeof(m_iface_addr)); + m_self_uid = self_uid; + m_mapper = std::move(mapper); +} + +LowLatSocket::~LowLatSocket() = default; + +bool LowLatSocket::init_socket(std::string interface, EthProtocol proto) { + m_transport = parse_transport(interface); + if (!m_transport) { + std::cerr << "LowLatSocket: this host requires a transport prefix " + << "(sim: or raw:) or OAN_TRANSPORT env var. " + << "Got: '" << interface << "'" << std::endl; + return false; + } -IfaceMeta get_iface_meta(std::string& name) { + IfaceMeta meta{}; + if (!m_transport->open(interface, proto, m_self_uid, meta)) { + return false; + } + + memcpy(m_iface_addr, meta.mac, 6); + + memset(m_hdr.h_dest, 0xFF, 6); + memcpy(m_hdr.h_source, meta.mac, 6); + m_hdr.h_proto = htons(proto); + m_self_proto = proto; + + return true; +} + +int LowLatSocket::backend_send(const uint8_t* data, size_t size, uint16_t dest_uid) { + if (!m_transport) return -1; + return m_transport->send(data, size, dest_uid); +} + +int LowLatSocket::backend_recv(uint8_t* data, size_t size, bool async) const { + if (!m_transport) return -1; + return m_transport->recv(data, size, async); +} + +int LowLatSocket::wait_readable(int timeout_ms) const { + if (!m_transport) return -1; + return m_transport->wait_readable(timeout_ms); +} + +#else // Zephyr firmware path — unchanged from pre-M3 + +IfaceMeta get_iface_meta(const std::string& name) { return _fetch_iface_meta(name); } @@ -111,12 +224,32 @@ bool LowLatSocket::init_socket(std::string interface, EthProtocol proto) { return true; } -int LowLatSocket::send_data_internal(uint8_t *data, size_t size) { +int LowLatSocket::backend_send(const uint8_t* data, size_t size, uint16_t dest_uid) { + (void)dest_uid; + // Zephyr's _send_data extern takes non-const uint8_t* for legacy ABI reasons; + // firmware impls (IO_Board_FW, RackedIO_Board_FW) do not mutate the buffer. + return send_data_internal(const_cast(data), size); +} + +int LowLatSocket::backend_recv(uint8_t* data, size_t size, bool async) const { + (void)async; + return recv_data_internal(data, size); +} + +int LowLatSocket::wait_readable(int timeout_ms) const { + // Zephyr firmware path: recv_data_internal is the blocking primitive, + // so we never busy-loop. Return readable immediately so any caller + // that pre-paces ends up calling recv right away. + (void)timeout_ms; + return 1; +} + +int LowLatSocket::send_data_internal(uint8_t* data, size_t size) { return _send_data(data, size); } -int LowLatSocket::recv_data_internal(uint8_t *data, size_t size) const { +int LowLatSocket::recv_data_internal(uint8_t* data, size_t size) const { return _recv_data(data, size, m_self_proto); } -#endif // __linux__ +#endif diff --git a/netutils/LowLatSocket.h b/netutils/LowLatSocket.h index 0196994..f3eafd9 100644 --- a/netutils/LowLatSocket.h +++ b/netutils/LowLatSocket.h @@ -34,7 +34,9 @@ struct EthernetHeader { } __attribute__((packed)); typedef EthernetHeader ethhdr; -#ifndef __ZEPHYR__ +#ifdef OAN_HOST_BACKENDS +#include +#elif !defined(__ZEPHYR__) #define htons(x) ((((x) & 0xFF00) >> 8) | (((x) & 0x00FF) << 8)) #else #include "zephyr/net/net_ip.h" @@ -150,17 +152,8 @@ class LowLatSocket { } } -#ifdef __linux__ - return sendto( - m_socket, - &llpck, sizeof(llpck), - MSG_DONTWAIT, - (sockaddr*)&m_iface_addr, - sizeof(m_iface_addr) - ); -#else - return send_data_internal((uint8_t*)&llpck, sizeof(INT_LLP)); -#endif // __linux__ + return backend_send(reinterpret_cast(&llpck), + sizeof(INT_LLP), dest_uid); } /** @@ -172,11 +165,7 @@ class LowLatSocket { */ template int receive_data(T* data, bool async = true) { -#ifdef __linux__ - return recv(m_socket, data, sizeof(T), async ? MSG_DONTWAIT : 0); -#else - return recv_data_internal((uint8_t*)data, sizeof(T)); -#endif // __linux__ + return backend_recv(reinterpret_cast(data), sizeof(T), async); } /** @@ -187,13 +176,18 @@ class LowLatSocket { * @return Received byte count */ int receive_data_raw(char* data, size_t size, bool async = true) const { -#ifdef __linux__ - return recv(m_socket, data, size, async ? MSG_DONTWAIT : 0); -#else - return recv_data_internal((uint8_t*)data, size); -#endif // __linux__ + return backend_recv(reinterpret_cast(data), size, async); } + const uint8_t* get_self_mac() const { return m_hdr.h_source; } + + // Block until this socket is readable or timeout_ms elapses. Lets RT + // loops on host backends pace themselves instead of spinning on + // EAGAIN. On Linux + AF_PACKET this is a no-op that returns 1 + // immediately so the existing tight-loop semantics are unchanged. + // Returns 1 readable / 0 timeout / -1 error. + int wait_readable(int timeout_ms) const; + private: /** * Finds a device MAC address based on its ID. @@ -202,10 +196,13 @@ class LowLatSocket { */ std::optional get_mac(uint16_t id); -#ifndef __linux__ + int backend_send(const uint8_t* data, size_t size, uint16_t dest_uid); + int backend_recv(uint8_t* data, size_t size, bool async) const; + +#if !defined(__linux__) && !defined(OAN_HOST_BACKENDS) int send_data_internal(uint8_t* data, size_t size); int recv_data_internal(uint8_t* data, size_t size) const; -#endif // __linux__ +#endif #ifdef __linux__ sockaddr_ll m_iface_addr{}; @@ -219,6 +216,10 @@ class LowLatSocket { EthProtocol m_self_proto; std::shared_ptr m_mapper; + +#ifdef OAN_HOST_BACKENDS + mutable std::unique_ptr m_transport; +#endif }; diff --git a/netutils/platform/rt.cpp b/netutils/platform/rt.cpp new file mode 100644 index 0000000..1312e5d --- /dev/null +++ b/netutils/platform/rt.cpp @@ -0,0 +1,179 @@ +// This file is part of the Open Audio Live System project, a live audio environment +// Copyright (c) 2026 - Mathis DELGADO +// +// This project is distributed under the Creative Commons CC-BY-NC-SA licence. https://creativecommons.org/licenses/by-nc-sa/4.0 + +#include "rt.h" + +#ifdef __linux__ + #include + #include + #include + #include +#elif defined(__APPLE__) + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include +#else + #include +#endif + +namespace oals::rt { + +#ifdef __linux__ + +void set_thread_realtime(uint8_t prio) { + sched_param sparams{}; + sparams.sched_priority = prio; + if (sched_setscheduler(0, SCHED_FIFO, &sparams) != 0) { + std::cerr << "[oals::rt] set_thread_realtime failed" << std::endl; + } +} + +void set_running_cpu(int cpu_id) { + cpu_set_t cs{}; + CPU_ZERO(&cs); + CPU_SET(cpu_id, &cs); + if (sched_setaffinity(0, sizeof(cpu_set_t), &cs) != 0) { + std::cerr << "[oals::rt] set_running_cpu failed" << std::endl; + } +} + +void set_process_scheduler_rr(int prio) { + sched_param params{}; + params.sched_priority = prio; + if (sched_setscheduler(0, SCHED_RR, ¶ms) != 0) { + std::cerr << "[oals::rt] set_process_scheduler_rr failed" << std::endl; + } +} + +void precise_sleep_ns(long ns) { + timespec ts{}; + ts.tv_sec = ns / 1'000'000'000L; + ts.tv_nsec = ns % 1'000'000'000L; + clock_nanosleep(CLOCK_MONOTONIC, 0, &ts, nullptr); +} + +#elif defined(__APPLE__) + +namespace { + +// Cached host timebase: mach absolute ticks -> nanoseconds is +// (ticks * numer / denom). thread_time_constraint_policy_data_t takes +// values in absolute mach ticks, so anything we compute in ns has to go +// through this. Init-once, lock-free read after. +mach_timebase_info_data_t s_tb_info = {}; +bool s_tb_init = false; + +void ensure_timebase() { + if (s_tb_init) return; + mach_timebase_info(&s_tb_info); + s_tb_init = true; +} + +uint32_t ns_to_mach_ticks(uint64_t ns) { + ensure_timebase(); + // ns * denom / numer — order matters to avoid overflow at audio-block + // scale (period ~667 µs, computation a fraction of that). + uint64_t ticks = (ns * s_tb_info.denom) / s_tb_info.numer; + if (ticks > UINT32_MAX) ticks = UINT32_MAX; + return static_cast(ticks); +} + +} // namespace + +// Pin the calling thread as a time-constraint thread. We map our Linux +// SCHED_FIFO priority (20..80) to a fraction of the audio block period: +// higher priority → larger computation budget within the same period. +// The audio block is 64 samples @ 96 kHz ≈ 667 µs; the pipe_updater +// (prio 80) and audio recv (prio 25) both want to finish well inside +// that. We pick: +// period = 667 µs (one audio block) +// computation = (prio / 100) * period, clamped to [50 µs, 500 µs] +// constraint = period (must complete within one period) +// preemptible = true (yield if we overrun — matches SCHED_FIFO behaviour) +// +// macOS scheduler treats this as "wake within `period`, give us up to +// `computation` of compute, must finish within `constraint`." Overrun +// doesn't kill us; the kernel just keeps running us at lower priority +// until we yield. That's the same shape as Linux SCHED_FIFO overrun. +// +// Refs: Apple TN2169, Chromium base/threading/platform_thread_mac.mm. +void set_thread_realtime(uint8_t prio) { + constexpr uint64_t k_audio_block_ns = 667'000; // 64 / 96000 * 1e9 ≈ 667 µs + + uint64_t computation_ns = + static_cast(k_audio_block_ns) * std::max(prio, 5) / 100; + if (computation_ns < 50'000) computation_ns = 50'000; // floor 50 µs + if (computation_ns > 500'000) computation_ns = 500'000; // ceil 500 µs (75% of period) + + thread_time_constraint_policy_data_t policy; + policy.period = ns_to_mach_ticks(k_audio_block_ns); + policy.computation = ns_to_mach_ticks(computation_ns); + policy.constraint = ns_to_mach_ticks(k_audio_block_ns); + policy.preemptible = TRUE; + + kern_return_t kr = thread_policy_set( + pthread_mach_thread_np(pthread_self()), + THREAD_TIME_CONSTRAINT_POLICY, + reinterpret_cast(&policy), + THREAD_TIME_CONSTRAINT_POLICY_COUNT); + + if (kr != KERN_SUCCESS) { + std::cerr << "[oals::rt] thread_policy_set(time_constraint) failed: " + << kr << std::endl; + } +} + +// thread_affinity_policy_set exists but is documented as a hint and is +// effectively a no-op on Apple Silicon (no P-core pinning). Skip — the +// scheduler does a better job placing time-constraint threads than we +// could anyway. +void set_running_cpu(int) {} + +// On Linux this sets the whole process to SCHED_RR. On Mac we use the +// hook as the natural one-time-per-process spot for mlockall: paging out +// the engine's RT stacks while a show is running is the worst failure +// mode there is. mlockall is per-process so this is idempotent if a +// caller invokes us more than once. +void set_process_scheduler_rr(int) { + static bool s_mlocked = false; + if (s_mlocked) return; + if (mlockall(MCL_CURRENT | MCL_FUTURE) != 0) { + std::cerr << "[oals::rt] mlockall failed (continuing): " + << strerror(errno) << std::endl; + } + s_mlocked = true; +} + +void precise_sleep_ns(long ns) { + timespec ts{}; + ts.tv_sec = ns / 1'000'000'000L; + ts.tv_nsec = ns % 1'000'000'000L; + nanosleep(&ts, nullptr); +} + +#else + +void set_thread_realtime(uint8_t) {} +void set_running_cpu(int) {} +void set_process_scheduler_rr(int){} + +void precise_sleep_ns(long ns) { + timespec ts{}; + ts.tv_sec = ns / 1'000'000'000L; + ts.tv_nsec = ns % 1'000'000'000L; + nanosleep(&ts, nullptr); +} + +#endif + +} diff --git a/netutils/platform/rt.h b/netutils/platform/rt.h new file mode 100644 index 0000000..6fbc3c6 --- /dev/null +++ b/netutils/platform/rt.h @@ -0,0 +1,18 @@ +// This file is part of the Open Audio Live System project, a live audio environment +// Copyright (c) 2026 - Mathis DELGADO +// +// This project is distributed under the Creative Commons CC-BY-NC-SA licence. https://creativecommons.org/licenses/by-nc-sa/4.0 + +#ifndef OALS_RT_H +#define OALS_RT_H + +#include + +namespace oals::rt { + void set_thread_realtime(uint8_t prio); + void set_running_cpu(int cpu_id); + void set_process_scheduler_rr(int prio); + void precise_sleep_ns(long ns); +} + +#endif diff --git a/netutils/transport/ITransport.cpp b/netutils/transport/ITransport.cpp new file mode 100644 index 0000000..3e525c2 --- /dev/null +++ b/netutils/transport/ITransport.cpp @@ -0,0 +1,119 @@ +#include "ITransport.h" + +#include +#include +#include +#include + +#include "../LowLatSocket.h" + +#include "SimTransport.h" +#include "RawMacTransport.h" +#ifdef __linux__ + #include "RawLinuxTransport.h" +#endif + +#ifdef __linux__ + #include + #include + #include + #include +#else + #include + #include + #include +#endif + +IfaceMeta host_iface_meta(const std::string& iface) { + IfaceMeta meta{}; + +#ifdef __linux__ + assert(iface.size() <= 16); + + int sock = ::socket(AF_INET, SOCK_DGRAM, 0); + if (sock < 0) { + perror("host_iface_meta: socket()"); + return meta; + } + + ifreq req{}; + memset(req.ifr_ifrn.ifrn_name, 0x00, 16); + memcpy(req.ifr_ifrn.ifrn_name, iface.data(), iface.size()); + + if (ioctl(sock, SIOCGIFINDEX, &req) < 0) { + perror("host_iface_meta: SIOCGIFINDEX"); + } + meta.idx = req.ifr_ifru.ifru_ivalue; + + if (ioctl(sock, SIOCGIFHWADDR, &req) < 0) { + perror("host_iface_meta: SIOCGIFHWADDR"); + } + memcpy(meta.mac, req.ifr_ifru.ifru_hwaddr.sa_data, 6); + + ::close(sock); +#else + ifaddrs* addrs = nullptr; + if (getifaddrs(&addrs) != 0) { + perror("host_iface_meta: getifaddrs"); + return meta; + } + + bool found = false; + for (ifaddrs* ifa = addrs; ifa != nullptr; ifa = ifa->ifa_next) { + if (ifa->ifa_addr == nullptr) continue; + if (ifa->ifa_addr->sa_family != AF_LINK) continue; + if (iface != ifa->ifa_name) continue; + + auto* sdl = reinterpret_cast(ifa->ifa_addr); + if (sdl->sdl_alen == 6) { + memcpy(meta.mac, LLADDR(sdl), 6); + meta.idx = static_cast(if_nametoindex(iface.c_str())); + found = true; + break; + } + } + + freeifaddrs(addrs); + + if (!found) { + std::cerr << "host_iface_meta: no AF_LINK entry for interface '" + << iface << "'" << std::endl; + } +#endif + + return meta; +} + +static bool starts_with(const std::string& s, const std::string& prefix) { + return s.size() >= prefix.size() && s.compare(0, prefix.size(), prefix) == 0; +} + +std::unique_ptr parse_transport(const std::string& iface) { + if (starts_with(iface, "sim:")) { + return std::make_unique(iface.substr(4)); + } + if (starts_with(iface, "raw:")) { +#ifdef __linux__ + return std::make_unique(); +#else + return std::make_unique(); +#endif + } + + const char* env = std::getenv("OAN_TRANSPORT"); + if (env != nullptr) { + std::string sel{env}; + if (sel == "sim") { + return std::make_unique(iface.empty() ? "default" : iface); + } + if (sel == "raw") { +#ifdef __linux__ + return std::make_unique(); +#else + return std::make_unique(); +#endif + } + } + + return nullptr; +} diff --git a/netutils/transport/ITransport.h b/netutils/transport/ITransport.h new file mode 100644 index 0000000..8f8d0a9 --- /dev/null +++ b/netutils/transport/ITransport.h @@ -0,0 +1,34 @@ +#ifndef OAN_ITRANSPORT_H +#define OAN_ITRANSPORT_H + +#include +#include +#include +#include + +struct IfaceMeta; +enum EthProtocol : uint16_t; + +struct ITransport { + virtual ~ITransport() = default; + + virtual bool open(const std::string& iface, EthProtocol proto, + uint16_t self_uid, IfaceMeta& out_meta) = 0; + virtual int send(const uint8_t* data, size_t len, uint16_t dest_uid) = 0; + virtual int recv(uint8_t* data, size_t len, bool async) = 0; + virtual void close() = 0; + + // Block until the underlying fd is readable or timeout_ms elapses. + // Returns 1 on readable, 0 on timeout, -1 on error. Lets call sites + // pace busy-loops on host backends without touching recv(). Default + // impl returns 1 immediately so the Linux RT path (RawLinuxTransport) + // keeps its existing tight-loop semantics — only host-backend + // implementations override. + virtual int wait_readable(int timeout_ms) { (void)timeout_ms; return 1; } +}; + +IfaceMeta host_iface_meta(const std::string& iface); + +std::unique_ptr parse_transport(const std::string& iface); + +#endif diff --git a/netutils/transport/RawLinuxTransport.cpp b/netutils/transport/RawLinuxTransport.cpp new file mode 100644 index 0000000..072e253 --- /dev/null +++ b/netutils/transport/RawLinuxTransport.cpp @@ -0,0 +1,60 @@ +#include "RawLinuxTransport.h" + +#ifdef __linux__ + +#include +#include +#include + +#include +#include +#include +#include + +RawLinuxTransport::~RawLinuxTransport() { + close(); +} + +bool RawLinuxTransport::open(const std::string& iface, EthProtocol proto, + uint16_t self_uid, IfaceMeta& out_meta) { + (void)self_uid; + + m_socket = ::socket(AF_PACKET, SOCK_RAW, htons(proto)); + if (m_socket < 0) { + std::cerr << "RawLinuxTransport: socket() failed, errno=" << errno << std::endl; + return false; + } + + out_meta = host_iface_meta(iface); + + m_iface_addr.sll_ifindex = out_meta.idx; + m_iface_addr.sll_halen = ETH_ALEN; + m_iface_addr.sll_protocol = htons(proto); + memcpy(m_iface_addr.sll_addr, out_meta.mac, 6); + + return true; +} + +int RawLinuxTransport::send(const uint8_t* data, size_t len, uint16_t dest_uid) { + (void)dest_uid; + return ::sendto( + m_socket, + data, len, + MSG_DONTWAIT, + reinterpret_cast(&m_iface_addr), + sizeof(m_iface_addr) + ); +} + +int RawLinuxTransport::recv(uint8_t* data, size_t len, bool async) { + return ::recv(m_socket, data, len, async ? MSG_DONTWAIT : 0); +} + +void RawLinuxTransport::close() { + if (m_socket >= 0) { + ::close(m_socket); + m_socket = -1; + } +} + +#endif // __linux__ diff --git a/netutils/transport/RawLinuxTransport.h b/netutils/transport/RawLinuxTransport.h new file mode 100644 index 0000000..fcf119a --- /dev/null +++ b/netutils/transport/RawLinuxTransport.h @@ -0,0 +1,29 @@ +#ifndef OAN_RAW_LINUX_TRANSPORT_H +#define OAN_RAW_LINUX_TRANSPORT_H + +#ifdef __linux__ + +#include + +#include "ITransport.h" +#include "../LowLatSocket.h" + +class RawLinuxTransport : public ITransport { +public: + RawLinuxTransport() = default; + ~RawLinuxTransport() override; + + bool open(const std::string& iface, EthProtocol proto, + uint16_t self_uid, IfaceMeta& out_meta) override; + int send(const uint8_t* data, size_t len, uint16_t dest_uid) override; + int recv(uint8_t* data, size_t len, bool async) override; + void close() override; + +private: + int m_socket{-1}; + sockaddr_ll m_iface_addr{}; +}; + +#endif // __linux__ + +#endif diff --git a/netutils/transport/RawMacTransport.cpp b/netutils/transport/RawMacTransport.cpp new file mode 100644 index 0000000..080f409 --- /dev/null +++ b/netutils/transport/RawMacTransport.cpp @@ -0,0 +1,27 @@ +// This file is part of the Open Audio Live System project, a live audio environment +// Copyright (c) 2026 - Mathis DELGADO +// +// This project is distributed under the Creative Commons CC-BY-NC-SA licence. https://creativecommons.org/licenses/by-nc-sa/4.0 + +#include "RawMacTransport.h" + +#include + +bool RawMacTransport::open(const std::string& iface, EthProtocol proto, + uint16_t self_uid, IfaceMeta& out_meta) { + (void)iface; (void)proto; (void)self_uid; (void)out_meta; + std::cerr << "RawMacTransport: BPF not implemented (Track 2)" << std::endl; + return false; +} + +int RawMacTransport::send(const uint8_t* data, size_t len, uint16_t dest_uid) { + (void)data; (void)len; (void)dest_uid; + return -1; +} + +int RawMacTransport::recv(uint8_t* data, size_t len, bool async) { + (void)data; (void)len; (void)async; + return -1; +} + +void RawMacTransport::close() {} diff --git a/netutils/transport/RawMacTransport.h b/netutils/transport/RawMacTransport.h new file mode 100644 index 0000000..f81fa1e --- /dev/null +++ b/netutils/transport/RawMacTransport.h @@ -0,0 +1,18 @@ +#ifndef OAN_RAW_MAC_TRANSPORT_H +#define OAN_RAW_MAC_TRANSPORT_H + +#include "ITransport.h" + +class RawMacTransport : public ITransport { +public: + RawMacTransport() = default; + ~RawMacTransport() override = default; + + bool open(const std::string& iface, EthProtocol proto, + uint16_t self_uid, IfaceMeta& out_meta) override; + int send(const uint8_t* data, size_t len, uint16_t dest_uid) override; + int recv(uint8_t* data, size_t len, bool async) override; + void close() override; +}; + +#endif diff --git a/netutils/transport/SimTransport.cpp b/netutils/transport/SimTransport.cpp new file mode 100644 index 0000000..77c18d6 --- /dev/null +++ b/netutils/transport/SimTransport.cpp @@ -0,0 +1,346 @@ +#include "SimTransport.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +// NOTE: wire framing is owned by the OALS sim_switch dev tool (which is the +// only program SimTransport can talk to). We deliberately do NOT include a +// header from OALS — OAN is vendored into firmware repos and must stay +// self-contained. The structs below are a local copy that must stay +// byte-identical with tools/sim_switch/sim_proto.h. The framing magic and +// version are the contract check. + +namespace { + +constexpr uint32_t SIM_MAGIC = 0x4F535354; // 'OSST' +constexpr uint8_t SIM_VERSION = 2; +constexpr uint32_t MAX_FRAME_PAYLOAD = 8192; // matches sim_switch + +struct SimHello { + uint32_t magic; + uint8_t version; + uint8_t _pad; + uint16_t ethertype; + uint16_t self_uid; + uint16_t flags; // SIM_HELLO_PROMISCUOUS etc. — engine never sets any. +} __attribute__((packed)); + +struct SimFrame { + uint32_t payload_len; + uint16_t ethertype; + uint16_t dest_uid; + uint16_t src_uid; // switch overwrites; sender writes 0 + uint16_t _pad; +} __attribute__((packed)); + +bool parse_mac(const std::string& s, uint8_t out[6]) { + unsigned int v[6]; + if (std::sscanf(s.c_str(), + "%x:%x:%x:%x:%x:%x", + &v[0], &v[1], &v[2], &v[3], &v[4], &v[5]) != 6) { + return false; + } + for (int i = 0; i < 6; ++i) { + if (v[i] > 0xFF) return false; + out[i] = static_cast(v[i]); + } + return true; +} + +void random_locally_admin_mac(uint8_t out[6]) { + std::random_device rd; + std::mt19937 gen(rd()); + std::uniform_int_distribution d(0, 255); + out[0] = 0x02; // locally administered, unicast + for (int i = 1; i < 6; ++i) out[i] = static_cast(d(gen)); +} + +// One MAC per process. The engine constructs 4 SimTransports (audio, disco, +// control, sync) — they must all carry the same MAC so peers see a single +// device identity. OAN_SIM_MAC env var can override; otherwise a random +// locally-administered MAC is minted once and reused across all instances. +const uint8_t* process_self_mac() { + static uint8_t s_mac[6]; + static bool s_init = false; + if (s_init) return s_mac; + + const char* env = std::getenv("OAN_SIM_MAC"); + if (env) { + if (parse_mac(env, s_mac)) { + s_init = true; + return s_mac; + } + std::cerr << "SimTransport: ignoring malformed OAN_SIM_MAC='" + << env << "' (expected 02:00:00:00:00:01)\n"; + } + random_locally_admin_mac(s_mac); + s_init = true; + return s_mac; +} + +} // namespace + +SimTransport::SimTransport(std::string s) { + parse_opts(s); + m_socket_path = "/tmp/osst-sim-" + m_daemon_name + ".sock"; + if (!m_mac_pinned) std::memcpy(m_self_mac, process_self_mac(), 6); +} + +SimTransport::~SimTransport() { + close(); +} + +void SimTransport::parse_opts(const std::string& s) { + // Split on commas: first token = daemon name, subsequent tokens = "key=value". + size_t start = 0; + int tok_idx = 0; + while (start <= s.size()) { + size_t comma = s.find(',', start); + std::string tok = s.substr(start, comma == std::string::npos ? std::string::npos : comma - start); + + if (tok_idx == 0) { + m_daemon_name = tok.empty() ? "default" : tok; + } else { + auto eq = tok.find('='); + if (eq != std::string::npos) { + std::string key = tok.substr(0, eq); + std::string val = tok.substr(eq + 1); + if (key == "mac") { + if (parse_mac(val, m_self_mac)) { + m_mac_pinned = true; + } else { + std::cerr << "SimTransport: ignoring malformed mac='" + << val << "' (expected 02:00:00:00:00:01)\n"; + } + } else { + std::cerr << "SimTransport: ignoring unknown option '" + << key << "'\n"; + } + } + } + + if (comma == std::string::npos) break; + start = comma + 1; + tok_idx++; + } + + if (m_daemon_name.empty()) m_daemon_name = "default"; +} + +bool SimTransport::open(const std::string& iface, EthProtocol proto, + uint16_t self_uid, IfaceMeta& out_meta) { + (void)iface; // already parsed in ctor + + m_fd = ::socket(AF_UNIX, SOCK_STREAM, 0); + if (m_fd < 0) { + std::cerr << "SimTransport: socket() failed: " << ::strerror(errno) << "\n"; + return false; + } + + sockaddr_un addr{}; + addr.sun_family = AF_UNIX; + if (m_socket_path.size() >= sizeof(addr.sun_path)) { + std::cerr << "SimTransport: socket path too long\n"; + ::close(m_fd); m_fd = -1; + return false; + } + std::strncpy(addr.sun_path, m_socket_path.c_str(), sizeof(addr.sun_path) - 1); + + if (::connect(m_fd, reinterpret_cast(&addr), sizeof(addr)) < 0) { + std::cerr << "SimTransport: connect to " << m_socket_path + << " failed (is sim_switch running?): " + << ::strerror(errno) << "\n"; + ::close(m_fd); m_fd = -1; + return false; + } + + SimHello h{ + SIM_MAGIC, + SIM_VERSION, + 0, + static_cast(proto), + self_uid, + 0 // flags — engine never sets PROMISCUOUS + }; + ssize_t hn = ::send(m_fd, &h, sizeof(h), 0); + if (hn != static_cast(sizeof(h))) { + std::cerr << "SimTransport: hello send failed: " << ::strerror(errno) << "\n"; + ::close(m_fd); m_fd = -1; + return false; + } + + int flags = ::fcntl(m_fd, F_GETFL, 0); + if (flags < 0 || ::fcntl(m_fd, F_SETFL, flags | O_NONBLOCK) < 0) { + std::cerr << "SimTransport: fcntl(O_NONBLOCK) failed: " + << ::strerror(errno) << "\n"; + ::close(m_fd); m_fd = -1; + return false; + } + + // SIGPIPE protection: macOS has no MSG_NOSIGNAL, so if the daemon dies + // mid-stream our next sendmsg would deliver SIGPIPE and kill the engine. + // Ignoring it process-wide is a library-side side effect, but it's the + // only portable knob — engines that need EPIPE behaviour explicitly + // were going to die from SIGPIPE anyway. Idempotent + harmless on Linux. + static bool s_sigpipe_ignored = false; + if (!s_sigpipe_ignored) { + std::signal(SIGPIPE, SIG_IGN); + s_sigpipe_ignored = true; + } + + // Pre-reserve the recv body buffer to the largest legal frame size so the + // audio hot path (recv at ~750 Hz/channel) never triggers a reallocation. + m_body_buf.reserve(MAX_FRAME_PAYLOAD); + + m_proto = proto; + m_self_uid = self_uid; + std::memcpy(out_meta.mac, m_self_mac, 6); + out_meta.idx = 0; + return true; +} + +int SimTransport::send(const uint8_t* data, size_t len, uint16_t dest_uid) { + if (m_fd < 0) return -1; + + SimFrame hdr{ + static_cast(len), + static_cast(m_proto), + dest_uid, + 0, // src_uid: switch populates from our hello + 0 + }; + + iovec iov[2] = { + { &hdr, sizeof(hdr) }, + { const_cast(data), len } + }; + msghdr m{}; + m.msg_iov = iov; + m.msg_iovlen = 2; + + ssize_t n; + do { + n = ::sendmsg(m_fd, &m, MSG_DONTWAIT +#ifdef __linux__ + | MSG_NOSIGNAL +#endif + ); + } while (n < 0 && errno == EINTR); + + if (n < 0) { + if (errno == EAGAIN || errno == EWOULDBLOCK) return 0; + return -1; + } + return static_cast(len); // caller cares about payload bytes +} + +int SimTransport::recv(uint8_t* data, size_t len, bool async) { + if (m_fd < 0) return -1; + + while (true) { + // Phase 1: read SimFrame header. + if (!m_have_hdr) { + while (m_hdr_read < sizeof(m_hdr_buf)) { + ssize_t n = ::read(m_fd, m_hdr_buf + m_hdr_read, + sizeof(m_hdr_buf) - m_hdr_read); + if (n > 0) { m_hdr_read += n; continue; } + if (n == 0) return -1; // peer closed + if (errno == EINTR) continue; + if (errno == EAGAIN || errno == EWOULDBLOCK) { + if (async) return 0; + // Blocking mode: wait for more data. + pollfd p{m_fd, POLLIN, 0}; + int pr = ::poll(&p, 1, -1); + if (pr < 0 && errno != EINTR) return -1; + continue; + } + return -1; + } + SimFrame hdr{}; + std::memcpy(&hdr, m_hdr_buf, sizeof(hdr)); + if (hdr.payload_len > MAX_FRAME_PAYLOAD) { + std::cerr << "SimTransport: oversize frame len=" + << hdr.payload_len << "; disconnecting\n"; + return -1; + } + m_body_expected = hdr.payload_len; + // resize() value-initialises, but we always overwrite via read() + // before reading m_body_buf[i], so the zero-fill is wasted work. + // Since reserve(MAX_FRAME_PAYLOAD) ran in open(), no allocation + // happens here — only setting size(). + m_body_buf.resize(m_body_expected); + m_body_read = 0; + m_have_hdr = true; + } + + // Phase 2: read body. + while (m_body_read < m_body_expected) { + ssize_t n = ::read(m_fd, m_body_buf.data() + m_body_read, + m_body_expected - m_body_read); + if (n > 0) { m_body_read += n; continue; } + if (n == 0) return -1; + if (errno == EINTR) continue; + if (errno == EAGAIN || errno == EWOULDBLOCK) { + if (async) return 0; + pollfd p{m_fd, POLLIN, 0}; + int pr = ::poll(&p, 1, -1); + if (pr < 0 && errno != EINTR) return -1; + continue; + } + return -1; + } + + // Phase 3: deliver. + size_t out_len = std::min(len, m_body_expected); + if (m_body_expected > len) { + // Caller's buffer was smaller than the payload. This matches + // AF_PACKET's truncation semantics but is almost certainly a sizing + // bug — log once per frame to aid debugging without spamming. + std::cerr << "SimTransport: recv truncated frame (" + << m_body_expected << " → " << len << " bytes)\n"; + } + std::memcpy(data, m_body_buf.data(), out_len); + + // Reset state for next frame. Keep m_body_buf capacity intact + // (resize(0) preserves capacity; clear() does too, but resize is the + // explicit "shrink length, retain storage" idiom). + m_have_hdr = false; + m_hdr_read = 0; + m_body_read = 0; + m_body_expected = 0; + m_body_buf.resize(0); + + return static_cast(out_len); + } +} + +int SimTransport::wait_readable(int timeout_ms) { + if (m_fd < 0) return -1; + pollfd p{m_fd, POLLIN, 0}; + int r = ::poll(&p, 1, timeout_ms); + if (r < 0) { + if (errno == EINTR) return 0; // treat as timeout — caller will retry + return -1; + } + return r; // 0 = timeout, 1 = readable +} + +void SimTransport::close() { + if (m_fd >= 0) { + ::close(m_fd); + m_fd = -1; + } +} diff --git a/netutils/transport/SimTransport.h b/netutils/transport/SimTransport.h new file mode 100644 index 0000000..5032470 --- /dev/null +++ b/netutils/transport/SimTransport.h @@ -0,0 +1,46 @@ +#ifndef OAN_SIM_TRANSPORT_H +#define OAN_SIM_TRANSPORT_H + +#include +#include +#include + +#include "ITransport.h" +#include "../LowLatSocket.h" // EthProtocol + +class SimTransport : public ITransport { +public: + // Argument is the post-"sim:" string from parse_transport — either + // "" or ",mac=02:00:00:00:00:01". + // Empty string defaults to daemon name "default". + explicit SimTransport(std::string daemon_name_with_opts); + ~SimTransport() override; + + bool open(const std::string& iface, EthProtocol proto, + uint16_t self_uid, IfaceMeta& out_meta) override; + int send(const uint8_t* data, size_t len, uint16_t dest_uid) override; + int recv(uint8_t* data, size_t len, bool async) override; + int wait_readable(int timeout_ms) override; + void close() override; + +private: + void parse_opts(const std::string& s); + + std::string m_daemon_name; + std::string m_socket_path; + uint8_t m_self_mac[6]{}; + bool m_mac_pinned{false}; + + int m_fd{-1}; + EthProtocol m_proto{}; + uint16_t m_self_uid{0}; + + bool m_have_hdr{false}; + uint8_t m_hdr_buf[12]{}; // sizeof(SimFrame) == 12 (v2) + size_t m_hdr_read{0}; + std::vector m_body_buf; + size_t m_body_read{0}; + uint32_t m_body_expected{0}; +}; + +#endif