#include "fgc/SerialMotorController.h" #include "fgc/Logger.h" #include "fgc/TelemetryParser.h" #include #include #include #include #include namespace fgc { struct SerialMotorController::Impl { Impl(std::string dev, unsigned int b) : device(std::move(dev)), baud(b), serial(io) {} std::string device; unsigned int baud; boost::asio::io_context io; boost::asio::serial_port serial; boost::asio::streambuf buffer; std::thread io_thread; std::mutex mutex; MotorTelemetry latest; std::atomic connected{false}; // DUMP capture: the firmware emits a "DUMP BEGIN" ... "DUMP END" block in // response to a DUMP command. We accumulate it line-by-line and publish the // completed block (latest_dump) for the UI / `dump` command. bool dumping = false; std::string dump_buf; std::string latest_dump; void doRead() { boost::asio::async_read_until( serial, buffer, '\n', [this](const boost::system::error_code& ec, std::size_t) { if (ec) { LOG_WARN << "Serial read failed: " << ec.message(); return; } std::istream is(&buffer); std::string line; std::getline(is, line); // Strip a trailing CR (firmware may send CRLF). if (!line.empty() && line.back() == '\r') line.pop_back(); dispatchLine(line); doRead(); }); } // Route an inbound line: ST -> telemetry; OK/ERR -> command ack; everything // else (DG/DUMP/BOOT/IOIN/GSTAT...) is async output we just trace. void dispatchLine(const std::string& line) { if (line.rfind("ST ", 0) == 0 || line == "ST") { if (auto t = parseTelemetryLine(line)) { LOG_TRACE_CAT(LogCat::Serial) << "RX " << line; std::lock_guard lock(mutex); latest = *t; } else { LOG_TRACE_CAT(LogCat::Serial) << "RX(unparsed ST) " << line; } } else if (line.rfind("ERR", 0) == 0) { LOG_TRACE_CAT(LogCat::Serial) << "RX " << line; LOG_WARN << "Motor controller: " << line; } else if (!line.empty()) { // OK acks and other async output (DG/DUMP/BOOT/...). LOG_TRACE_CAT(LogCat::Serial) << "RX " << line; captureDump(line); } } // Assemble the multi-line DUMP block. On completion, store it and emit it // once at INFO so it's visible regardless of the SERIAL trace toggle. void captureDump(const std::string& line) { if (line.rfind("DUMP BEGIN", 0) == 0) { dumping = true; dump_buf = line + "\n"; return; } if (!dumping) return; dump_buf += line + "\n"; if (line.rfind("DUMP END", 0) == 0) { dumping = false; { std::lock_guard lock(mutex); latest_dump = dump_buf; } LOG_INFO << "firmware dump:\n" << dump_buf; dump_buf.clear(); } } }; SerialMotorController::SerialMotorController(std::string device, unsigned int baud) : impl_(std::make_unique(std::move(device), baud)) {} SerialMotorController::~SerialMotorController() { stop(); } void SerialMotorController::start() { namespace asio = boost::asio; boost::system::error_code ec; impl_->serial.open(impl_->device, ec); if (ec) { LOG_ERROR << "Failed to open serial port " << impl_->device << ": " << ec.message(); impl_->connected = false; return; } impl_->serial.set_option(asio::serial_port_base::baud_rate(impl_->baud)); impl_->serial.set_option(asio::serial_port_base::character_size(8)); impl_->serial.set_option( asio::serial_port_base::parity(asio::serial_port_base::parity::none)); impl_->serial.set_option( asio::serial_port_base::stop_bits(asio::serial_port_base::stop_bits::one)); impl_->serial.set_option( asio::serial_port_base::flow_control(asio::serial_port_base::flow_control::none)); impl_->connected = true; impl_->doRead(); impl_->io_thread = std::thread([this] { impl_->io.run(); }); LOG_INFO << "Serial controller started on " << impl_->device << " @ " << impl_->baud; } void SerialMotorController::stop() { if (!impl_) return; impl_->io.stop(); if (impl_->io_thread.joinable()) impl_->io_thread.join(); boost::system::error_code ec; if (impl_->serial.is_open()) impl_->serial.close(ec); impl_->connected = false; } void SerialMotorController::sendCommand(const std::string& cmd) { if (!impl_->connected) return; LOG_TRACE_CAT(LogCat::Serial) << "TX " << cmd; // The firmware requires a newline terminator on every command. auto data = std::make_shared(cmd + "\n"); boost::asio::async_write(impl_->serial, boost::asio::buffer(*data), [data](const boost::system::error_code& ec, std::size_t) { if (ec) LOG_WARN << "Serial write failed: " << ec.message(); }); } MotorTelemetry SerialMotorController::telemetry() { std::lock_guard lock(impl_->mutex); return impl_->latest; } std::string SerialMotorController::lastDump() { std::lock_guard lock(impl_->mutex); return impl_->latest_dump; } bool SerialMotorController::connected() const { return impl_->connected; } } // namespace fgc