|
|
// led/Bx6k1YyLedClient.cxx
|
|
|
#include "Bx6k1YyLedClient.hpp"
|
|
|
|
|
|
#include "BX6K1YYProtocol.hpp"
|
|
|
|
|
|
#include <arpa/inet.h>
|
|
|
#include <cerrno>
|
|
|
#include <chrono>
|
|
|
#include <cstdio>
|
|
|
#include <cstring>
|
|
|
#include <fcntl.h>
|
|
|
#include <iconv.h>
|
|
|
#include <netinet/in.h>
|
|
|
#include <netinet/tcp.h>
|
|
|
#include <poll.h>
|
|
|
#include <sys/socket.h>
|
|
|
#include <sys/types.h>
|
|
|
#include <unistd.h>
|
|
|
|
|
|
#include <algorithm>
|
|
|
#include <vector>
|
|
|
|
|
|
namespace led {
|
|
|
|
|
|
namespace {
|
|
|
|
|
|
bool encodeUtf8(const std::string& in, const std::string& encoding, std::string& bytes) {
|
|
|
if (encoding.empty() || encoding == "UTF-8" || encoding == "utf-8") {
|
|
|
bytes = in;
|
|
|
return true;
|
|
|
}
|
|
|
iconv_t cd = iconv_open(encoding.c_str(), "UTF-8//IGNORE");
|
|
|
if (cd == reinterpret_cast<iconv_t>(-1)) return false;
|
|
|
|
|
|
size_t inLeft = in.size();
|
|
|
size_t outLeft = in.size() * 4 + 16;
|
|
|
std::vector<char> out(outLeft);
|
|
|
char* inBuf = const_cast<char*>(in.data());
|
|
|
char* outBuf = out.data();
|
|
|
|
|
|
size_t res = iconv(cd, &inBuf, &inLeft, &outBuf, &outLeft);
|
|
|
iconv_close(cd);
|
|
|
if (res == static_cast<size_t>(-1)) return false;
|
|
|
bytes.assign(out.data(), out.size() - outLeft);
|
|
|
return true;
|
|
|
}
|
|
|
|
|
|
bool setNonBlocking(int fd, bool nb) {
|
|
|
int flags = fcntl(fd, F_GETFL, 0);
|
|
|
if (flags < 0) return false;
|
|
|
return fcntl(fd, F_SETFL, nb ? (flags | O_NONBLOCK) : (flags & ~O_NONBLOCK)) == 0;
|
|
|
}
|
|
|
|
|
|
bool connectWithTimeout(int fd, const sockaddr_in& addr, int timeoutMs) {
|
|
|
int rc = ::connect(fd, reinterpret_cast<const sockaddr*>(&addr), sizeof(addr));
|
|
|
if (rc == 0) return true;
|
|
|
if (errno != EINPROGRESS) return false;
|
|
|
pollfd pfd{fd, POLLOUT, 0};
|
|
|
if (::poll(&pfd, 1, timeoutMs) <= 0) return false;
|
|
|
int err = 0; socklen_t len = sizeof(err);
|
|
|
if (getsockopt(fd, SOL_SOCKET, SO_ERROR, &err, &len) < 0) return false;
|
|
|
return err == 0;
|
|
|
}
|
|
|
|
|
|
bool sendAll(int fd, const uint8_t* data, size_t len, int timeoutMs) {
|
|
|
size_t sent = 0;
|
|
|
while (sent < len) {
|
|
|
pollfd pfd{fd, POLLOUT, 0};
|
|
|
if (::poll(&pfd, 1, timeoutMs) <= 0) return false;
|
|
|
ssize_t n = ::send(fd, data + sent, len - sent, MSG_NOSIGNAL);
|
|
|
if (n <= 0) return false;
|
|
|
sent += static_cast<size_t>(n);
|
|
|
}
|
|
|
return true;
|
|
|
}
|
|
|
|
|
|
// 收到至少一个 0x5A 字节或超时
|
|
|
bool recvUntilTail(int fd, std::vector<uint8_t>& buf, int timeoutMs) {
|
|
|
uint8_t chunk[1024];
|
|
|
auto deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(timeoutMs);
|
|
|
while (true) {
|
|
|
auto now = std::chrono::steady_clock::now();
|
|
|
if (now >= deadline) return !buf.empty();
|
|
|
int wait = static_cast<int>(
|
|
|
std::chrono::duration_cast<std::chrono::milliseconds>(deadline - now).count());
|
|
|
pollfd pfd{fd, POLLIN, 0};
|
|
|
if (::poll(&pfd, 1, wait) <= 0) return !buf.empty();
|
|
|
ssize_t n = ::recv(fd, chunk, sizeof(chunk), 0);
|
|
|
if (n <= 0) return !buf.empty();
|
|
|
buf.insert(buf.end(), chunk, chunk + n);
|
|
|
if (std::find(buf.begin(), buf.end(), 0x5A) != buf.end()) return true;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
} // anonymous namespace
|
|
|
|
|
|
Bx6k1YyLedClient::Bx6k1YyLedClient(Config cfg) : m_cfg(std::move(cfg)) {}
|
|
|
|
|
|
Bx6k1YyLedClient::~Bx6k1YyLedClient() { closeSocket(); }
|
|
|
|
|
|
void Bx6k1YyLedClient::closeSocket() {
|
|
|
if (m_fd >= 0) { ::close(m_fd); m_fd = -1; }
|
|
|
}
|
|
|
|
|
|
bool Bx6k1YyLedClient::ensureConnected(std::string& error) {
|
|
|
if (m_fd >= 0) return true;
|
|
|
|
|
|
int fd = ::socket(AF_INET, SOCK_STREAM, 0);
|
|
|
if (fd < 0) { error = std::string("socket: ") + std::strerror(errno); return false; }
|
|
|
setNonBlocking(fd, true);
|
|
|
|
|
|
sockaddr_in addr{};
|
|
|
addr.sin_family = AF_INET;
|
|
|
addr.sin_port = htons(m_cfg.port);
|
|
|
if (inet_pton(AF_INET, m_cfg.ip.c_str(), &addr.sin_addr) != 1) {
|
|
|
::close(fd);
|
|
|
error = "bad ip " + m_cfg.ip;
|
|
|
return false;
|
|
|
}
|
|
|
if (!connectWithTimeout(fd, addr, m_cfg.connectTimeoutMs)) {
|
|
|
::close(fd);
|
|
|
error = "connect " + m_cfg.ip + ":" + std::to_string(m_cfg.port) + " failed";
|
|
|
return false;
|
|
|
}
|
|
|
setNonBlocking(fd, false);
|
|
|
int one = 1;
|
|
|
setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &one, sizeof(one));
|
|
|
m_fd = fd;
|
|
|
return true;
|
|
|
}
|
|
|
|
|
|
bool Bx6k1YyLedClient::queryScreenSize(int& outW, int& outH, std::string& error) {
|
|
|
if (m_fd < 0) { error = "not connected"; return false; }
|
|
|
auto frame = buildScreenParameterQueryFrame();
|
|
|
if (!sendAll(m_fd, frame.data(), frame.size(), m_cfg.responseTimeoutMs)) {
|
|
|
error = "send screen query failed"; return false;
|
|
|
}
|
|
|
std::vector<uint8_t> buf;
|
|
|
if (!recvUntilTail(m_fd, buf, m_cfg.responseTimeoutMs)) {
|
|
|
error = "recv screen query timeout"; return false;
|
|
|
}
|
|
|
auto head = std::find(buf.begin(), buf.end(), 0xA5);
|
|
|
if (head == buf.end()) { error = "no head"; return false; }
|
|
|
auto tailIt = std::find(buf.rbegin(), buf.rend(), 0x5A);
|
|
|
if (tailIt == buf.rend()) { error = "no tail"; return false; }
|
|
|
std::vector<uint8_t> phy1;
|
|
|
std::vector<uint8_t> oneFrame(head, tailIt.base());
|
|
|
if (!decodeFrame(oneFrame, phy1, error)) return false;
|
|
|
return parseScreenDimensions(phy1, outW, outH, error);
|
|
|
}
|
|
|
|
|
|
bool Bx6k1YyLedClient::displayText(const std::string& utf8Text, int areaId, std::string& error) {
|
|
|
if (!ensureConnected(error)) return false;
|
|
|
|
|
|
if (m_screenW <= 0 || m_screenH <= 0) {
|
|
|
int w = 0, h = 0;
|
|
|
if (queryScreenSize(w, h, error)) {
|
|
|
m_screenW = w; m_screenH = h;
|
|
|
} else {
|
|
|
// 查询失败:使用回退分辨率,不阻塞刷新
|
|
|
m_screenW = m_cfg.fallbackScreenWidth;
|
|
|
m_screenH = m_cfg.fallbackScreenHeight;
|
|
|
error.clear();
|
|
|
}
|
|
|
}
|
|
|
|
|
|
std::string encoded;
|
|
|
if (!encodeUtf8(utf8Text, m_cfg.textEncoding, encoded)) {
|
|
|
error = "encode failed"; return false;
|
|
|
}
|
|
|
|
|
|
const int lineH = m_screenH; // 单行:使用整屏高度
|
|
|
const bool isTemporary = (areaId == kAreaTemporary);
|
|
|
const int timeoutSec = isTemporary ? kTemporaryTimeoutSec : 0;
|
|
|
const int effectiveArea = (areaId == kAreaDefault) ? kAreaDefault : kAreaTemporary;
|
|
|
|
|
|
std::vector<uint8_t> frame = buildDynamicTextFrame(encoded,
|
|
|
m_screenW, lineH,
|
|
|
effectiveArea,
|
|
|
isTemporary, timeoutSec);
|
|
|
if (frame.empty()) { error = "build frame failed"; return false; }
|
|
|
if (!sendAll(m_fd, frame.data(), frame.size(), m_cfg.responseTimeoutMs)) {
|
|
|
error = "send frame failed"; return false;
|
|
|
}
|
|
|
std::vector<uint8_t> ack;
|
|
|
recvUntilTail(m_fd, ack, m_cfg.responseTimeoutMs); // 吃掉 ACK
|
|
|
|
|
|
// [诊断] 解 ACK 帧并把 PHY1 全打出来,看控制卡是成功还是返回错误码
|
|
|
if (!ack.empty()) {
|
|
|
std::vector<uint8_t> phy1;
|
|
|
std::string decErr;
|
|
|
if (decodeFrame(ack, phy1, decErr)) {
|
|
|
std::fprintf(stderr, "[Led] ACK phy1 (%zu B):", phy1.size());
|
|
|
for (uint8_t b : phy1) std::fprintf(stderr, " %02X", b);
|
|
|
std::fprintf(stderr, "\n");
|
|
|
// BX-6K1-YY ACK payload 通常从 phy1[14] 起;payload[5]=ErrorCode,0=成功
|
|
|
if (phy1.size() >= 20) {
|
|
|
std::fprintf(stderr, "[Led] ACK payload[5] (ErrorCode) = 0x%02X\n",
|
|
|
phy1[14 + 5]);
|
|
|
}
|
|
|
} else {
|
|
|
std::fprintf(stderr, "[Led] ACK decode failed: %s, raw (%zu B):",
|
|
|
decErr.c_str(), ack.size());
|
|
|
for (uint8_t b : ack) std::fprintf(stderr, " %02X", b);
|
|
|
std::fprintf(stderr, "\n");
|
|
|
}
|
|
|
} else {
|
|
|
std::fprintf(stderr, "[Led] ACK empty (no response from controller)\n");
|
|
|
}
|
|
|
|
|
|
return true;
|
|
|
}
|
|
|
|
|
|
} // namespace led
|