Files
udpscope/src/source/e1608/e1608_link.hpp
T

114 lines
3.8 KiB
C++

// SPDX-License-Identifier: MIT
// SPDX-FileCopyrightText: 2026 Martino Ferrari
#pragma once
#include <array>
#include <cstddef>
#include <cstdint>
#include <stdexcept>
#include <string>
#include <vector>
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<std::uint8_t, 6>;
/// 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<std::string> 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<DeviceInfo> discover(const std::vector<std::string>& 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<std::uint8_t> command(std::uint8_t cmd, const std::vector<std::uint8_t>& 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