#include "serial_link.hpp" #include "sdkconfig.h" #include #include #include #include #include #include #include #include #include #include namespace microbu::serial { namespace { std::uint16_t crc16_ccitt_false_step(std::uint16_t crc, std::uint8_t octet) { crc ^= static_cast(octet) << 8; for (int bit = 0; bit < 8; ++bit) crc = (crc & 0x8000) ? static_cast((crc << 1) ^ 0x1021) : static_cast(crc << 1); return crc; } } std::uint16_t crc16_ccitt_false(const std::uint8_t* data, std::size_t length) { std::uint16_t crc = 0xFFFF; for (std::size_t i = 0; i < length; ++i) crc = crc16_ccitt_false_step(crc, data[i]); return crc; } Bytes encode_frame(FrameType type, const Bytes& payload) { Bytes out; out.reserve(payload.size() + 7); out.push_back(0xAA); out.push_back(0x55); out.push_back(static_cast(type)); out.push_back(payload.size() & 0xFF); out.push_back(payload.size() >> 8); out.insert(out.end(), payload.begin(), payload.end()); const auto crc = crc16_ccitt_false(out.data() + 2, out.size() - 2); out.push_back(crc & 0xFF); out.push_back(crc >> 8); return out; } void Decoder::feed(const std::uint8_t* data, std::size_t length, const Handler& handler) { for (std::size_t i = 0; i < length; ++i) { const std::uint8_t b = data[i]; switch (state_) { case State::SYNC0: state_ = (b == 0xAA) ? State::SYNC1 : State::SYNC0; break; case State::SYNC1: state_ = (b == 0x55) ? State::TYPE : (b == 0xAA ? State::SYNC1 : State::SYNC0); break; case State::TYPE: type_ = b; state_ = State::LEN_LO; break; case State::LEN_LO: length_ = b; state_ = State::LEN_HI; break; case State::LEN_HI: length_ |= static_cast(b) << 8; payload_.clear(); if (length_ > maximum_payload) state_ = State::SYNC0; // untrustworthy boundary: resync else state_ = length_ == 0 ? State::CRC_LO : State::PAYLOAD; break; case State::PAYLOAD: payload_.push_back(b); if (payload_.size() >= length_) state_ = State::CRC_LO; break; case State::CRC_LO: crc_ = b; state_ = State::CRC_HI; break; case State::CRC_HI: { crc_ |= static_cast(b) << 8; // CRC over head then payload without concatenating them: same running value as encode_frame. const std::uint8_t head[3] = {type_, static_cast(length_ & 0xFF), static_cast(length_ >> 8)}; std::uint16_t crc = 0xFFFF; for (auto octet : head) crc = crc16_ccitt_false_step(crc, octet); for (auto octet : payload_) crc = crc16_ccitt_false_step(crc, octet); if (crc == crc_) { ++frames_; handler(Frame {static_cast(type_), payload_}); } else ++crc_errors_; state_ = State::SYNC0; break; } } } } namespace { SemaphoreHandle_t writer_lock = nullptr; Counters the_counters; Decoder::Handler frame_handler; std::atomic last_frame_at_ms {0}; // 32-bit: no libatomic needed on RV32 #if CONFIG_MICROBU_LOG_OVER_LINK vprintf_like_t previous_vprintf = nullptr; #endif bool write_all(const Bytes& frame) { std::size_t sent = 0; while (sent < frame.size()) { // MicrOBU: 50 ms, not the colleague's 500. A phone that is plugged in but not reading (app // not running) fills the driver's buffer, and every write then waits out this timeout on // the station task, which also serves BLE and the radio. const int count = usb_serial_jtag_write_bytes(frame.data() + sent, frame.size() - sent, pdMS_TO_TICKS(50)); if (count <= 0) return false; // the host detects an incomplete frame by CRC sent += count; } return true; } #if CONFIG_MICROBU_LOG_OVER_LINK // ESP_LOG sink: one LOG frame per call, never a raw write into the frame stream. int log_to_frame(const char* format, va_list args) { char line[256]; const int length = std::vsnprintf(line, sizeof line, format, args); if (length <= 0) return 0; std::size_t n = static_cast(length) < sizeof line ? length : sizeof line - 1; while (n > 0 && (line[n - 1] == '\n' || line[n - 1] == '\r')) --n; if (n == 0) return length; // esp_log colour escapes are stripped by the emulator; keep the line verbatim otherwise. write(FrameType::LOG, Bytes(line, line + n)); return length; } #endif void reader_task(void*) { Decoder decoder; std::uint8_t buffer[512]; for (;;) { const int count = usb_serial_jtag_read_bytes(buffer, sizeof buffer, pdMS_TO_TICKS(20)); if (count > 0) { decoder.feed(buffer, static_cast(count), [](Frame frame) { last_frame_at_ms = static_cast(esp_timer_get_time() / 1000); frame_handler(std::move(frame)); }); the_counters.frames = decoder.frames(); the_counters.crc_errors = decoder.crc_errors(); } } } } void start(const Decoder::Handler& on_frame) { frame_handler = on_frame; writer_lock = xSemaphoreCreateMutex(); usb_serial_jtag_driver_config_t config {}; config.rx_buffer_size = 8192; config.tx_buffer_size = 8192; ESP_ERROR_CHECK(usb_serial_jtag_driver_install(&config)); #if CONFIG_MICROBU_LOG_OVER_LINK previous_vprintf = esp_log_set_vprintf(log_to_frame); #endif xTaskCreate(reader_task, "link_rx", 6144, nullptr, 12, nullptr); } bool write(FrameType type, const Bytes& payload) { if (payload.size() > maximum_payload || !writer_lock) return false; // MicrOBU: nothing on the native port (phone on BLE, or unplugged). Without this check every // write, the 1 Hz STATUS included, would block the station task until the timeout. if (!usb_serial_jtag_is_connected()) { ++the_counters.not_connected; return false; } const auto frame = encode_frame(type, payload); if (xSemaphoreTake(writer_lock, pdMS_TO_TICKS(1000)) != pdTRUE) { ++the_counters.write_failures; return false; } const bool ok = write_all(frame); xSemaphoreGive(writer_lock); if (!ok) ++the_counters.write_failures; return ok; } Counters counters() { return the_counters; } std::uint32_t last_frame_ms() { return last_frame_at_ms.load(); } } // namespace microbu::serial