// SPDX-License-Identifier: MIT // SPDX-FileCopyrightText: 2026 Martino Ferrari #pragma once #include #include #include #include #include #include namespace udpscope::e1608 { constexpr int kDefaultPort = 54211; constexpr std::uint16_t kProductId = 0x012F; /// Every failure of the link. `Rejected` (a bad connection code) is the only one a retry cannot /// fix. `Command` is a reply the board answered with a nonzero status, or a reply of the wrong /// length: the link itself is fine. `Protocol` is a framing error (sync, checksum, id). class LinkError : public std::runtime_error { public: enum class Kind { Io, Timeout, Rejected, Busy, Protocol, Command }; LinkError(Kind kind, const std::string& what) : std::runtime_error(what), kind_(kind) {} Kind kind() const noexcept { return kind_; } private: Kind kind_; }; /// An owned file descriptor. class Fd { public: Fd() = default; explicit Fd(int fd) : fd_(fd) {} Fd(const Fd&) = delete; Fd& operator=(const Fd&) = delete; Fd(Fd&& o) noexcept : fd_(o.release()) {} Fd& operator=(Fd&& o) noexcept { if (this != &o) reset(o.release()); return *this; } ~Fd() { reset(); } int get() const noexcept { return fd_; } bool valid() const noexcept { return fd_ >= 0; } int release() noexcept { const int f = fd_; fd_ = -1; return f; } /// Closes the current descriptor (if any) and takes `fd`. void reset(int fd = -1) noexcept; private: int fd_ = -1; }; using Mac = std::array; /// Six hex bytes separated by `:` or `-`. False (and `out` untouched) on anything else. bool parse_mac(const std::string& text, Mac& out); /// Lower-case `aa:bb:cc:dd:ee:ff`. std::string format_mac(const Mac& mac); struct DeviceInfo { std::string ip; Mac mac{}; std::uint16_t product_id = 0; }; /// Where to send a discovery `D`: an empty subnet is the limited broadcast; `a.b.c` (an optional /// trailing dot) is a unicast to hosts .1 to .254; anything else is empty. std::vector sweep_targets(const std::string& subnet); /// Sends `D` to every target, then collects replies for `wait_ms`. Keeps E-1608s only (product id /// `kProductId`) and one entry per MAC. std::vector discover(const std::vector& targets, int port, int wait_ms); /// One board: the UDP handshake, the TCP command channel, and the scan socket (`port + 1`). /// Not thread safe; the source's producer thread owns it. class Link { public: /// Handshake, then the command connection. Throws `LinkError` (`Rejected` for a bad code). void connect(const std::string& host, int port, std::uint32_t code, int timeout_ms); /// Closes the command and scan sockets. Safe to call at any time. void close(); bool is_open() const noexcept { return cmd_.valid(); } /// One framed exchange. `expect >= 0` demands a reply payload of exactly that length. Throws /// `Command` for a nonzero status or a length mismatch, `Protocol` for a bad frame. std::vector command(std::uint8_t cmd, const std::vector& payload = {}, int expect = -1); /// Opens the scan stream socket (a 1 MiB receive buffer, set before connecting). void open_scan(); void close_scan(); bool scan_open() const noexcept { return scan_.valid(); } /// The single `0x00` byte on the command socket that starts the stream after scan start. void send_filler(); /// Waits up to `timeout_ms` for scan bytes and reads what is there (at most `cap`). Returns 0 on /// a timeout. Throws `Io` when the board closed the socket. std::size_t read_scan(std::uint8_t* buf, std::size_t cap, int timeout_ms); private: Fd cmd_; Fd scan_; std::string host_; int port_ = kDefaultPort; int timeout_ms_ = 2000; std::uint8_t next_id_ = 0; }; } // namespace udpscope::e1608