e1608: link and device layers (discovery, handshake, framed commands, calibration, decode)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
558d13d521
commit
5ac1b36a1f
5 files changed
+766
No files matched your search
@@ -0,0 +1,113 @@
|
||||
// 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
|
||||
Reference in new issue
Block a user