diff --git a/src/communication.cpp b/src/communication.cpp index 69d9f7e..af615e0 100644 --- a/src/communication.cpp +++ b/src/communication.cpp @@ -162,7 +162,7 @@ void sendResponse(uint8_t* response, uint8_t len) { uint16_t command = (response[0] << 8) | response[1]; uint8_t status = (len >= 3) ? response[2] : 0x00; // Encrypt all authenticated responses except auth/version handshakes and FE/FF status. - // (0x0070–0x0074 direct-write/LED acks must be encrypted too — LAN/BLE clients decrypt every response.) + // Direct-write / partial-write / LED acks must be encrypted too; LAN/BLE clients decrypt every response. if (command != 0x0050 && command != 0x0043 && status != 0xFE && status != 0xFF) { uint8_t nonce[16]; uint8_t auth_tag[12]; @@ -562,6 +562,9 @@ void imageDataWritten(BLEConnHandle conn_hdl, BLECharPtr chr, uint8_t* data, uin writeSerial("=== DIRECT WRITE END COMMAND (0x0072) ==="); handleDirectWriteEnd(data + 2, len - 2); break; + case 0x0076: + handlePartialWriteStart(data + 2, len - 2); + break; case 0x0073: writeSerial("=== LED ACTIVATE COMMAND (0x0073) ==="); handleLedActivate(data + 2, len - 2); diff --git a/src/display_service.cpp b/src/display_service.cpp index 5224920..a20e94f 100644 --- a/src/display_service.cpp +++ b/src/display_service.cpp @@ -50,10 +50,46 @@ extern bool directWritePlane2; extern bool directWriteBitplanes; extern bool directWriteCompressed; extern bool directWriteActive; +extern uint8_t directWriteDataKind; extern uint8_t* compressedDataBuffer; extern uint8_t dictionaryBuffer[]; extern uint8_t decompressionChunk[]; +extern uint32_t displayed_etag; + +static const uint8_t DATA_KIND_NONE = 0u; +static const uint8_t DATA_KIND_FULL = 1u; +static const uint8_t DATA_KIND_PARTIAL = 2u; + +static const uint8_t ERR_ETAG_MISMATCH = 0x01u; +static const uint8_t ERR_MIXED_DATA = 0x02u; +static const uint8_t ERR_RECT_OOB = 0x03u; +static const uint8_t ERR_PARTIAL_VERSION = 0x04u; +static const uint8_t ERR_RECT_ALIGN = 0x05u; +static const uint8_t ERR_PARTIAL_FLAGS = 0x06u; +static const uint8_t ERR_PARTIAL_SIZE = 0x07u; +static const uint8_t ERR_PARTIAL_STREAM = 0x08u; +static const uint8_t ERR_PARTIAL_UNSUPPORTED = 0x09u; + +static const uint8_t PARTIAL_WRITE_PROTOCOL_V1 = 0x01u; +static const uint16_t PARTIAL_FLAG_COMPRESSED = 0x0004u; +static const uint16_t PARTIAL_FLAG_STORE_ETAG = 0x0008u; + +struct PartialStreamContext { + bool active; + uint16_t flags; + uint16_t x; + uint16_t y; + uint16_t width; + uint16_t height; + uint32_t expected_stream_size; + uint32_t logical_bytes_written; + uint32_t bytes_remaining_in_phase; + uint32_t old_plane_bytes_written; + uint32_t new_plane_bytes_written; + uint8_t phase; // 0 = old image (PLANE_1), 1 = new image (PLANE_0), 2 = complete +}; + uint32_t max_compressed_image_rx_bytes(uint8_t tm) { if ((tm & TRANSMISSION_MODE_ZIP) == 0) return 0; if ((tm & TRANSMISSION_MODE_ZIPXL) != 0 && @@ -83,6 +119,13 @@ void bbepStartWrite(BBEPDISP *pBBEP, int iPlane); void bbepWriteData(BBEPDISP *pBBEP, uint8_t *pData, int iLen); bool bbepIsBusy(BBEPDISP *pBBEP); void flashLed(uint8_t color, uint8_t brightness); +static void clear_partial_planes_to_white(void); +static void setup_phase(void); +static bool partial_consume_bytes(uint8_t* data, uint32_t len); +static bool decompress_partial_stream(void); +static uint32_t calc_controller_plane_bytes(uint16_t width, uint16_t height); +static void send_direct_write_nack(uint8_t opcode, uint8_t error, bool cleanupState); +static PartialStreamContext partialCtx = {}; #define AXP2101_SLAVE_ADDRESS 0x34 #define AXP2101_REG_POWER_STATUS 0x00 #define AXP2101_REG_DC_ONOFF_DVM_CTRL 0x80 @@ -186,6 +229,7 @@ int mapEpd(int id){ case 0x003F: return EP31_240x320; case 0x0040: return EP75YR_800x480; case 0x0041: return EP_PANEL_UNDEFINED; + case 0x0042: return EP133_960x680; default: return EP_PANEL_UNDEFINED; } } @@ -1085,6 +1129,8 @@ void updatemsdata(){ } void handleDirectWriteCompressedData(uint8_t* data, uint16_t len) { + // Compressed full-image 0x71 chunks are buffered as compressed bytes. + // uzlib is run once at 0x72, so we never keep a decompressed full image in RAM. if (!compressedDataBuffer) { cleanupDirectWriteState(false); uint8_t errorResponse[] = {0xFF, 0xFF}; @@ -1108,6 +1154,8 @@ void handleDirectWriteCompressedData(uint8_t* data, uint16_t len) { } void decompressDirectWriteData() { + // Inflate the buffered compressed full-image stream and write each output + // chunk directly to the active panel write window. if (directWriteCompressedReceived == 0) return; struct uzlib_uncomp d; memset(&d, 0, sizeof(d)); @@ -1156,6 +1204,8 @@ void cleanupDirectWriteState(bool refreshDisplay) { directWriteTotalBytes = 0; directWriteRefreshMode = 0; directWriteStartTime = 0; + directWriteDataKind = DATA_KIND_NONE; + memset(&partialCtx, 0, sizeof(partialCtx)); if (refreshDisplay && displayPowerState) { #if defined(TARGET_ESP32) && defined(OPENDISPLAY_SEEED_GFX) if (seeed_driver_used()) { @@ -1172,7 +1222,13 @@ void cleanupDirectWriteState(bool refreshDisplay) { } void handleDirectWriteStart(uint8_t* data, uint16_t len) { + if (partialCtx.active) { + send_direct_write_nack(0x70, ERR_MIXED_DATA, true); + return; + } if (directWriteActive) cleanupDirectWriteState(false); + bool isCompressedStart = (len >= 4); + #if defined(TARGET_ESP32) && defined(OPENDISPLAY_SEEED_GFX) if (seeed_driver_used()) { seeed_gfx_prepare_hardware(); @@ -1181,7 +1237,7 @@ void handleDirectWriteStart(uint8_t* data, uint16_t len) { uint8_t colorScheme = globalConfig.displays[0].color_scheme; directWriteBitplanes = (colorScheme == 1 || colorScheme == 2); directWritePlane2 = false; - directWriteCompressed = (len >= 4); + directWriteCompressed = isCompressedStart; directWriteWidth = globalConfig.displays[0].pixel_width; directWriteHeight = globalConfig.displays[0].pixel_height; uint32_t pixels = (uint32_t)directWriteWidth * (uint32_t)directWriteHeight; @@ -1240,12 +1296,179 @@ void handleDirectWriteStart(uint8_t* data, uint16_t len) { sendResponse(ackResponse, sizeof(ackResponse)); } +void handlePartialWriteStart(uint8_t* data, uint16_t len) { + if (directWriteActive) cleanupDirectWriteState(false); + +#if defined(TARGET_ESP32) && defined(OPENDISPLAY_SEEED_GFX) + if (seeed_driver_used()) { + send_direct_write_nack(0x76, ERR_PARTIAL_FLAGS, false); + return; + } +#endif + + if (len < 19 || data[0] != PARTIAL_WRITE_PROTOCOL_V1) { + send_direct_write_nack(0x76, ERR_PARTIAL_VERSION, false); + return; + } + + uint16_t flags = ((uint16_t)data[1] << 8) | data[2]; + uint32_t oldEtag = ((uint32_t)data[3] << 24) | ((uint32_t)data[4] << 16) | + ((uint32_t)data[5] << 8) | (uint32_t)data[6]; + uint16_t rectX = ((uint16_t)data[7] << 8) | data[8]; + uint16_t rectY = ((uint16_t)data[9] << 8) | data[10]; + uint16_t rectW = ((uint16_t)data[11] << 8) | data[12]; + uint16_t rectH = ((uint16_t)data[13] << 8) | data[14]; + uint32_t uncompSize; + memcpy(&uncompSize, data + 15, 4); // 4-byte LE + + // Validate reserved flags. Only compressed and store-etag are defined. + if (flags & ~(PARTIAL_FLAG_COMPRESSED | PARTIAL_FLAG_STORE_ETAG)) { + send_direct_write_nack(0x76, ERR_PARTIAL_FLAGS, false); + return; + } + + if (oldEtag == 0 || displayed_etag == 0 || displayed_etag != oldEtag) { + send_direct_write_nack(0x76, ERR_ETAG_MISMATCH, false); + return; + } + + uint16_t dispW = globalConfig.displays[0].pixel_width; + uint16_t dispH = globalConfig.displays[0].pixel_height; + if (getBitsPerPixel() != 1) { + // bb_epaper partial refresh support is effectively non-existent for + // 2bpp+ panels, and physical panels may not support that mode either. + // This protocol uses two 1bpp controller planes as old/new image memory. + send_direct_write_nack(0x76, ERR_PARTIAL_UNSUPPORTED, false); + return; + } + + if (rectW == 0 || rectH == 0 || + (uint32_t)rectX + rectW > dispW || + (uint32_t)rectY + rectH > dispH) { + send_direct_write_nack(0x76, ERR_RECT_OOB, false); + return; + } + + if ((rectX & 7u) != 0 || (rectW & 7u) != 0) { + send_direct_write_nack(0x76, ERR_RECT_ALIGN, false); + return; + } + + uint32_t planeBytes = calc_controller_plane_bytes(rectW, rectH); + if (uncompSize != planeBytes * 2u) { + send_direct_write_nack(0x76, ERR_PARTIAL_SIZE, false); + return; + } + + bool isCompressed = (flags & PARTIAL_FLAG_COMPRESSED) != 0; + if (isCompressed && !compressedDataBuffer) { + displayed_etag = 0; + uint8_t errResponse[] = {0xFF, 0xFF}; + sendResponse(errResponse, sizeof(errResponse)); + return; + } + + // Hardware init + directWriteWidth = dispW; + directWriteHeight = dispH; + directWriteActive = true; + directWriteDataKind = DATA_KIND_PARTIAL; + directWriteStartTime = millis(); + if (displayPowerState) { + pwrmgm(false); + delay(50); + } + pwrmgm(true); + bbepInitIO(&bbep, globalConfig.displays[0].dc_pin, globalConfig.displays[0].reset_pin, + globalConfig.displays[0].busy_pin, globalConfig.displays[0].cs_pin, + globalConfig.displays[0].data_pin, globalConfig.displays[0].clk_pin, 8000000); + bbepWakeUp(&bbep); + if (bbep.pInitPart != NULL) { + bbepSendCMDSequence(&bbep, bbep.pInitPart); + } else { + bbepSendCMDSequence(&bbep, bbep.pInitFull); + } + clear_partial_planes_to_white(); + + // Initialize stream context + memset(&partialCtx, 0, sizeof(partialCtx)); + partialCtx.active = true; + partialCtx.flags = flags; + partialCtx.x = rectX; + partialCtx.y = rectY; + partialCtx.width = rectW; + partialCtx.height = rectH; + partialCtx.expected_stream_size = uncompSize; + + if (isCompressed) { + directWriteCompressedBuffer = compressedDataBuffer; + directWriteCompressedReceived = 0; + } + + // Process optional initial stream bytes before ACK + if (len > 19) { + uint16_t initLen = len - 19; + if (isCompressed) { + uint32_t cap = max_compressed_image_rx_bytes(globalConfig.displays[0].transmission_modes); + if (cap == 0 || initLen > cap) { + send_direct_write_nack(0x76, ERR_PARTIAL_STREAM, true); + return; + } + memcpy(directWriteCompressedBuffer, data + 19, initLen); + directWriteCompressedReceived = initLen; + } else { + if (!partial_consume_bytes(data + 19, (uint32_t)initLen)) { + send_direct_write_nack(0x76, ERR_PARTIAL_STREAM, true); + return; + } + } + } + + uint8_t ackResponse[] = {0x00, 0x76}; + sendResponse(ackResponse, sizeof(ackResponse)); +} + void handleDirectWriteData(uint8_t* data, uint16_t len) { if (!directWriteActive || len == 0) return; + if (directWriteDataKind == DATA_KIND_PARTIAL) { + bool isCompressed = (partialCtx.flags & PARTIAL_FLAG_COMPRESSED) != 0; + if (isCompressed) { + // Match compressed full-image uploads: collect the zlib stream + // across 0x76/0x71, then inflate at 0x72 through the partial sink. + if (!directWriteCompressedBuffer) { + displayed_etag = 0; + cleanupDirectWriteState(false); + uint8_t errResponse[] = {0xFF, 0xFF}; + sendResponse(errResponse, sizeof(errResponse)); + return; + } + uint32_t cap = max_compressed_image_rx_bytes(globalConfig.displays[0].transmission_modes); + uint32_t newTotal = directWriteCompressedReceived + len; + if (cap == 0 || newTotal > cap) { + send_direct_write_nack(0x71, ERR_PARTIAL_STREAM, true); + return; + } + memcpy(directWriteCompressedBuffer + directWriteCompressedReceived, data, len); + directWriteCompressedReceived += len; + } else { + // Raw partial 0x71 data is already panel-plane data, so stream it + // immediately and let partial_consume_bytes() retarget planes. + if (!partial_consume_bytes(data, (uint32_t)len)) { + send_direct_write_nack(0x71, ERR_PARTIAL_STREAM, true); + return; + } + } + uint8_t ackResponse[] = {0x00, 0x71}; + sendResponse(ackResponse, sizeof(ackResponse)); + return; + } + directWriteDataKind = DATA_KIND_FULL; if (directWriteCompressed) { handleDirectWriteCompressedData(data, len); return; } + // Raw full-image 0x71 data streams directly to the panel; only compressed + // mode needs the intermediate compressed buffer. uint32_t remainingBytes = (directWriteBytesWritten < directWriteTotalBytes) ? (directWriteTotalBytes - directWriteBytesWritten) : 0; uint16_t bytesToWrite = (len > remainingBytes) ? remainingBytes : len; if (bytesToWrite > 0) { @@ -1270,10 +1493,88 @@ void handleDirectWriteData(uint8_t* data, uint16_t len) { void handleDirectWriteEnd(uint8_t* data, uint16_t len) { if (!directWriteActive) return; directWriteStartTime = 0; + + if (directWriteDataKind == DATA_KIND_PARTIAL) { + bool isCompressed = (partialCtx.flags & PARTIAL_FLAG_COMPRESSED) != 0; + bool storeEtag = (partialCtx.flags & PARTIAL_FLAG_STORE_ETAG) != 0; + + if (isCompressed && !decompress_partial_stream()) { + send_direct_write_nack(0x72, ERR_PARTIAL_STREAM, true); + return; + } + + uint32_t planeBytes = calc_controller_plane_bytes(partialCtx.width, partialCtx.height); + + if (partialCtx.logical_bytes_written != partialCtx.expected_stream_size || + partialCtx.old_plane_bytes_written != planeBytes || + partialCtx.new_plane_bytes_written != planeBytes) { + send_direct_write_nack(0x72, ERR_PARTIAL_STREAM, true); + return; + } + + if (storeEtag && (data == nullptr || len < 5)) { + send_direct_write_nack(0x72, ERR_PARTIAL_STREAM, true); + return; + } + + bool hasNewEtag = storeEtag; + uint32_t newEtag = 0; + if (hasNewEtag) { + newEtag = ((uint32_t)data[1] << 24) | ((uint32_t)data[2] << 16) | + ((uint32_t)data[3] << 8) | (uint32_t)data[4]; + } + + // Default PARTIAL; allow FAST override; FULL → PARTIAL for partial streams + int refreshMode = REFRESH_PARTIAL; + if (data != nullptr && len >= 1 && data[0] == 1) refreshMode = REFRESH_FAST; + + writeSerial(String("EPD partial refresh: ") + + (refreshMode == REFRESH_FAST ? "FAST" : "PARTIAL") + + " (mode=" + String(refreshMode) + + ", rect=" + String(partialCtx.x) + "," + String(partialCtx.y) + + " " + String(partialCtx.width) + "x" + String(partialCtx.height) + ")", true); + uint8_t ackResponse[] = {0x00, 0x72}; + sendResponse(ackResponse, sizeof(ackResponse)); + delay(20); + + bbepRefresh(&bbep, refreshMode); + bool refreshSuccess = waitforrefresh(60); + bbepSleep(&bbep, 1); + delay(50); + cleanupDirectWriteState(false); + + if (refreshSuccess) { + displayed_etag = hasNewEtag ? newEtag : 0; + uint8_t refreshResponse[] = {0x00, 0x73}; + sendResponse(refreshResponse, sizeof(refreshResponse)); + } else { + displayed_etag = 0; + uint8_t timeoutResponse[] = {0x00, 0x74}; + sendResponse(timeoutResponse, sizeof(timeoutResponse)); + } + return; + } + if (directWriteCompressed && directWriteCompressedReceived > 0) decompressDirectWriteData(); + + // Optional new_etag tail: refresh_mode(1) + new_etag(4 BE) when len >= 5. + bool hasNewEtag = (data != nullptr && len >= 5); + uint32_t newEtag = 0; + if (hasNewEtag) { + newEtag = ((uint32_t)data[1] << 24) | ((uint32_t)data[2] << 16) | + ((uint32_t)data[3] << 8) | (uint32_t)data[4]; + } + int refreshMode = REFRESH_FULL; - if (data != nullptr && len >= 1 && data[0] == 1) refreshMode = REFRESH_FAST; - writeSerial(String("EPD refresh: ") + (refreshMode == REFRESH_FAST ? "FAST" : "FULL") + " (mode=" + String(refreshMode) + + if (data != nullptr && len >= 1) { + if (data[0] == 1) refreshMode = REFRESH_FAST; + else if (data[0] == 2) refreshMode = REFRESH_PARTIAL; + else refreshMode = REFRESH_FULL; + } + const char* refreshLabel = "FULL"; + if (refreshMode == REFRESH_FAST) refreshLabel = "FAST"; + else if (refreshMode == REFRESH_PARTIAL) refreshLabel = "PARTIAL"; + writeSerial(String("EPD refresh: ") + refreshLabel + " (mode=" + String(refreshMode) + ", end payload " + (data != nullptr && len > 0 ? ("0x" + String(data[0], HEX)) : String("none (auto)")) + ")", true); uint8_t ackResponse[] = {0x00, 0x72}; @@ -1295,10 +1596,133 @@ void handleDirectWriteEnd(uint8_t* data, uint16_t len) { delay(50); cleanupDirectWriteState(false); if (refreshSuccess) { + displayed_etag = hasNewEtag ? newEtag : 0; uint8_t refreshResponse[] = {0x00, 0x73}; sendResponse(refreshResponse, sizeof(refreshResponse)); } else { + displayed_etag = 0; uint8_t timeoutResponse[] = {0x00, 0x74}; sendResponse(timeoutResponse, sizeof(timeoutResponse)); } } + +static void clear_partial_planes_to_white(void) { + // bb_epaper's partial refresh path runs the panel's partial init sequence + // inside bbepRefresh(). Some panel init sequences force the RAM window + // back to the full panel, so partial refresh may touch panel memory outside + // the 0x76 dirty rectangle. Seed both planes with white first so untouched + // areas refresh as white instead of stale or uninitialized RAM. + // This is controller RAM, not logical image bpp: each controller plane is 1 bpp. + const uint32_t total_bytes = ((uint32_t)bbep.native_width * (uint32_t)bbep.native_height) / 8; + uint8_t white[64]; + memset(white, 0xFF, sizeof(white)); + + bbepSetAddrWindow(&bbep, 0, 0, bbep.native_width, bbep.native_height); + bbepStartWrite(&bbep, PLANE_1); + for (uint32_t written = 0; written < total_bytes; written += sizeof(white)) { + uint32_t chunk = total_bytes - written; + if (chunk > sizeof(white)) chunk = sizeof(white); + bbepWriteData(&bbep, white, (int)chunk); + } + + bbepSetAddrWindow(&bbep, 0, 0, bbep.native_width, bbep.native_height); + bbepStartWrite(&bbep, PLANE_0); + for (uint32_t written = 0; written < total_bytes; written += sizeof(white)) { + uint32_t chunk = total_bytes - written; + if (chunk > sizeof(white)) chunk = sizeof(white); + bbepWriteData(&bbep, white, (int)chunk); + } +} + +static void setup_phase(void) { + uint32_t plane_bytes = calc_controller_plane_bytes(partialCtx.width, partialCtx.height); + + if (partialCtx.phase > 1) { + partialCtx.bytes_remaining_in_phase = 0; + return; + } + + int plane = (partialCtx.phase == 0) ? PLANE_1 : PLANE_0; + bbepSetAddrWindow(&bbep, partialCtx.x, partialCtx.y, partialCtx.width, partialCtx.height); + bbepStartWrite(&bbep, plane); + partialCtx.bytes_remaining_in_phase = plane_bytes; +} + +static bool partial_consume_bytes(uint8_t* data, uint32_t len) { + // Partial streams are two controller-plane images concatenated: + // old image to PLANE_1, then new image to PLANE_0. This sink accepts raw + // or decompressed bytes, writes as much as fits in the current plane, then + // opens the same rectangle on the next plane and continues with no buffering. + uint32_t offset = 0; + while (offset < len) { + if (partialCtx.bytes_remaining_in_phase == 0) { + setup_phase(); + if (partialCtx.bytes_remaining_in_phase == 0) + return false; // extra bytes beyond expected stream size + } + + uint32_t can_consume = len - offset; + if (can_consume > partialCtx.bytes_remaining_in_phase) + can_consume = partialCtx.bytes_remaining_in_phase; + + bbepWriteData(&bbep, data + offset, (int)can_consume); + + if (partialCtx.phase == 0) partialCtx.old_plane_bytes_written += can_consume; + else partialCtx.new_plane_bytes_written += can_consume; + + partialCtx.bytes_remaining_in_phase -= can_consume; + partialCtx.logical_bytes_written += can_consume; + offset += can_consume; + + if (partialCtx.bytes_remaining_in_phase == 0) { + partialCtx.phase++; + } + } + return true; +} + +static bool decompress_partial_stream(void) { + // Like decompressDirectWriteData(), compressed partial uploads are inflated + // only at 0x72. The output chunks go through partial_consume_bytes() so the + // old/new plane boundary can retarget the panel write window mid-stream. + if (!directWriteCompressedBuffer || directWriteCompressedReceived == 0) return false; + struct uzlib_uncomp d; + memset(&d, 0, sizeof(d)); + d.source = directWriteCompressedBuffer; + d.source_limit = directWriteCompressedBuffer + directWriteCompressedReceived; + d.source_read_cb = NULL; + uzlib_init(); + int hdr = uzlib_zlib_parse_header(&d); + if (hdr < 0) return false; + uint16_t window = 0x100 << hdr; + if (window > (uint16_t)(32 * 1024)) window = (uint16_t)(32 * 1024); + uzlib_uncompress_init(&d, dictionaryBuffer, window); + int res; + do { + d.dest_start = decompressionChunk; + d.dest = decompressionChunk; + d.dest_limit = decompressionChunk + 4096; + res = uzlib_uncompress_chksum(&d); + size_t bytesOut = d.dest - d.dest_start; + if (bytesOut > 0) { + if (!partial_consume_bytes(decompressionChunk, (uint32_t)bytesOut)) return false; + } + if (res < 0) return false; + if (partialCtx.logical_bytes_written > partialCtx.expected_stream_size) return false; + } while (res == TINF_OK); + + return res == TINF_DONE && + partialCtx.logical_bytes_written == partialCtx.expected_stream_size && + d.source == d.source_limit; +} + +static uint32_t calc_controller_plane_bytes(uint16_t width, uint16_t height) { + return ((uint32_t)(width + 7u) / 8u) * height; +} + +static void send_direct_write_nack(uint8_t opcode, uint8_t error, bool cleanupState) { + displayed_etag = 0; + if (cleanupState) cleanupDirectWriteState(false); + uint8_t errResponse[] = {0xFF, opcode, error, 0x00}; + sendResponse(errResponse, sizeof(errResponse)); +} diff --git a/src/display_service.h b/src/display_service.h index 98fbd0d..d478fbe 100644 --- a/src/display_service.h +++ b/src/display_service.h @@ -29,6 +29,7 @@ void handleDirectWriteCompressedData(uint8_t* data, uint16_t len); void decompressDirectWriteData(); void cleanupDirectWriteState(bool refreshDisplay); void handleDirectWriteEnd(uint8_t* data, uint16_t len); +void handlePartialWriteStart(uint8_t* data, uint16_t len); int getplane(); int getBitsPerPixel(); diff --git a/src/main.h b/src/main.h index 264999f..21d19ab 100644 --- a/src/main.h +++ b/src/main.h @@ -190,6 +190,7 @@ uint16_t directWriteWidth = 0; // Display width in pixels uint16_t directWriteHeight = 0; // Display height in pixels uint32_t directWriteTotalBytes = 0; // Total bytes expected per plane (for bitplanes) or total (for others) uint8_t directWriteRefreshMode = 0; // 0 = FULL (default), 1 = FAST/PARTIAL (if supported) +uint8_t directWriteDataKind = 0; // none; display_service.cpp tracks full vs partial 0x71 streams // Direct write compressed mode: use same buffer as regular image upload uint32_t directWriteCompressedSize = 0; // Total compressed size expected @@ -261,6 +262,7 @@ void handleDirectWriteStart(uint8_t* data, uint16_t len); void handleDirectWriteData(uint8_t* data, uint16_t len); void handleDirectWriteEnd(uint8_t* data = nullptr, uint16_t len = 0); void handleDirectWriteCompressedData(uint8_t* data, uint16_t len); +void handlePartialWriteStart(uint8_t* data, uint16_t len); int mapEpd(int id); uint8_t getFirmwareMajor(); uint8_t getFirmwareMinor(); @@ -301,6 +303,14 @@ struct SecurityConfig securityConfig = {0}; EncryptionSession encryptionSession = {0}; bool encryptionInitialized = false; +#ifdef TARGET_ESP32 +// 0x00000000 = "not set". Persists across deep sleep on ESP32. +RTC_DATA_ATTR uint32_t displayed_etag = 0; +#else +// 0x00000000 = "not set". Non-ESP32 targets reset this on boot. +uint32_t displayed_etag = 0; +#endif + #ifdef TARGET_ESP32 // RTC memory variables for deep sleep state tracking RTC_DATA_ATTR bool woke_from_deep_sleep = false; @@ -384,4 +394,4 @@ MyBLEServerCallbacks staticServerCallbacks; // Static callback object (no dynam MyBLECharacteristicCallbacks staticCharCallbacks; // Static callback object (no dynamic allocation) #endif -extern const uint8_t writelineFont[] PROGMEM; \ No newline at end of file +extern const uint8_t writelineFont[] PROGMEM;