Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
114 lines
3.8 KiB
C++
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
|