/* * ESP32-C3 multi-transport serial bridge — Proof of Concept * --------------------------------------------------------- * Three independent serial channels: * 1. BLE : Nordic UART Service (NUS) via NimBLE * 2. USB : USB-CDC (HWCDC over the USB Serial/JTAG controller -> "Serial") * 3. UART : Serial1 on GPIO0 (RX) / GPIO1 (TX) * * Every byte received on ANY channel is echoed to ALL channels, including * the one it came from. * * The onboard LED reacts to traffic: each received byte flashes it brighter, * then it fades back down (WS2812 also shifts hue). Purely cosmetic. * * Target board: ESP32-C3 SuperMini. Two hardware variants are supported via * the LED_TYPE build flag: * LED_TYPE 0 -> classic SuperMini: plain BLUE LED on GPIO8, active-low. * LED_TYPE 1 -> SuperMini Plus/V2: addressable WS2812B NeoPixel on GPIO8. * */ #include #include #ifndef LED_TYPE #define LED_TYPE 0 #endif #if LED_TYPE == 1 #include #endif // --------------------------------------------------------------------------- // Configuration // --------------------------------------------------------------------------- // UART1 pins on the SuperMini. GPIO0/GPIO1 are free, broken out on the header, // and carry no boot-strapping or USB role (18/19 = native USB, 20/21 = UART0). static constexpr int UART1_RX_PIN = 0; static constexpr int UART1_TX_PIN = 1; static constexpr uint32_t UART_BAUD = 115200; // Onboard LED on GPIO8 for both SuperMini variants. static constexpr int LED_PIN = 8; static constexpr int LED_COUNT = 1; static constexpr uint8_t LED_MAX_BRIGHTNESS = 200; #if LED_TYPE == 0 // Classic SuperMini blue LED is active-low (LOW = on). We drive brightness // via LEDC PWM on this channel. static constexpr bool LED_ACTIVE_LOW = true; static constexpr int LED_PWM_FREQ = 5000; static constexpr int LED_PWM_RES_BITS = 8; #endif // Nordic UART Service UUIDs (de-facto standard for "BLE serial"). static const char *NUS_SERVICE_UUID = "6e400001-b5a3-f393-e0a9-e50e24dcca9e"; static const char *NUS_RX_CHAR_UUID = "6e400002-b5a3-f393-e0a9-e50e24dcca9e"; // write (central -> device) static const char *NUS_TX_CHAR_UUID = "6e400003-b5a3-f393-e0a9-e50e24dcca9e"; // notify (device -> central) // --------------------------------------------------------------------------- // LED state // --------------------------------------------------------------------------- #if LED_TYPE == 1 static CRGB leds[LED_COUNT]; static uint8_t ledHue = 0; #endif static uint8_t ledBrightness = 0; // --------------------------------------------------------------------------- // BLE state // --------------------------------------------------------------------------- static NimBLECharacteristic *bleTxChar = nullptr; // notify to central static volatile bool bleConnected = false; // Bytes received over BLE are buffered here from the callback context and // drained in loop() to keep the BLE stack callbacks short. static String bleRxBuffer; // --------------------------------------------------------------------------- // Forward declarations // --------------------------------------------------------------------------- static void broadcast(const uint8_t *data, size_t len, int sourceChannel); static void onByteActivity(uint8_t b); // Channel identifiers (used only for logging / clarity). enum Channel { CH_BLE = 0, CH_USB = 1, CH_UART = 2 }; // --------------------------------------------------------------------------- // BLE callbacks // --------------------------------------------------------------------------- class ServerCallbacks : public NimBLEServerCallbacks { void onConnect(NimBLEServer *server, NimBLEConnInfo &connInfo) override { bleConnected = true; // Keep advertising so more than one central / reconnect works smoothly. NimBLEDevice::startAdvertising(); } void onDisconnect(NimBLEServer *server, NimBLEConnInfo &connInfo, int reason) override { bleConnected = false; NimBLEDevice::startAdvertising(); } }; class RxCharCallbacks : public NimBLECharacteristicCallbacks { void onWrite(NimBLECharacteristic *characteristic, NimBLEConnInfo &connInfo) override { std::string value = characteristic->getValue(); if (!value.empty()) { bleRxBuffer += String(value.c_str()); } } }; // --------------------------------------------------------------------------- // Output helpers per channel // --------------------------------------------------------------------------- static void writeToUsb(const uint8_t *data, size_t len) { Serial.write(data, len); } static void writeToUart(const uint8_t *data, size_t len) { Serial1.write(data, len); } static void writeToBle(const uint8_t *data, size_t len) { if (!bleConnected || bleTxChar == nullptr) { return; } // BLE notifications are limited by the negotiated MTU; chunk conservatively. const size_t chunk = 180; for (size_t offset = 0; offset < len; offset += chunk) { size_t n = min(chunk, len - offset); bleTxChar->setValue(data + offset, n); bleTxChar->notify(); } } // Send a block of bytes to every channel. The source channel is included on // purpose (local echo) per the requirement. static void broadcast(const uint8_t *data, size_t len, int sourceChannel) { (void)sourceChannel; // all channels receive it regardless of source writeToUsb(data, len); writeToUart(data, len); writeToBle(data, len); // Drive the LED animation from the traffic. for (size_t i = 0; i < len; ++i) { onByteActivity(data[i]); } } // --------------------------------------------------------------------------- // LED animation // --------------------------------------------------------------------------- static void onByteActivity(uint8_t b) { #if LED_TYPE == 1 // Shift hue by the byte value so different data => different colours. ledHue += (uint8_t)(b | 0x01); #else (void)b; #endif // Flash to full on activity; loop() fades it back down. ledBrightness = LED_MAX_BRIGHTNESS; } static void applyLedBrightness() { #if LED_TYPE == 1 leds[0] = CHSV(ledHue, 255, ledBrightness); FastLED.show(); #else // Map 0..255 brightness to the PWM duty, honouring active-low wiring. uint8_t duty = LED_ACTIVE_LOW ? (uint8_t)(255 - ledBrightness) : ledBrightness; ledcWrite(LED_PIN, duty); #endif } static void updateLed() { static uint32_t lastFade = 0; uint32_t now = millis(); if (now - lastFade >= 20) { // fade step every 20 ms lastFade = now; if (ledBrightness > 4) { ledBrightness -= 4; } else { ledBrightness = 0; } } applyLedBrightness(); } // --------------------------------------------------------------------------- // Setup // --------------------------------------------------------------------------- static void setupBle() { NimBLEDevice::init("ESP32C3-SerialBridge"); NimBLEDevice::setPower(ESP_PWR_LVL_P9); NimBLEServer *server = NimBLEDevice::createServer(); server->setCallbacks(new ServerCallbacks()); NimBLEService *service = server->createService(NUS_SERVICE_UUID); // RX: central writes to us. NimBLECharacteristic *rxChar = service->createCharacteristic( NUS_RX_CHAR_UUID, NIMBLE_PROPERTY::WRITE | NIMBLE_PROPERTY::WRITE_NR); rxChar->setCallbacks(new RxCharCallbacks()); // TX: we notify the central. bleTxChar = service->createCharacteristic( NUS_TX_CHAR_UUID, NIMBLE_PROPERTY::NOTIFY); service->start(); NimBLEAdvertising *advertising = NimBLEDevice::getAdvertising(); advertising->addServiceUUID(NUS_SERVICE_UUID); advertising->setName("ESP32C3-SerialBridge"); advertising->enableScanResponse(true); NimBLEDevice::startAdvertising(); } void setup() { // USB-CDC (HWCDC). Shows up as a virtual COM port over native USB. Serial.begin(115200); // Hardware UART1 on custom pins. Serial1.begin(UART_BAUD, SERIAL_8N1, UART1_RX_PIN, UART1_TX_PIN); // Onboard LED. #if LED_TYPE == 1 // SuperMini Plus / V2: addressable WS2812 NeoPixel. FastLED.addLeds(leds, LED_COUNT); FastLED.setBrightness(255); // per-pixel brightness handled via CHSV value leds[0] = CRGB::Black; FastLED.show(); #else // Classic SuperMini: plain blue LED dimmed via LEDC PWM. ledcAttach(LED_PIN, LED_PWM_FREQ, LED_PWM_RES_BITS); ledBrightness = 0; applyLedBrightness(); // start off #endif bleRxBuffer.reserve(256); setupBle(); const char *banner = "\r\n[ESP32-C3 multi-serial bridge ready: BLE + USB-CDC + UART1]\r\n"; broadcast(reinterpret_cast(banner), strlen(banner), CH_USB); } // --------------------------------------------------------------------------- // Main loop // --------------------------------------------------------------------------- void loop() { uint8_t buf[128]; // --- USB-CDC input --- if (int avail = Serial.available()) { size_t n = Serial.readBytes(buf, min((int)sizeof(buf), avail)); if (n) broadcast(buf, n, CH_USB); } // --- UART1 input --- if (int avail = Serial1.available()) { size_t n = Serial1.readBytes(buf, min((int)sizeof(buf), avail)); if (n) broadcast(buf, n, CH_UART); } // --- BLE input (buffered by the write callback) --- if (bleRxBuffer.length() > 0) { // Grab and clear atomically-ish. Callbacks run in the NimBLE task; // a brief race here is acceptable for a PoC. String chunk = bleRxBuffer; bleRxBuffer = ""; broadcast(reinterpret_cast(chunk.c_str()), chunk.length(), CH_BLE); } updateLed(); }