Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions ats-mini/BleHidCentral.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -237,16 +237,17 @@ void BleHidCentral::onScanStart()
char statusLine[40];
uint8_t maxAttempts = MAX_SCAN_ATTEMPTS;

drawScreen();
// Draw the status once and return. The previous code also drew a blank screen
// first (a visible flash) and blocked the cooperative loop with delay(500) per
// attempt (up to 1.5 s total). The message persists until the next redraw, so
// no delay is needed and the loop stays responsive while scanning.
if (maxAttempts)
{
snprintf(statusLine, sizeof(statusLine), "Scanning for BLE HID %u/%u...", scanAttempts, maxAttempts);
drawScreen(statusLine);
}
else
drawScreen("Scanning for BLE HID...");

delay(500);
}

bool BleHidCentral::acceptsAdvertisement(BLEAdvertisedDevice& device)
Expand Down
8 changes: 7 additions & 1 deletion ats-mini/BleHidCentral.h
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,13 @@

#include "BleCentral.h"

#define BLE_SCAN_INTERVAL 100
// Milliseconds (the BLEScan API divides by 0.625 to get hardware units). A 400 ms
// interval with a 100 ms window is a 25% scan duty cycle instead of the previous
// 100% (interval == window): it cuts the RF the 2.4 GHz radio radiates next to
// the SI4732 antenna (lower SW/AM noise floor and current draw while scanning)
// and adds at most ~300 ms of discovery latency per cycle, imperceptible for a
// one-time HID pairing.
#define BLE_SCAN_INTERVAL 400
#define BLE_SCAN_WINDOW 100

struct BleHidState {
Expand Down
18 changes: 13 additions & 5 deletions ats-mini/BleMode.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,17 @@ int bleLoop(uint8_t bleMode)
if (bleMode == BLE_ADHOC)
{
if (BLESerial.isConnected())
{
remoteTickTime(&BLESerial, &remoteBLEState);
if (!BLESerial.isConnected()) return 0;
remoteMemoryDumpTick(&BLESerial, &remoteBLEState);
}
else
{
// Scope a chunked "$" dump to a single connection: drop any in-progress
// dump so it does not resume and stream unsolicited slots to a new client.
remoteBLEState.memoryDumpSlot = -1;
return 0;
}
if (BLESerial.available())
return remoteDoCommand(&BLESerial, &remoteBLEState, BLESerial.read());
return 0;
Expand All @@ -92,12 +101,11 @@ int bleLoop(uint8_t bleMode)
if (bleMode != BLE_HID)
return 0;

// Show the connecting status without the previous blank-screen flash and
// blocking delay(500). The message persists until the next redraw, and
// BLEHid.loop() below drives the actual connect on this same tick.
if (BLEHid.isStarted() && !BLEHid.isConnected() && BLEHid.isConnectPending() && BLEHid.peerName())
{
drawScreen();
drawScreen("Connecting BLE HID", BLEHid.peerName());
delay(500);
}

BLEHid.loop();
if (!BLEHid.isConnected()) return 0;
Expand Down
188 changes: 159 additions & 29 deletions ats-mini/Remote.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,15 @@ static void remoteCaptureScreen(Stream* stream)
uint16_t width = spr.width();
uint16_t height = spr.height();

// Read the sprite framebuffer directly instead of calling readPixel()+printf()
// 54,400 times. spr.getPointer() returns the raw 16bpp framebuffer base; the
// sprite is created unrotated as 320x170 with no sub-viewport, so pixel (x,y)
// is at fb[x + y*width]. The stored word equals htons(spr.readPixel(x,y)), so
// emitting it as four lowercase hex digits is byte-for-byte identical to the
// previous per-pixel printf("%04x", htons(...)) output.
const uint16_t *fb = (const uint16_t *)spr.getPointer();
if(!fb) return;

// 14 bytes of BMP header
stream->println("");
stream->print("424d"); // BM
Expand All @@ -46,14 +55,90 @@ static void remoteCaptureScreen(Stream* stream)
stream->print("e0070000"); // Green mask
stream->println("1f000000"); // Blue mask

// Image data
// Image data: build each row in one buffer and emit it with a single write().
// The 320x170 sprite yields 1280 hex chars + CRLF per row; rowBuf lives in
// .bss (not on the cooperative loop's stack) and is safe because this path is
// single-entrant (reached only from remoteDoCommand()'s 'C' case).
static const char hex[] = "0123456789abcdef";
static char rowBuf[320 * 4 + 2];
for(int y=height-1 ; y>=0 ; y--)
{
const uint16_t *row = fb + (uint32_t)y * width;
char *p = rowBuf;
for(int x=0 ; x<width ; x++)
{
stream->printf("%04x", htons(spr.readPixel(x, y)));
uint16_t v = row[x]; // == htons(spr.readPixel(x, y))
*p++ = hex[(v >> 12) & 0xF];
*p++ = hex[(v >> 8) & 0xF];
*p++ = hex[(v >> 4) & 0xF];
*p++ = hex[ v & 0xF];
}
stream->println("");
*p++ = '\r'; *p++ = '\n'; // matches println("")
stream->write((const uint8_t *)rowBuf, p - rowBuf);
}
stream->flush();
}

//
// Capture the screen as a raw little-endian RGB565 BMP (command 'c').
//
// Additive, opt-in alternative to 'C': it emits ~2x fewer bytes (a binary BMP
// instead of ASCII hex) and does no per-pixel formatting. The decoded image is
// byte-for-byte identical to xxd-decoding the 'C' output. A leading
// "BMP:<size>\r\n" ASCII frame lets line-oriented consumers find the binary
// start and pre-allocate.
//
static void remoteCaptureScreenBinary(Stream* stream)
{
uint16_t width = spr.width();
uint16_t height = spr.height();
const uint16_t *fb = (const uint16_t *)spr.getPointer();
if(!fb) return;

uint32_t fileSize = 14 + 40 + 12 + (uint32_t)width * height * 2;
uint32_t pixOffset = 14 + 40 + 12;
uint32_t hsz = 40, compr = 3;
uint32_t w = width, ht = height;
uint32_t rm = 0xF800, gm = 0x07E0, bm = 0x001F;
uint16_t planes = 1, bpp = 16;

// BMP fields are little-endian; on the little-endian ESP32 write them in
// native order (do NOT reuse the htonl() trick the hex 'C' path uses).
uint8_t h[66];
h[0] = 'B'; h[1] = 'M';
memcpy(h + 2, &fileSize, 4);
memset(h + 6, 0, 4);
memcpy(h + 10, &pixOffset, 4);
memcpy(h + 14, &hsz, 4);
memcpy(h + 18, &w, 4);
memcpy(h + 22, &ht, 4);
memcpy(h + 26, &planes, 2);
memcpy(h + 28, &bpp, 2);
memcpy(h + 30, &compr, 4);
memset(h + 34, 0, 20);
memcpy(h + 54, &rm, 4);
memcpy(h + 58, &gm, 4);
memcpy(h + 62, &bm, 4);

stream->printf("BMP:%u\r\n", (unsigned int)fileSize);
stream->write(h, sizeof(h));

// Pixels, bottom-up. The framebuffer stores each RGB565 word byte-swapped
// (TFT_eSprite::drawPixel does color=(color>>8)|(color<<8) at 16bpp), so emit
// the high byte first to produce the little-endian RGB565 a BI_BITFIELDS BMP
// expects. These bytes equal xxd-decoding the 'C' output.
static uint8_t bmpRow[320 * 2];
for(int y=height-1 ; y>=0 ; y--)
{
const uint16_t *row = fb + (uint32_t)y * width;
uint8_t *p = bmpRow;
for(int x=0 ; x<width ; x++)
{
uint16_t v = row[x];
*p++ = (uint8_t)(v >> 8);
*p++ = (uint8_t)(v & 0xFF);
}
stream->write(bmpRow, p - bmpRow);
}
stream->flush();
}
Expand All @@ -62,20 +147,50 @@ char remoteReadChar(Stream* stream)
{
char key;

while (!stream->available());
// Bound the wait so a BLE disconnect mid-command (e.g. during '#' or '^')
// cannot spin the cooperative loop forever and trip the task watchdog. The
// deadline never fires on USB or on a well-behaved BLE host that sends the
// whole command at once; delay(1) yields to the scheduler while waiting.
uint32_t deadline = millis() + 2000;
while (!stream->available())
{
if ((int32_t)(millis() - deadline) >= 0) return 0; // abort signal
delay(1);
}
key = stream->read();
stream->print(key);
return key;
}

// Wait until a byte is available, then return it (via peek(), without consuming)
// as an int; return -1 if none arrives within the deadline. Multi-byte commands
// can dribble in over several loop ticks when typed in a terminal, so the parsers
// must block here rather than give up on a momentarily empty buffer. The deadline
// + delay(1) replace the previous unbounded busy-spin (`while(peek()==0xFF)`),
// which on this toolchain (char is unsigned on Xtensa, so peek()==-1 became 0xFF)
// would loop forever on a BLE disconnect and trip the task watchdog.
static int remotePeekBlocking(Stream* stream)
{
uint32_t deadline = millis() + 2000;
int peeked;
while ((peeked = stream->peek()) < 0)
{
if ((int32_t)(millis() - deadline) >= 0) return -1;
delay(1);
}
return peeked;
}

long int remoteReadInteger(Stream* stream)
{
long int result = 0;
while (true) {
char ch = stream->peek();
if (ch == 0xFF) {
continue;
} else if ((ch >= '0') && (ch <= '9')) {
int peeked = remotePeekBlocking(stream);
if (peeked < 0) {
return result;
}
char ch = (char)peeked;
if ((ch >= '0') && (ch <= '9')) {
ch = remoteReadChar(stream);
// Can overflow, but it's ok
result = result * 10 + (ch - '0');
Expand All @@ -89,28 +204,23 @@ void remoteReadString(Stream* stream, char *bufStr, uint8_t bufLen)
{
uint8_t length = 0;
while (true) {
char ch = stream->peek();
if (ch == 0xFF) {
continue;
} else if (ch == ',' || ch < ' ') {
int peeked = remotePeekBlocking(stream);
if (peeked < 0 || (char)peeked == ',' || (char)peeked < ' ') {
bufStr[length] = '\0';
return;
}
char ch = remoteReadChar(stream);
bufStr[length] = ch;
if (++length >= bufLen - 1) {
bufStr[length] = '\0';
return;
} else {
ch = remoteReadChar(stream);
bufStr[length] = ch;
if (++length >= bufLen - 1) {
bufStr[length] = '\0';
return;
}
}
}
}

static bool expectNewline(Stream* stream)
{
char ch;
while ((ch = stream->peek()) == 0xFF);
if (ch == '\r') {
if (remotePeekBlocking(stream) == '\r') {
stream->read();
return true;
}
Expand All @@ -119,8 +229,10 @@ static bool expectNewline(Stream* stream)

static bool remoteShowError(Stream* stream, const char *message)
{
// Consume the remaining input
while (stream->available()) remoteReadChar(stream);
// Drain the remaining input without echoing it (remoteReadChar() echoes each
// byte, which over BLE means an extra write() per leftover byte before the
// error message, and pollutes the stream for line-oriented consumers).
while (stream->available()) stream->read();
stream->printf("\r\nError: %s\r\n", message);
return false;
}
Expand Down Expand Up @@ -155,13 +267,24 @@ static bool remoteSetFrequency(Stream *stream)
return true;
}

static void remoteGetMemories(Stream* stream)
//
// Emit one populated memory slot per call. The "$" command starts the dump by
// setting state->memoryDumpSlot = 0; this is then driven once per loop tick so
// a BLE transfer (~2 KB/s) hands out one ~35-byte slot at a time instead of
// blocking the cooperative loop for ~1.7 s. The wire format is unchanged.
//
void remoteMemoryDumpTick(Stream* stream, RemoteState* state)
{
for (uint8_t i = 0; i < getTotalMemories(); i++) {
if (state->memoryDumpSlot < 0) return;

while (state->memoryDumpSlot < getTotalMemories()) {
uint8_t i = state->memoryDumpSlot++;
if (memories[i].freq) {
stream->printf("#%02d,%s,%ld,%s\r\n", i + 1, bands[memories[i].band].bandName, memories[i].freq, bandModeDesc[memories[i].mode]);
return;
}
}
state->memoryDumpSlot = -1;
}

static bool remoteSetMemory(Stream* stream)
Expand Down Expand Up @@ -328,10 +451,11 @@ void remotePrintStatus(Stream* stream, RemoteState* state)
//
void remoteTickTime(Stream* stream, RemoteState* state)
{
if(state->remoteLogOn && (millis() - state->remoteTimer >= 500))
uint32_t now = millis();
if(state->remoteLogOn && (now - state->remoteTimer >= 500))
{
// Mark time and increment diagnostic sequence number
state->remoteTimer = millis();
state->remoteTimer = now;
state->remoteSeqnum++;
// Show status
remotePrintStatus(stream, state);
Expand Down Expand Up @@ -435,12 +559,17 @@ int remoteDoCommand(Stream* stream, RemoteState* state, char key)
state->remoteLogOn = false;
remoteCaptureScreen(stream);
break;
case 'c':
state->remoteLogOn = false;
remoteCaptureScreenBinary(stream);
break;
case 't':
state->remoteLogOn = !state->remoteLogOn;
break;

case '$':
remoteGetMemories(stream);
// Start a chunked dump; slots are emitted by remoteMemoryDumpTick().
state->memoryDumpSlot = 0;
break;
case '#':
if (remoteSetMemory(stream))
Expand Down Expand Up @@ -475,6 +604,7 @@ static int serialLoop(Stream* stream, RemoteState* state, uint8_t usbMode)
if(usbMode == USB_OFF) return 0;

remoteTickTime(stream, state);
remoteMemoryDumpTick(stream, state);

if (stream->available())
return remoteDoCommand(stream, state, stream->read());
Expand Down
4 changes: 4 additions & 0 deletions ats-mini/Remote.h
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,13 @@ typedef struct {
uint32_t remoteTimer = millis();
uint8_t remoteSeqnum = 0;
bool remoteLogOn = false;
// Cursor for the chunked "$" memory dump (-1 = inactive). The dump emits one
// populated slot per loop tick so a BLE transfer does not block the main loop.
int16_t memoryDumpSlot = -1;
} RemoteState;

void remoteTickTime(Stream* stream, RemoteState* state);
void remoteMemoryDumpTick(Stream* stream, RemoteState* state);
int remoteDoCommand(Stream* stream, RemoteState* state, char key);
int serialLoop(uint8_t usbMode);
bool serialConsumeAbortPending(uint8_t usbMode);
Expand Down
8 changes: 7 additions & 1 deletion ats-mini/ats-mini.ino
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,13 @@ SI4735_fixed rx;
//
void setup()
{
// Enable serial port
// Enable serial port.
// Both build profiles use USBMode=hwcdc, so the 115200 baud argument is only a
// CDC line-coding label and does not limit throughput (USB FS runs much
// faster). Enlarging the TX ring (default 256 B) lets bulk output such as the
// screenshot drain more smoothly with fewer producer stalls; it must precede
// begin() and is drawn from internal DRAM, not the PSRAM sprite framebuffer.
Serial.setTxBufferSize(4096);
Serial.begin(115200);

// Encoder pins. Enable internal pull-ups
Expand Down
1 change: 1 addition & 0 deletions changelog/+ble-scan-rf.changed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Reduce Bluetooth RF interference and improve responsiveness during BLE HID pairing: scanning now uses a 25% duty cycle instead of 100%, and the blocking `delay()` calls and a screen flash during HID scan/connect were removed.
1 change: 1 addition & 0 deletions changelog/+remote-protocol-robustness.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fix several USB/Bluetooth remote-control issues: a Bluetooth LE disconnect in the middle of a command no longer spins the main loop until the watchdog reboots the device; `$` memory dumps now stream one slot per tick instead of freezing the UI over Bluetooth; and error replies no longer echo leftover input bytes.
1 change: 1 addition & 0 deletions changelog/+screenshot-binary.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add a compact binary screenshot command `c` (raw little-endian RGB565 BMP). It is about half the size on the wire of the hex `C` command and decodes to a byte-identical image, which helps especially over the slower Bluetooth LE link.
1 change: 1 addition & 0 deletions changelog/+screenshot-speed.changed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Greatly speed up screenshot capture (`C`): the framebuffer is now read directly and each row is sent in a single write instead of 54,400 per-pixel `printf` calls, freeing the main loop much sooner. The output is byte-for-byte identical. The USB TX buffer was also enlarged for smoother bulk transfers.
Loading
Loading