From bc76878f63d0ab234bed90cdecb3f00d7f800ee0 Mon Sep 17 00:00:00 2001 From: erikkaashoek Date: Mon, 23 Mar 2026 08:10:07 +0100 Subject: [PATCH] Working --- .github/copilot-instructions.md | 49 +++++ PROTOCOL.md | 377 ++++++++++++++++++++++++++++++++ main.c | 147 ++++++++++++- nanovna.h | 39 ++-- 4 files changed, 590 insertions(+), 22 deletions(-) create mode 100644 .github/copilot-instructions.md create mode 100644 PROTOCOL.md diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..ad9084f --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,49 @@ +# Copilot Instructions for tinySA + +## Build Environment + +Building requires the ARM GCC toolchain and MSYS2 utilities from ChibiStudio. + +Before running `make`, set up the environment by running `C:\ChibiStudio\start_gcc113.bat`. +This configures the following PATH entries: + +- `C:\ChibiStudio\tools\msys2\usr\bin` — provides `make`, `bash`, and other Unix utilities +- `C:\ChibiStudio\tools\openocd\bin` — OpenOCD for flashing +- `C:\ChibiStudio\tools\GNU Tools ARM Embedded\11.3 2022.08\bin` — ARM GCC 11.3 toolchain + +Or set up the PATH manually in a terminal before building: + +```bat +set PATH=C:\ChibiStudio\tools\msys2\usr\bin;%PATH% +set PATH=C:\ChibiStudio\tools\openocd\bin;%PATH% +set PATH=C:\ChibiStudio\tools\GNU Tools ARM Embedded\11.3 2022.08\arm-none-eabi\bin;%PATH% +set PATH=C:\ChibiStudio\tools\GNU Tools ARM Embedded\11.3 2022.08\bin;%PATH% +``` + +## Building + +To build for the **tinySA Ultra** (STM32F303): + +```sh +make TARGET=F303 +``` + +To flash: + +```sh +make TARGET=F303 dfu flash +``` + +## Project Structure + +- `sa_core.c` / `sa_cmd.c` / `ui.c` — main application logic +- `si4468.c` / `si4432.c` — RF chip drivers +- `main.c` — entry point +- `NANOVNA_STM32_F303/` — board-specific files for the tinySA (F303 target) +- `ChibiOS/` — RTOS + +## remote control protocol + +See 'PROTOCOL.md' for details on the remote control protocol used by the tinySA. + +tinySA is a resource constraint device. All code must use minimum CPU cycles, RAM and ROM. The codebase is written in C, with some assembly for critical sections. C++ is not used to avoid the overhead of exceptions and RTTI. diff --git a/PROTOCOL.md b/PROTOCOL.md new file mode 100644 index 0000000..7539078 --- /dev/null +++ b/PROTOCOL.md @@ -0,0 +1,377 @@ +# tinyGTC Remote Control & Screen Mirroring Protocol + +This document describes the serial communication protocol used between the **tinyGTC-App** Windows application and a compatible device. It is intended to enable third-party device firmware to be controlled and mirrored by this application. + +--- + +## 1. USB Device Identification + +The application discovers devices on the USB bus using the Windows Setup API. A device is considered compatible when both of the following match: + +| Property | Value | +|----------|-------| +| USB Vendor ID (VID) | `0x0483` (STMicroelectronics) | +| USB Product ID (PID) | `0x5741` (tinyGTC) or `0x5740` (tinySA) | + +The device must expose a virtual COM port (VCP). + +--- + +## 2. Serial Port Parameters + +| Parameter | Value | +|-----------|-------| +| Data bits | 8 | +| Stop bits | 1 | +| Parity | None | +| Flow control | None | + + +Timeouts configured by the host: + +| Timeout | Value | +|---------|-------| +| ReadIntervalTimeout | 50 ms | +| ReadTotalTimeoutConstant | 500 ms | +| ReadTotalTimeoutMultiplier | 10 ms/byte | +| WriteTotalTimeoutConstant | 500 ms | +| WriteTotalTimeoutMultiplier | 10 ms/byte | + +--- + +## 3. Initialization Sequence + +Upon connecting, the host performs the following steps: + +1. Flush any data already in the device's RX buffer. +2. Wait **100 ms**. +3. Send `scpi off\r` (disables SCPI mode on the device). +4. Wait **100 ms**. +5. Send `capt\r\n` (requests the first full screen capture). + +--- + +## 4. Host → Device Commands + +All commands are plain ASCII text terminated with `\r` (carriage return). A trailing `\n` (line feed) is also accepted. There is no binary encoding on the host-to-device path. + +| Command | Description | +|---------|-------------| +| `scpi off\r` | Disable SCPI/UART command mode on the device | +| `capt\r` | Request a full screen capture (device replies asynchronously; see Section 6.1) | +| `refresh on\r` | Enable automatic push of incremental screen updates | +| `refresh rle\r` | Enable automatic push of incremental screen updates using rle encoding | +| `refresh off\r` | Disable automatic push of incremental screen updates | +| `touch X Y\r` | Simulate a touch-screen press at pixel coordinate (X, Y) | +| `release\r` | Release a previous touch press | + +In case of a tinySA the host first sends 'refresh rle' en check if the command is understood. If not a 'refresh on' command is send and the pixel transfers will be done without rle. +If the 'refresh rle' command is accepted it is assumed the tinySA will use rle for pixel transfer +A tinyGTC will always use rle when transferring pixels. + +### Touch Coordinate System + +- Origin (0, 0) is the **top-left** corner of the display. +- X increases to the right; Y increases downward. +- Coordinates are in **device pixels** (not scaled), matching the display dimensions (see Section 8). +- The host converts scaled screen-window coordinates to device pixels before sending. + +### Command Interaction Notes + +- `touch` and `release` are typically used together: send `touch X Y\r`, wait ≥100 ms, then send `release\r`. +- `refresh on` causes the device to continuously push `bulk`, `fill`, and `flip` events as the screen changes. +- `refresh off` stops the push; the host can still request individual full captures with `capt`. + +--- + +## 5. Device → Host Protocol Overview + +The device communicates back to the host using a **line-based event model**: + +1. The device sends a **text event line** terminated with `\r\n`. +2. Immediately following the event line, it sends **binary payload data** (or no data, for lines that are purely informational). + +The host reads one `\r\n`-terminated line, inspects it for known keywords, and then reads the binary payload appropriate to that keyword. + +### Event Keywords + +The host performs a substring search on each received line: + +| Substring present in line | Event type | Binary payload follows? | +|---------------------------|------------|------------------------| +| `"apt"` **or** `"ture"` | Full screen capture | Yes – see Section 6.1 | +| `"ulk"` | Bulk region update | Yes – see Section 6.2 | +| `"ill"` | Fill region update | Yes – see Section 6.3 | +| `"lip"` | Flip (rotation change) | Yes – see Section 6.4 | +| (anything else) | Informational / ignored | No | + +> **Note:** The keyword matching is case-sensitive and uses partial substring matching, so the device is free to include additional text in the line (e.g. `"> capture\r\n"` matches `"apt"` and `"ture"`; `"> bulk\r\n"` matches `"ulk"`). + +--- + +## 6. Device → Host Binary Payloads + +### 6.1 Full Screen Capture + +**Trigger:** Event line containing `"apt"` or `"ture"`. + +**Payload:** Exactly `width × height` pixels, each pixel encoded as a 2-byte little-endian word in the compact RLE pixel format described in Section 7. + +- Pixels are ordered **left-to-right, top-to-bottom** (row-major). +- Rotation is fixed at **232** (standard landscape orientation) for full captures. +- The total uncompressed pixel count is always `width × height`; the RLE encoding means the byte count may be less, but the pixel count is exact. + +Example for a 480×320 display: the host reads exactly `480 × 320 = 153,600` decoded pixel values (each returned by one invocation of the pixel decoder in Section 7). + +--- + +### 6.2 Bulk Region Update + +**Trigger:** Event line containing `"ulk"`. + +**Payload structure:** + +| Bytes | Type | Description | +|-------|------|-------------| +| 2 | `uint16_t` little-endian | X – left edge of the region | +| 2 | `uint16_t` little-endian | Y – top edge of the region | +| 2 | `uint16_t` little-endian | W – width of the region | +| 2 | `uint16_t` little-endian | H – height of the region | +| W × H × ~2 bytes | compact pixel stream | Pixel data in Section 7 format | + +- Pixels are ordered row-by-row within the bounding rectangle. +- The region must be fully within the display: `x + w ≤ width`, `y + h ≤ height`. +- The host decodes exactly `W × H` pixels from the compact stream and writes them into the frame buffer at the correct positions, respecting the current rotation (Section 8). + +--- + +### 6.3 Fill Region Update + +**Trigger:** Event line containing `"ill"`. + +**Payload structure:** + +| Bytes | Type | Description | +|-------|------|-------------| +| 2 | `uint16_t` little-endian | X | +| 2 | `uint16_t` little-endian | Y | +| 2 | `uint16_t` little-endian | W | +| 2 | `uint16_t` little-endian | H | +| 2 | `uint16_t` **big-endian** | Fill color (standard RGB565, MSB first) | +| 2 | raw bytes | End marker: `0x00 0x40` | + +- The host fills the entire `W × H` rectangle with the given color. +- The end marker bytes are `0x00 0x40` (this is the byte-swapped representation of `RUN_END = 0x4000`). + +--- + +### 6.4 Flip (Rotation Change) + +**Trigger:** Event line containing `"lip"`. + +**Payload structure:** + +| Bytes | Type | Description | +|-------|------|-------------| +| 2 | `uint16_t` little-endian | X | +| 2 | `uint16_t` little-endian | Y | +| 2 | `uint16_t` little-endian | W | +| 2 | `uint16_t` little-endian | H | +| 2 | `uint16_t` little-endian | New rotation value | +| 2 | raw bytes | End marker: `0x00 0x40` | + +- The rotation value changes the mapping from incoming pixel data to the frame buffer (see Section 8). +- The end marker bytes are `0x00 0x40`. + +--- + +## 7. Compact Pixel Encoding (Inline RLE) + +All pixel streams (full captures and bulk regions) use the same compact 2-byte-per-word encoding. Each 2-byte word encodes **one color value plus a repeat count**, packing both into the bits of a standard 16-bit RGB565 word. + +### 7.1 Bit Layout + +The 16-bit word is received **little-endian** (low byte first over the wire): + +``` +Bit: [15][14][13][12][11][10][ 9][ 8][ 7][ 6][ 5][ 4][ 3][ 2][ 1][ 0] +Role: c c c B B B c c R R R c c G G G +``` + +- **Uppercase** = color bits: `B` (blue, 3 bits), `R` (red, 3 bits), `G` (green, 3 bits) +- **Lowercase** `c` = repeat-count bits (7 bits total) + +Mask constants: +``` +REPEAT_MASK = 0xe318 (bits [15:13], [9:8], [4:3]) +COLOR_MASK = 0x1ce7 (bits [12:10], [7:5], [2:0]) +``` + +### 7.2 Decoding Algorithm + +``` +Function ReadPixel() → rgb565_pixel: + + // If there are pending repeats, return the stored color + if remaining > 0: + remaining -= 1 + return current_color + + // Read 2 bytes little-endian from the serial stream + byte0, byte1 ← read 2 bytes + word ← byte0 | (byte1 << 8) + + // Extract 7-bit repeat count from the interleaved count bits + remaining ← ((word & 0xe000) >> 9) // bits [15:13] → count bits [6:4] + | ((word & 0x0300) >> 6) // bits [9:8] → count bits [3:2] + | ((word & 0x0018) >> 3) // bits [4:3] → count bits [1:0] + // remaining is now 0..127 + + // Fill the count bit positions with 1s to complete the RGB565 word + word |= 0xe318 + + // Byte-swap to produce standard big-endian RGB565 + pixel ← ((word & 0x00ff) << 8) | ((word & 0xff00) >> 8) + + current_color ← pixel + return pixel +``` + +> **Pixel delivery:** the first call delivers the pixel once; subsequent calls deliver the same pixel `remaining` more times without reading from the stream. The total delivery count per encoded word is `1 + remaining` (i.e. 1 to 128). + +### 7.3 Color Fidelity Note + +The encoding trades 7 color LSBs for the run-length count, so each channel carries only 3 significant bits out of the RGB565 5/6/5 bit allocation. The reconstruction forces the count-bit positions to `1`, so the decoded RGB565 color has certain bits always set (`0xe318` after the byte-swap), resulting in slight color quantization compared to an uncompressed RGB565 stream. The host applies no further correction. + +### 7.4 Encoding a Pixel Word (for device firmware implementors) + +``` +Function EncodePixel(rgb565_pixel, repeat_count) → 2 bytes: + // repeat_count: 0 = send once, 127 = send 128 times + // rgb565_pixel is standard RGB565 (big-endian convention) + + // Byte-swap to match wire format + word ← ((rgb565_pixel & 0x00ff) << 8) | ((rgb565_pixel & 0xff00) >> 8) + + // Clear the count bit positions and insert count + word &= COLOR_MASK // 0x1ce7 – keep only color bits + word |= ( (((repeat_count) << 9) & 0xe000) // count bits [6:4] → [15:13] + |(((repeat_count) << 6) & 0x0300) // count bits [3:2] → [9:8] + |(((repeat_count) << 3) & 0x0018) ) // count bits [1:0] → [4:3] + + // Transmit little-endian + byte0 ← word & 0xff + byte1 ← (word >> 8) & 0xff + send byte0, byte1 +``` + +> **Important:** When encoding, first byte-swap the pixel (so it is stored in the same non-standard orientation that the receiver expects), then clear color bits, then insert the count. The color bits stored in the wire format are the 3 MSBs of each channel from the original RGB565 value (after the byte-swap operation). + +--- + +## 8. Rotation / Coordinate Mapping + +The rotation value (set by `flip` events) controls how bulk pixel data is written into the frame buffer. + +| Rotation value | Meaning | Pixel mapping | +|----------------|---------|---------------| +| `232` | Standard landscape | `framebuffer[(y + row) * width + (x + col)] = pixel` (row-major, direct) | +| `136` | Rotated (portrait) | `destX = y + row; destY = height − (x + col); framebuffer[destY * width + destX] = pixel` | + +The full-screen capture and fill operations always use rotation `232` regardless of the current rotation state. + +--- + +## 9. Supported Device Screen Sizes + +The host must know the display dimensions in advance (configured via command-line argument or defaulting to tinyGTC size). The device does not announce its dimensions over the protocol. + +| Device | Width (pixels) | Height (pixels) | +|--------|---------------|-----------------| +| tinyGTC (default) | 480 | 320 | +| tinyGTC Ultra | 480 | 320 | +| tinySA Ultra | 480 | 320 | +| NanoVNA-H4 | 480 | 320 | +| tinySA | 320 | 240 | +| NanoVNA-H | 320 | 240 | + +--- + +## 10. Frame Buffer Format + +The host maintains an in-memory frame buffer of `width × height` 16-bit RGB565 values (stored as `uint32_t` but only the lower 16 bits are used). The host converts to 32-bit BGRA for display: + +``` +R = ((rgb565 & 0xF800) >> 11) << 3 // 5-bit red → 8-bit +G = ((rgb565 & 0x07E0) >> 5) << 2 // 6-bit green → 8-bit +B = (rgb565 & 0x001F) << 3 // 5-bit blue → 8-bit +``` + +Optional invert mode: `R = 255 − R`, `G = 255 − G`, `B = 255 − B`. + +--- + +## 11. Connection Management + +- The host auto-discovers the first matching USB VCP at startup. +- Connection health is checked every **1000 ms** via `ClearCommError`; hardware errors (`CE_BREAK`, `CE_FRAME`, `CE_OVERRUN`, `CE_RXOVER`, `CE_TXFULL`) indicate lost connection. +- On disconnect, the host waits **500 ms** then attempts to re-open and re-initialize the device. +- On re-connection, the host does **not** automatically re-enable `refresh on`; the device resumes the capture stream via `capt`. +- On clean disconnect, the host sends `refresh off\r` before closing the port. + +--- + +## 12. Protocol State Machine Summary + +``` +HOST DEVICE + | | + |── scpi off\r ──────────────────────────────► | (disable SCPI) + |── capt\r\n ─────────────────────────────────► | (request first frame) + | | + | ◄──────────── "> capture\r\n" ────────────── | (event line) + | ◄──── width×height pixels (compact RLE) ───── | (pixel stream) + | | + | [user enables auto-refresh] | + |── refresh on\r ──────────────────────────────► | + | | + | ◄──────────── "> bulk\r\n" ────────────────── | (dirty region) + | ◄─── 8-byte header + W×H pixels ──────────── | + | | + | ◄──────────── "> fill\r\n" ────────────────── | (solid fill) + | ◄─── 8-byte header + 2-byte color + 2-byte end marker ─── | + | | + | ◄──────────── "> flip\r\n" ────────────────── | (rotation) + | ◄─── 8-byte header + 2-byte rotation + 2-byte end marker ─| + | | + | [user clicks on mirrored screen] | + |── touch 123 87\r ────────────────────────────► | + | [wait ~100ms] | + |── release\r ─────────────────────────────────► | + | | + |── refresh off\r ─────────────────────────────► | (on disconnect) +``` + +--- + +## 13. Implementing a Compatible Device + +To make a device controllable by this application, the firmware must: + +1. **Enumerate as a USB CDC VCP** with `VID=0x0483`, appropriate PID, and bus-reported device description containing `"tinyGTC Command port"`. + +2. **Accept the serial commands** in Section 4 on the VCP UART at 115200 8N1. + +3. **On `scpi off\r`:** disable any SCPI mode and prepare for the remote protocol. + +4. **On `capt\r`:** send the event line `"> capture\r\n"` (or any line containing `"apt"` or `"ture"`), immediately followed by the full pixel stream (Section 6.1) using the compact pixel encoding (Section 7). + +5. **For incremental updates (when `refresh on` is active):** for each display region that changes, send one of: + - A line containing `"ulk"` + 8-byte header + pixel stream (Section 6.2) for arbitrary content. + - A line containing `"ill"` + 8-byte header + fill color + end marker (Section 6.3) for solid-color fills. + - A line containing `"lip"` + 8-byte header + rotation + end marker (Section 6.4) when the display rotation changes. + +6. **On `touch X Y\r`:** simulate a touchscreen press at the given coordinates. + +7. **On `release\r`:** release the simulated touch. + diff --git a/main.c b/main.c index 76614b5..8159495 100644 --- a/main.c +++ b/main.c @@ -966,13 +966,69 @@ VNA_SHELL_FUNCTION(cmd_remark) #ifdef __REMOTE_DESKTOP__ uint8_t remote_mouse_down = false; uint8_t auto_capture = false; +uint8_t rle = false; + +// RLE-encode a pixel buffer and send it over the USB shell stream. +// buf : pixel_t array in tinySA wire format (byte-swapped RGB565) +// count : number of pixels +// Used for bulk region updates (PROTOCOL.md §6.2 / §7). No RLE terminator +// is appended: the host reads exactly W*H decoded pixels by count. +// +// out[32] layout: up to 29 run-code words, then 1 last-run word + 2 words +// for the "ch> " prompt = 32 total. Only ONE streamWrite at the final flush +// avoids USB CDC packet ordering issues that arise from multiple writes. +void do_send_rle(const uint16_t *buf, int count) +{ + uint16_t out[32]; + int outidx = 0; + uint16_t cur = 0; + int run = 0; + + while (count-- > 0) { + uint16_t p = (*buf++) & RLE_COLOR_MASK; + if (run == 0 || (cur & RLE_COLOR_MASK) != p || run >= 128) { + if (run > 0) { + out[outidx++] = cur; + if (outidx >= 29) { // flush; keep 3 slots: last-run + 2x "ch> " + streamWrite(shell_stream, (void*)out, (uint16_t)(outidx * 2)); + //osalThreadSleepMilliseconds(10); + outidx = 0; + } + } + cur = p; // repeat_count = 0 → pixel delivered once + run = 1; + } else { + cur = p | RLE_ENCODE_COUNT(run); + run++; + } + } + // Append last run + "ch> " into the same buffer; one send, zero extra packets + if (run > 0) + out[outidx++] = cur; + out[outidx++] = (uint16_t)('c' | ('h' << 8)); // bytes: 'c', 'h' + out[outidx++] = (uint16_t)('>' | (' ' << 8)); // bytes: '>', ' ' + streamWrite(shell_stream, (void*)out, (uint16_t)(outidx * 2)); + //osalThreadSleepMilliseconds(10); +} void send_region(remote_region_t *rd, uint8_t * buf, uint16_t size) { if (SDU1.config->usbp->state == USB_ACTIVE) { - streamWrite(shell_stream, (void*) rd, sizeof(remote_region_t)); - streamWrite(shell_stream, (void*) buf, size); - streamWrite(shell_stream, (void*)"ch> ", 4); + // Write tag and coordinates as two explicit writes to avoid any + // struct-layout / sizeof ambiguity (same pattern as cmd_capt). + const int16_t dims[4] = {rd->x, rd->y, rd->w, rd->h}; + streamWrite(shell_stream, (void*) rd->new_str, 6); +// osalThreadSleepMilliseconds(1); + streamWrite(shell_stream, (void*) dims, 8); +// osalThreadSleepMilliseconds(1); + if (rle && size > 2) { // bulk pixels -> RLE encode + do_send_rle((const uint16_t *)buf, size >> 1); + } else { // fill/flip or raw-pixel mode + streamWrite(shell_stream, (void*) buf, size); +// osalThreadSleepMilliseconds(1); + streamWrite(shell_stream, (void*)"ch> ", 4); +// osalThreadSleepMilliseconds(1); + } } else auto_capture = false; @@ -980,12 +1036,20 @@ void send_region(remote_region_t *rd, uint8_t * buf, uint16_t size) VNA_SHELL_FUNCTION(cmd_refresh) { -// read pixel count at one time (PART*2 bytes required for read buffer) - int m = generic_option_cmd("refresh", "off|on", argc, argv[0]); - if (m>=0) { - auto_capture = m; - } + int m = generic_option_cmd("refresh", "off|on|rle", argc, argv[0]); + if (m == 0) { auto_capture = false; rle = false; } + else if (m == 1) { auto_capture = true; rle = false; } + else if (m == 2) { auto_capture = true; rle = true; } } + +VNA_SHELL_FUNCTION(cmd_rle) +{ + int m = generic_option_cmd("rle", "off|on", argc, argv[0]); + if (m == 0) { rle = false; } + else if (m == 1) { rle = true; } +} + + VNA_SHELL_FUNCTION(cmd_touch) { if (argc != 2) return; @@ -1028,6 +1092,71 @@ VNA_SHELL_FUNCTION(cmd_capture) } } +// RLE-encoded screen capture. Sends a "bulk\r\n" + x,y,w,h header (14 bytes) +// using two explicit writes to avoid struct-layout ambiguity, then RLE encodes +// the full screen in 2-row chunks reusing spi_buffer (no extra RAM). RLE state +// is kept across row boundaries so runs are never split artificially. +// "ch> " prompt is left to the shell on return. +VNA_SHELL_FUNCTION(cmd_capt) +{ + (void)argc; + (void)argv; +#ifdef TINYSA4 +#if SPI_BUFFER_SIZE < (2*LCD_WIDTH) +#error "Low size of spi_buffer for cmd_capt" +#endif +#else +#if SPI_BUFFER_SIZE < (3*LCD_WIDTH + 1) +#error "Low size of spi_buffer for cmd_capt" +#endif +#endif +#if 0 + // Send "bulk\r\n" header (6 bytes) then x,y,w,h (8 bytes) explicitly + // to avoid any compiler struct-layout ambiguity. + static const char capt_tag[] = "bulk\r\n"; + const int16_t capt_dims[4] = {0, 0, LCD_WIDTH, LCD_HEIGHT}; + streamWrite(shell_stream, (void*)capt_tag, 6); + osalThreadSleepMilliseconds(1); + streamWrite(shell_stream, (void*)capt_dims, 8); + osalThreadSleepMilliseconds(1); +#endif + uint16_t out[32]; + int outidx = 0; + uint16_t cur = 0; + int run = 0; + int y; + for (y = 0; y < LCD_HEIGHT; y += 2) { + ili9341_read_memory(0, y, LCD_WIDTH, 2, spi_buffer); + const uint16_t *px = (const uint16_t *)spi_buffer; + int n = 2 * LCD_WIDTH; + while (n-- > 0) { + uint16_t p = (*px++) & RLE_COLOR_MASK; + if (run == 0 || (cur & RLE_COLOR_MASK) != p || run >= 128) { + if (run > 0) { + out[outidx++] = cur; + if (outidx >= 29) { + streamWrite(shell_stream, (void*)out, (uint16_t)(outidx * 2)); + osalThreadSleepMilliseconds(1); + outidx = 0; + } + } + cur = p; + run = 1; + } else { + cur = p | RLE_ENCODE_COUNT(run); + run++; + } + } + } + // flush last run; shell appends "ch> " prompt automatically after return + if (run > 0) + out[outidx++] = cur; + if (outidx > 0) { + streamWrite(shell_stream, (void*)out, (uint16_t)(outidx * 2)); + osalThreadSleepMilliseconds(1); + } +} + #ifdef ENABLE_SD_CARD_CMD #ifndef __USE_SD_CARD__ #error "Need enable SD card support __USE_SD_CARD__ in nanovna.h, for use ENABLE_SD_CARD_CMD" @@ -2489,8 +2618,10 @@ static const VNAShellCommand commands[] = {"usart_cfg" , cmd_usart_cfg , CMD_WAIT_MUTEX | CMD_RUN_IN_LOAD}, #endif {"capture" , cmd_capture , CMD_WAIT_MUTEX | CMD_RUN_IN_UI}, + {"capt" , cmd_capt , CMD_WAIT_MUTEX | CMD_RUN_IN_UI}, #ifdef __REMOTE_DESKTOP__ {"refresh" , cmd_refresh , 0}, + {"rle" , cmd_rle , 0}, {"touch" , cmd_touch , 0}, {"release" , cmd_release , 0}, #endif diff --git a/nanovna.h b/nanovna.h index 18fd053..0f92c78 100644 --- a/nanovna.h +++ b/nanovna.h @@ -296,6 +296,17 @@ const char *get_hw_version_text(void); #ifdef __REMOTE_DESKTOP__ extern uint8_t remote_mouse_down; extern uint8_t auto_capture; +extern uint8_t rle_capture; // When set, bulk pixel data is RLE-encoded +// RLE pixel encoding (PROTOCOL.md §7): +// 16-bit wire word (transmitted LE): colour bits in 0x1ce7, count bits in 0xe318 +// repeat_count 0 = pixel appears once, 127 = appears 128 times +#define RLE_COLOR_MASK 0x1ce7u +#define RLE_REPEAT_MASK 0xe318u +#define RLE_RUN_END 0x4000u +#define RLE_ENCODE_COUNT(n) ( (((uint16_t)(n) << 9) & 0xe000u) \ + | (((uint16_t)(n) << 6) & 0x0300u) \ + | (((uint16_t)(n) << 3) & 0x0018u) ) +void do_send_rle(const uint16_t *buf, int count); typedef struct { char new_str[6]; int16_t x; @@ -1100,14 +1111,14 @@ typedef uint16_t pixel_t; [LCD_BG_COLOR ] = RGB565( 0, 0, 0), \ [LCD_FG_COLOR ] = RGB565(255,255,255), \ [LCD_GRID_COLOR ] = RGB565(128,128,128), \ -[LCD_MENU_COLOR ] = RGB565(230,230,230), \ +[LCD_MENU_COLOR ] = RGB565(224,224,224), \ [LCD_MENU_TEXT_COLOR ] = RGB565( 0, 0, 0), \ -[LCD_MENU_ACTIVE_COLOR] = RGB565(210,210,210), \ +[LCD_MENU_ACTIVE_COLOR] = RGB565(192,192,192), \ [LCD_TRACE_1_COLOR ] = RGB565(255,255, 0), \ [LCD_TRACE_2_COLOR ] = RGB565( 64,255, 64), \ [LCD_TRACE_3_COLOR ] = RGB565(255, 64, 64), \ [LCD_TRACE_4_COLOR ] = RGB565(255, 0,255), \ -[LCD_NORMAL_BAT_COLOR ] = RGB565( 31,227, 0), \ +[LCD_NORMAL_BAT_COLOR ] = RGB565( 32,224, 0), \ [LCD_LOW_BAT_COLOR ] = RGB565(255, 0, 0), \ [LCD_TRIGGER_COLOR ] = RGB565( 0, 0,255), \ [LCD_RISE_EDGE_COLOR ] = RGB565(255,255,255), \ @@ -1119,10 +1130,10 @@ typedef uint16_t pixel_t; [LCD_BRIGHT_COLOR_BLUE] = RGB565( 0, 0,255), \ [LCD_BRIGHT_COLOR_RED ] = RGB565(255,128,128), \ [LCD_BRIGHT_COLOR_GREEN]= RGB565( 0,255, 0), \ -[LCD_DARK_GREY ] = RGB565(140,140,140), \ -[LCD_LIGHT_GREY ] = RGB565(220,220,220), \ -[LCD_HAM_COLOR ] = RGB565( 80, 80, 80), \ -[LCD_GRID_VALUE_COLOR ] = RGB565(196,196,196), \ +[LCD_DARK_GREY ] = RGB565(160,160,160), \ +[LCD_LIGHT_GREY ] = RGB565(224,224,224), \ +[LCD_HAM_COLOR ] = RGB565( 96, 96, 96), \ +[LCD_GRID_VALUE_COLOR ] = RGB565(192,192,192), \ [LCD_M_REFERENCE ] = RGB565(255,255,255), \ [LCD_M_DELTA ] = RGB565( 0,255, 0), \ [LCD_M_NOISE ] = RGB565( 0,255,255), \ @@ -1133,14 +1144,14 @@ typedef uint16_t pixel_t; [LCD_BG_COLOR ] = RGB565( 0, 0, 0), \ [LCD_FG_COLOR ] = RGB565(255,255,255), \ [LCD_GRID_COLOR ] = RGB565(128,128,128), \ -[LCD_MENU_COLOR ] = RGB565(230,230,230), \ +[LCD_MENU_COLOR ] = RGB565(224,224,224), \ [LCD_MENU_TEXT_COLOR ] = RGB565( 0, 0, 0), \ -[LCD_MENU_ACTIVE_COLOR] = RGB565(210,210,210), \ +[LCD_MENU_ACTIVE_COLOR] = RGB565(192,192,192), \ [LCD_TRACE_1_COLOR ] = RGB565(255,255, 0), \ [LCD_TRACE_2_COLOR ] = RGB565( 64,255, 64), \ [LCD_TRACE_3_COLOR ] = RGB565(255, 0,255), \ [LCD_TRACE_4_COLOR ] = RGB565(255, 64, 64), \ -[LCD_NORMAL_BAT_COLOR ] = RGB565( 31,227, 0), \ +[LCD_NORMAL_BAT_COLOR ] = RGB565( 32,224, 0), \ [LCD_LOW_BAT_COLOR ] = RGB565(255, 0, 0), \ [LCD_TRIGGER_COLOR ] = RGB565( 0, 0,255), \ [LCD_RISE_EDGE_COLOR ] = RGB565(255,255,255), \ @@ -1152,10 +1163,10 @@ typedef uint16_t pixel_t; [LCD_BRIGHT_COLOR_BLUE] = RGB565( 0, 0,255), \ [LCD_BRIGHT_COLOR_RED ] = RGB565(255,128,128), \ [LCD_BRIGHT_COLOR_GREEN]= RGB565( 0,255, 0), \ -[LCD_DARK_GREY ] = RGB565(140,140,140), \ -[LCD_LIGHT_GREY ] = RGB565(220,220,220), \ -[LCD_HAM_COLOR ] = RGB565( 40, 40, 40), \ -[LCD_GRID_VALUE_COLOR ] = RGB565(196,196,196), \ +[LCD_DARK_GREY ] = RGB565(160,160,160), \ +[LCD_LIGHT_GREY ] = RGB565(224,224,224), \ +[LCD_HAM_COLOR ] = RGB565( 32, 32, 32), \ +[LCD_GRID_VALUE_COLOR ] = RGB565(192,192,192), \ [LCD_M_REFERENCE ] = RGB565(255,255,255), \ [LCD_M_DELTA ] = RGB565( 0,255, 0), \ [LCD_M_NOISE ] = RGB565( 0,255,255), \