Add ESP32-S3 BLE-to-RS422 bridge firmware for Carryout G2
NimBLE-based Nordic UART Service (NUS) bridge on ESP32-S3-DevKitC-1. Transparent passthrough: BLE client writes → UART1 TX → RS-422 → G2, and G2 → RS-422 → UART1 RX → BLE notifications. USB serial serves as debug monitor and fallback input. Uses two MAX485 modules (one locked TX, one locked RX) with a SparkFun BSS138 level converter for 3.3V/5V translation. Wiring schematic and RJ-12 pinout documented in docs/ble-bridge-wiring.md.
This commit is contained in:
parent
1192b31166
commit
068f38d7eb
5
.gitignore
vendored
5
.gitignore
vendored
@ -10,3 +10,8 @@ build/
|
||||
.env
|
||||
*.so
|
||||
.ruff_cache/
|
||||
|
||||
# PlatformIO
|
||||
.pio/
|
||||
.pioenvs/
|
||||
.piolibdeps/
|
||||
|
||||
120
docs/ble-bridge-wiring.md
Normal file
120
docs/ble-bridge-wiring.md
Normal file
@ -0,0 +1,120 @@
|
||||
# BLE Bridge Wiring — ESP32-S3 + 2× MAX485
|
||||
|
||||
Transparent BLE-to-RS422 bridge for the Winegard Carryout G2 satellite dish.
|
||||
|
||||
## Parts
|
||||
|
||||
- ESP32-S3-DevKitC-1-N16R8
|
||||
- 2× MAX485 TTL-to-RS485 module
|
||||
- 1× SparkFun Bidirectional Logic Level Converter (BOB-12009, BSS138-based)
|
||||
- RJ-12 6P6C straight-wired cable with breakout
|
||||
- Hookup wire / jumpers
|
||||
|
||||
## Schematic
|
||||
|
||||
```
|
||||
SparkFun Level Converter (BOB-12009)
|
||||
┌──────────────────────────────────────┐
|
||||
│ │
|
||||
ESP32 3V3 ──────────────►│ LV HV │◄── ESP32 5V
|
||||
ESP32 GND ──────────────►│ GND GND │◄── (shared)
|
||||
│ │
|
||||
ESP32 GPIO17 (TX) ──────►│ LV1 HV1 │──────► MAX485₁ DI
|
||||
ESP32 GPIO18 (RX) ◄──────│ LV2 HV2 │◄────── MAX485₂ RO
|
||||
│ │
|
||||
│ LV3 (spare) HV3 (spare) │
|
||||
│ LV4 (spare) HV4 (spare) │
|
||||
└──────────────────────────────────────┘
|
||||
|
||||
|
||||
MAX485 Board 1 (TX only) MAX485 Board 2 (RX only)
|
||||
┌────────────────────────┐ ┌────────────────────────┐
|
||||
│ VCC ◄── 5V │ │ VCC ◄── 5V │
|
||||
│ GND ◄── GND │ │ GND ◄── GND │
|
||||
│ │ │ │
|
||||
│ DI ◄── HV1 │ │ RO ──► HV2 │
|
||||
│ RO (unused) │ │ DI (unused) │
|
||||
│ │ │ │
|
||||
│ DE ◄── 5V ┐ locked │ │ DE ◄── GND ┐ locked │
|
||||
│ RE ◄── 5V ┘ TX mode │ │ RE ◄── GND ┘ RX mode │
|
||||
│ │ │ │
|
||||
│ A ───────────────────┼──► pin 2 │ A ◄──────────────────┼── pin 4
|
||||
│ B ───────────────────┼──► pin 3 │ B ◄──────────────────┼── pin 5
|
||||
└────────────────────────┘ └────────────────────────┘
|
||||
|
||||
RJ-12 to Carryout G2
|
||||
┌───────────────────────────┐
|
||||
│ Pin 1 (White) ── GND │◄── ESP32 GND
|
||||
│ Pin 2 (Red) ── TX+/TA │◄── A₁
|
||||
│ Pin 3 (Black) ── TX-/TB │◄── B₁
|
||||
│ Pin 4 (Yellow) ── RX+/RA │──► A₂
|
||||
│ Pin 5 (Green) ── RX-/RB │──► B₂
|
||||
│ Pin 6 (Blue) ── N/C │
|
||||
└───────────────────────────┘
|
||||
```
|
||||
|
||||
## Power Rails
|
||||
|
||||
```
|
||||
ESP32 5V ──┬── Level Converter HV
|
||||
├── MAX485₁ VCC
|
||||
├── MAX485₁ DE + RE (tied high = TX mode)
|
||||
└── MAX485₂ VCC
|
||||
|
||||
ESP32 3V3 ─── Level Converter LV
|
||||
|
||||
ESP32 GND ─┬── Level Converter GND
|
||||
├── MAX485₁ GND
|
||||
├── MAX485₂ GND
|
||||
├── MAX485₂ DE + RE (tied low = RX mode)
|
||||
└── RJ-12 Pin 1
|
||||
```
|
||||
|
||||
## RJ-12 Cable Notes
|
||||
|
||||
Straight-wired 6P6C. Pin 1 is leftmost when looking at the jack with the clip
|
||||
facing away from you (tab down). Wire colors per the standard flat cable:
|
||||
|
||||
| Pin | Color | Function | Connects to |
|
||||
|-----|--------|------------------|--------------------|
|
||||
| 1 | White | GND | Common ground |
|
||||
| 2 | Red | TX+ (TA) | MAX485₁ A |
|
||||
| 3 | Black | TX- (TB) | MAX485₁ B |
|
||||
| 4 | Yellow | RX+ (RA) | MAX485₂ A |
|
||||
| 5 | Green | RX- (RB) | MAX485₂ B |
|
||||
| 6 | Blue | N/C | — |
|
||||
|
||||
If crimping your own cable, verify pin-to-color with a multimeter before
|
||||
connecting to the dish. RJ-12 crimps are easy to get reversed (pins mirror
|
||||
if the connector is flipped). A wrong connection won't damage anything
|
||||
(differential signals are current-limited) but communication won't work.
|
||||
|
||||
## How It Works
|
||||
|
||||
The Carryout G2 uses RS-422 full-duplex: two separate differential pairs,
|
||||
one for each direction. The MAX485 is a half-duplex RS-485 transceiver with
|
||||
a shared A/B pair and direction control pins (DE/RE). By hardwiring DE/RE,
|
||||
each board is locked into a single direction:
|
||||
|
||||
- **Board 1 (TX):** DE=HIGH, RE=HIGH → driver always enabled, receiver disabled.
|
||||
ESP32 UART1 TX → level shifter → DI → differential A/B → G2 serial RX.
|
||||
|
||||
- **Board 2 (RX):** DE=LOW, RE=LOW → driver disabled, receiver always enabled.
|
||||
G2 serial TX → differential A/B → RO → level shifter → ESP32 UART1 RX.
|
||||
|
||||
The SparkFun level converter translates between 3.3V (ESP32) and 5V (MAX485)
|
||||
on both data lines. The two spare channels (LV3/HV3, LV4/HV4) are available
|
||||
if DE/RE ever need GPIO control for a half-duplex variant.
|
||||
|
||||
## Firmware
|
||||
|
||||
See `firmware/ble-bridge/` — transparent BLE Nordic UART Service (NUS) bridge.
|
||||
The firmware is the same regardless of whether the RS-422 transceiver is a
|
||||
MAX490 (single full-duplex chip) or two MAX485s (locked half-duplex pair).
|
||||
It only sees UART TX/RX on GPIO17/18.
|
||||
|
||||
## Loopback Test (no dish)
|
||||
|
||||
Before connecting to the G2, verify the bridge by shorting MAX485₁ A to
|
||||
MAX485₂ A, and MAX485₁ B to MAX485₂ B (loop TX back into RX). Anything
|
||||
sent via BLE or USB serial should echo back.
|
||||
39
firmware/ble-bridge/include/config.h
Normal file
39
firmware/ble-bridge/include/config.h
Normal file
@ -0,0 +1,39 @@
|
||||
#pragma once
|
||||
|
||||
// --- GPIO Pin Assignments ---
|
||||
// UART1 to RS-422 module via 3.3V<->5V level shifter
|
||||
#define PIN_RS422_TX 17 // ESP32 TX -> level shifter -> MAX490 RXD
|
||||
#define PIN_RS422_RX 18 // MAX490 TXD -> level shifter -> ESP32 RX
|
||||
|
||||
// Onboard RGB LED (WS2812, DevKitC-1 V1.1)
|
||||
#define PIN_LED 38
|
||||
|
||||
// --- RS-422 UART (UART1) ---
|
||||
#define RS422_BAUD 115200
|
||||
#define RS422_CONFIG SERIAL_8N1
|
||||
|
||||
// --- BLE Configuration ---
|
||||
#define BLE_DEVICE_NAME "Travler-G2"
|
||||
#define BLE_MTU 517
|
||||
// Max payload per NUS notification (must fit in ATT_MTU - 3)
|
||||
#define BLE_NOTIFY_MAX 240
|
||||
|
||||
// Nordic UART Service UUIDs
|
||||
#define NUS_SERVICE_UUID "6E400001-B5A3-F393-E0A9-E50E24DCCA9E"
|
||||
#define NUS_RX_UUID "6E400002-B5A3-F393-E0A9-E50E24DCCA9E" // Client writes here
|
||||
#define NUS_TX_UUID "6E400003-B5A3-F393-E0A9-E50E24DCCA9E" // ESP32 notifies here
|
||||
|
||||
// --- Timing ---
|
||||
// Inter-byte coalescing window: collect bytes arriving within this
|
||||
// gap into one BLE notification instead of sending byte-by-byte
|
||||
#define COALESCE_MS 3
|
||||
|
||||
// LED refresh interval
|
||||
#define LED_UPDATE_MS 50
|
||||
|
||||
// --- LED ---
|
||||
#define LED_BRIGHTNESS 30 // 0-255, keep low to avoid blinding in enclosure
|
||||
#define LED_COUNT 1
|
||||
|
||||
// --- Buffers ---
|
||||
#define UART_RX_BUF_SIZE 512
|
||||
21
firmware/ble-bridge/platformio.ini
Normal file
21
firmware/ble-bridge/platformio.ini
Normal file
@ -0,0 +1,21 @@
|
||||
; ESP32-S3 BLE-to-RS422 Bridge for Winegard Carryout G2
|
||||
; Transparent NUS (Nordic UART Service) serial bridge
|
||||
|
||||
[env:esp32s3]
|
||||
platform = espressif32
|
||||
board = esp32-s3-devkitc-1
|
||||
framework = arduino
|
||||
|
||||
; NimBLE for BLE (lighter than BlueDroid), NeoPixel for status LED
|
||||
lib_deps =
|
||||
h2zero/NimBLE-Arduino@^2.1
|
||||
adafruit/Adafruit NeoPixel@^1.12
|
||||
|
||||
build_flags =
|
||||
-DBOARD_HAS_PSRAM
|
||||
-DBOARD_HAS_PSRAM_OPI
|
||||
; Serial via CP2102N UART0 port, not USB-CDC
|
||||
-DARDUINO_USB_CDC_ON_BOOT=0
|
||||
|
||||
monitor_speed = 115200
|
||||
upload_speed = 921600
|
||||
187
firmware/ble-bridge/src/main.cpp
Normal file
187
firmware/ble-bridge/src/main.cpp
Normal file
@ -0,0 +1,187 @@
|
||||
#include <Arduino.h>
|
||||
#include <NimBLEDevice.h>
|
||||
#include <Adafruit_NeoPixel.h>
|
||||
#include "config.h"
|
||||
|
||||
// --- Globals ---
|
||||
static NimBLEServer *pServer = nullptr;
|
||||
static NimBLECharacteristic *pTxChar = nullptr; // ESP32 -> Client (notify)
|
||||
static NimBLECharacteristic *pRxChar = nullptr; // Client -> ESP32 (write)
|
||||
static Adafruit_NeoPixel led(LED_COUNT, PIN_LED, NEO_GRB + NEO_KHZ800);
|
||||
|
||||
static bool deviceConnected = false;
|
||||
static uint32_t lastActivityMs = 0;
|
||||
static uint32_t lastLedUpdateMs = 0;
|
||||
|
||||
// Coalescing buffer for UART1 RX -> BLE TX
|
||||
static uint8_t coalBuf[UART_RX_BUF_SIZE];
|
||||
static size_t coalLen = 0;
|
||||
static uint32_t coalLastByteMs = 0;
|
||||
|
||||
// --- BLE Callbacks ---
|
||||
|
||||
class ServerCallbacks : public NimBLEServerCallbacks {
|
||||
void onConnect(NimBLEServer *server, NimBLEConnInfo &connInfo) override {
|
||||
deviceConnected = true;
|
||||
// Request fast connection interval (7.5ms-15ms) for low latency
|
||||
// params: min_interval, max_interval, latency, supervision_timeout
|
||||
// intervals are in 1.25ms units: 6 = 7.5ms, 12 = 15ms
|
||||
server->updateConnParams(connInfo.getConnHandle(), 6, 12, 0, 200);
|
||||
Serial.println("[BLE] Client connected");
|
||||
}
|
||||
|
||||
void onDisconnect(NimBLEServer *server, NimBLEConnInfo &connInfo, int reason) override {
|
||||
deviceConnected = false;
|
||||
Serial.printf("[BLE] Client disconnected (reason=0x%02x), re-advertising\n", reason);
|
||||
NimBLEDevice::startAdvertising();
|
||||
}
|
||||
};
|
||||
|
||||
class RxCallbacks : public NimBLECharacteristicCallbacks {
|
||||
void onWrite(NimBLECharacteristic *pChar, NimBLEConnInfo &connInfo) override {
|
||||
std::string val = pChar->getValue();
|
||||
if (val.length() > 0) {
|
||||
// Forward BLE RX -> RS-422 TX
|
||||
Serial1.write((const uint8_t *)val.data(), val.length());
|
||||
Serial.printf("[BLE->422] %u bytes\n", val.length());
|
||||
lastActivityMs = millis();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// --- LED Status ---
|
||||
|
||||
static void updateLed() {
|
||||
uint32_t now = millis();
|
||||
if (now - lastLedUpdateMs < LED_UPDATE_MS) return;
|
||||
lastLedUpdateMs = now;
|
||||
|
||||
if (!deviceConnected) {
|
||||
// Blue breathing: sine wave on blue channel
|
||||
float phase = (float)(now % 3000) / 3000.0f * TWO_PI;
|
||||
uint8_t brightness = (uint8_t)((sinf(phase) + 1.0f) * 0.5f * LED_BRIGHTNESS);
|
||||
led.setPixelColor(0, led.Color(0, 0, brightness));
|
||||
} else if (now - lastActivityMs < 100) {
|
||||
// Cyan flash: recent data activity
|
||||
led.setPixelColor(0, led.Color(0, LED_BRIGHTNESS, LED_BRIGHTNESS));
|
||||
} else {
|
||||
// Solid green: connected, idle
|
||||
led.setPixelColor(0, led.Color(0, LED_BRIGHTNESS, 0));
|
||||
}
|
||||
led.show();
|
||||
}
|
||||
|
||||
// --- BLE Setup ---
|
||||
|
||||
static void initBLE() {
|
||||
NimBLEDevice::init(BLE_DEVICE_NAME);
|
||||
NimBLEDevice::setMTU(BLE_MTU);
|
||||
// Set TX power to maximum for range through enclosure
|
||||
NimBLEDevice::setPower(ESP_PWR_LVL_P9);
|
||||
|
||||
pServer = NimBLEDevice::createServer();
|
||||
pServer->setCallbacks(new ServerCallbacks());
|
||||
|
||||
NimBLEService *pService = pServer->createService(NUS_SERVICE_UUID);
|
||||
|
||||
// TX characteristic: ESP32 notifies client with data from G2
|
||||
pTxChar = pService->createCharacteristic(
|
||||
NUS_TX_UUID,
|
||||
NIMBLE_PROPERTY::NOTIFY
|
||||
);
|
||||
|
||||
// RX characteristic: client writes data destined for G2
|
||||
pRxChar = pService->createCharacteristic(
|
||||
NUS_RX_UUID,
|
||||
NIMBLE_PROPERTY::WRITE | NIMBLE_PROPERTY::WRITE_NR
|
||||
);
|
||||
pRxChar->setCallbacks(new RxCallbacks());
|
||||
|
||||
pService->start();
|
||||
|
||||
// Advertising config
|
||||
NimBLEAdvertising *pAdv = NimBLEDevice::getAdvertising();
|
||||
pAdv->addServiceUUID(NUS_SERVICE_UUID);
|
||||
pAdv->setAppearance(0x0080); // Generic Computer
|
||||
pAdv->setScanResponse(true);
|
||||
NimBLEDevice::startAdvertising();
|
||||
|
||||
Serial.printf("[BLE] Advertising as \"%s\"\n", BLE_DEVICE_NAME);
|
||||
}
|
||||
|
||||
// --- UART Bridge Loop ---
|
||||
|
||||
static void bridgeLoop() {
|
||||
uint32_t now = millis();
|
||||
|
||||
// --- UART1 RX (G2) -> coalesce -> BLE TX + USB echo ---
|
||||
while (Serial1.available()) {
|
||||
if (coalLen < UART_RX_BUF_SIZE) {
|
||||
coalBuf[coalLen++] = Serial1.read();
|
||||
} else {
|
||||
Serial1.read(); // Drain overflow
|
||||
}
|
||||
coalLastByteMs = millis();
|
||||
}
|
||||
|
||||
// Flush coalesced buffer after inter-byte gap expires
|
||||
if (coalLen > 0 && (millis() - coalLastByteMs >= COALESCE_MS)) {
|
||||
// Always echo to USB serial for monitoring
|
||||
Serial.write(coalBuf, coalLen);
|
||||
|
||||
// Send via BLE if connected, chunked to BLE_NOTIFY_MAX
|
||||
if (deviceConnected && pTxChar != nullptr) {
|
||||
size_t offset = 0;
|
||||
while (offset < coalLen) {
|
||||
size_t chunk = min((size_t)BLE_NOTIFY_MAX, coalLen - offset);
|
||||
pTxChar->setValue(coalBuf + offset, chunk);
|
||||
pTxChar->notify();
|
||||
offset += chunk;
|
||||
}
|
||||
Serial.printf("[422->BLE] %u bytes\n", coalLen);
|
||||
lastActivityMs = millis();
|
||||
}
|
||||
|
||||
coalLen = 0;
|
||||
}
|
||||
|
||||
// --- USB Serial RX -> UART1 TX (fallback input) ---
|
||||
while (Serial.available()) {
|
||||
uint8_t c = Serial.read();
|
||||
Serial1.write(c);
|
||||
lastActivityMs = millis();
|
||||
}
|
||||
}
|
||||
|
||||
// --- Arduino Entry Points ---
|
||||
|
||||
void setup() {
|
||||
// USB serial console (UART0 via CP2102N)
|
||||
Serial.begin(115200);
|
||||
delay(500); // Let USB enumerate
|
||||
Serial.println();
|
||||
Serial.println("=== Travler-G2 BLE Bridge ===");
|
||||
Serial.println("RS-422: 115200 8N1 on GPIO17(TX)/GPIO18(RX)");
|
||||
Serial.println("BLE: Nordic UART Service (NUS)");
|
||||
Serial.println();
|
||||
|
||||
// RS-422 UART (UART1)
|
||||
Serial1.begin(RS422_BAUD, RS422_CONFIG, PIN_RS422_RX, PIN_RS422_TX);
|
||||
Serial.println("[UART1] RS-422 initialized");
|
||||
|
||||
// Status LED
|
||||
led.begin();
|
||||
led.setBrightness(LED_BRIGHTNESS);
|
||||
led.setPixelColor(0, led.Color(0, 0, LED_BRIGHTNESS)); // Blue at boot
|
||||
led.show();
|
||||
|
||||
// BLE
|
||||
initBLE();
|
||||
|
||||
Serial.println("[BOOT] Ready. Waiting for BLE client...");
|
||||
}
|
||||
|
||||
void loop() {
|
||||
bridgeLoop();
|
||||
updateLed();
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user