From 48df14ffa13c5d40f4f293139079118f8c99e04a Mon Sep 17 00:00:00 2001 From: TheHexaMaster <129121144+TheHexaMaster@users.noreply.github.com> Date: Mon, 16 Feb 2026 10:19:58 +0100 Subject: [PATCH] FM24Cxx: FRAM (I2C) console driver with block read/write/format (#24457) * FM24Cxx: FRAM (I2C) console driver with block read/write/format * Fix formatting in license comment section --- I2CDEVICES.md | 3 +- tasmota/my_user_config.h | 9 + .../tasmota_xdrv_driver/xdrv_93_fm24cxx.ino | 702 ++++++++++++++++++ 3 files changed, 713 insertions(+), 1 deletion(-) create mode 100644 tasmota/tasmota_xdrv_driver/xdrv_93_fm24cxx.ino diff --git a/I2CDEVICES.md b/I2CDEVICES.md index 6598e3932..4fdc51467 100644 --- a/I2CDEVICES.md +++ b/I2CDEVICES.md @@ -138,5 +138,6 @@ Index | Define | Driver | Device | Address(es) | Bus2 | Descrip 95 | USE_AGS02MA | xsns_118 | AGS02MA | 0x1A | | TVOC Gas sensor 96 | USE_RX8025 | xdrv_56 | RX8025 | 0x32 | Yes | RX8025 RTC 97 | USE_SEN6X | xsns_119 | SEN6X | 0x6B | Yes | Gas (CO2/VOC/NOx index) and air quality (PPM <1,<2.5,<4,<10) - + 98 | USE_FM24CXX | xdrv_93 | FM24CXX | 0x50 - 0x57| Yes | FM24CXX - External FRAM with console / berry R/W operations + NOTE: Bus2 supported on ESP32 only. diff --git a/tasmota/my_user_config.h b/tasmota/my_user_config.h index a7b4e8f75..16eeadc35 100644 --- a/tasmota/my_user_config.h +++ b/tasmota/my_user_config.h @@ -842,6 +842,15 @@ // #define USE_GRAPH // Enable line charts with displays // #define NUM_GRAPHS 4 // Max 16 +// #define USE_FM24CXX // External FRAM module over I2C accesible via tasmota console / berry. be used to store super volatile data, frequently-changing settings, logs etc... (ps: shall work with AT24C32-AT24C512, but use with care - wear) + #define FM24CXX_I2C_ADD 0x57 // Fram I2C address. + #define FM24CXX_CAPACITY 8192 // FRAM Module size in bytes. 8192 = 64kbits (FM24C64), 4096 = 32kbits (FM24C32) etc.. + #define FM24CXX_BLOCK_SIZE 256 // Parsed block size. When 256 and FRAM Size 8192, there are 8192/256 = 32 blocks per 256 bytes. + #define FM24CXX_I2C_CHUNK 32 // I2C Read Chunk size. For maximum compatibility, use 32. ESP32 S3,P4 works with 128 (a bit faster writes) + #define FM24CXX_MAX_WRITE_BYTES 4096 // Maximum bytes to be written at single cmd + #define FM24CXX_JSON_MAX_BYTES 4096 // Maximum bytes to get in single json response in FramReadRaw cmd. Above raise error. + + #endif // USE_I2C //#define USE_DISPLAY // Add I2C/TM1637/MAX7219 Display Support (+2k code) diff --git a/tasmota/tasmota_xdrv_driver/xdrv_93_fm24cxx.ino b/tasmota/tasmota_xdrv_driver/xdrv_93_fm24cxx.ino new file mode 100644 index 000000000..05f51a355 --- /dev/null +++ b/tasmota/tasmota_xdrv_driver/xdrv_93_fm24cxx.ino @@ -0,0 +1,702 @@ +/* + xdrv_93_fm24cxx.ino - External FRAM over I2C with console / berry R/W operations + + Copyright (C) 2026 by Martin Macák - HexaMaster + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#ifdef USE_I2C +#ifdef USE_FM24CXX + +#define XDRV_93 93 +#define XI2C_98 98 + +#ifndef FM24CXX_I2C_ADD +#define FM24CXX_I2C_ADD 0x57 +#endif + +#ifndef FM24CXX_BLOCK_SIZE +#define FM24CXX_BLOCK_SIZE 0 +#endif + +#ifndef FM24CXX_CAPACITY +#define FM24CXX_CAPACITY 8192 +#endif + +#ifndef FM24CXX_I2C_CHUNK +#define FM24CXX_I2C_CHUNK 32 +#endif + +#ifndef FM24CXX_MAX_WRITE_BYTES +#define FM24CXX_MAX_WRITE_BYTES 4096 +#endif + +#ifndef FM24CXX_JSON_MAX_BYTES +#define FM24CXX_JSON_MAX_BYTES 4096 +#endif + +#ifndef FM24CXX_JSON_MAX_STRING +#define FM24CXX_JSON_MAX_STRING 4096 +#endif + +// TX payload must also include 2 address bytes in the same Wire transmission. +// Typical Wire TX buffer is 32 bytes -> 30 bytes payload + 2 bytes addr. +#define FM24CXX_TX_PAYLOAD_MAX (FM24CXX_I2C_CHUNK > 2 ? (FM24CXX_I2C_CHUNK - 2) : 1) + +struct { + bool detected = false; + uint8_t address = (uint8_t)FM24CXX_I2C_ADD; + uint8_t bus = 0; + + uint32_t total_bytes = (uint32_t)FM24CXX_CAPACITY; + + uint32_t block_size = (uint32_t)FM24CXX_BLOCK_SIZE; // 0 => whole chip + uint16_t block_count = 1; + + uint32_t last_block_size = 0; // if total_bytes not divisible +} fm24; + +// ---------- Helpers ---------- + +static char* Fm24_JsonEscapeAlloc(const char *in, uint32_t len) { + // worst-case: every char -> \u00XX (6 chars) + uint32_t need = 0; + for (uint32_t i = 0; i < len; i++) { + uint8_t c = (uint8_t)in[i]; + if (c == '\"' || c == '\\' || c == '\b' || c == '\f' || c == '\n' || c == '\r' || c == '\t') need += 2; + else if (c < 0x20) need += 6; // control char -> \u00XX + else need += 1; + } + + char *out = (char*)malloc(need + 1); + if (!out) return nullptr; + + static const char kHex[] = "0123456789ABCDEF"; + char *d = out; + + for (uint32_t i = 0; i < len; i++) { + uint8_t c = (uint8_t)in[i]; + switch (c) { + case '\"': *d++='\\'; *d++='\"'; break; + case '\\': *d++='\\'; *d++='\\'; break; + case '\b': *d++='\\'; *d++='b'; break; + case '\f': *d++='\\'; *d++='f'; break; + case '\n': *d++='\\'; *d++='n'; break; + case '\r': *d++='\\'; *d++='r'; break; + case '\t': *d++='\\'; *d++='t'; break; + default: + if (c < 0x20) { + *d++='\\'; *d++='u'; *d++='0'; *d++='0'; + *d++=kHex[(c >> 4) & 0x0F]; + *d++=kHex[c & 0x0F]; + } else { + *d++ = (char)c; + } + break; + } + } + + *d = 0; + return out; +} + +static void Fm24_RecalcBlocks(void) { + uint32_t bs = fm24.block_size; + + if (bs == 0 || bs >= fm24.total_bytes) { + fm24.block_size = fm24.total_bytes; + fm24.block_count = 1; + fm24.last_block_size = fm24.total_bytes; + return; + } + + uint32_t full = fm24.total_bytes / bs; + uint32_t rem = fm24.total_bytes % bs; + + fm24.block_count = (uint16_t)full; + if (rem) fm24.block_count++; + + fm24.last_block_size = rem ? rem : bs; +} + +static bool Fm24_GetBlockParams(uint32_t block, uint32_t *start, uint32_t *len) { + if (!fm24.detected) return false; + if (block >= fm24.block_count) return false; + + uint32_t s = block * fm24.block_size; + uint32_t l = fm24.block_size; + + if ((block == (uint32_t)(fm24.block_count - 1)) && (fm24.last_block_size != fm24.block_size)) { + l = fm24.last_block_size; + } + + if (s + l > fm24.total_bytes) return false; + *start = s; + *len = l; + return true; +} + +// Robust hex parser: accepts "AABBCC", "AA BB CC", "0xAA,0xBB,0xCC", etc. +static uint32_t Fm24_ParseHexBytes(const char *s, uint8_t *out, uint32_t out_max) { + uint32_t n = 0; + int hi = -1; + + while (*s && n < out_max) { + + // Skip 0x / 0X prefix as a whole token (do NOT consume '0' as a nibble) + if (s[0] == '0' && (s[1] == 'x' || s[1] == 'X')) { + s += 2; + continue; + } + + char c = *s++; + int v = -1; + + if (c >= '0' && c <= '9') v = c - '0'; + else if (c >= 'a' && c <= 'f') v = 10 + (c - 'a'); + else if (c >= 'A' && c <= 'F') v = 10 + (c - 'A'); + else continue; // separators + + if (hi < 0) hi = v; + else { + out[n++] = (uint8_t)((hi << 4) | v); + hi = -1; + } + } + + return n; +} + +static void Fm24_LogHexLine(uint32_t addr, const uint8_t *buf, uint32_t len) { + AddLog(LOG_LEVEL_INFO, PSTR("FM24CXX: 0x%04X: %*_H"), (uint16_t)addr, (int)len, buf); +} + +// ---------- Low-level I2C FRAM access (16-bit address, MSB first) ---------- + +static bool Fm24_I2cRead(uint16_t mem_addr, uint8_t *buf, uint32_t len) { + + TwoWire& myWire = I2cGetWire(fm24.bus); + if (&myWire == nullptr) { return false; } + + uint32_t done = 0; + while (done < len) { + uint32_t chunk = len - done; + if (chunk > FM24CXX_I2C_CHUNK) chunk = FM24CXX_I2C_CHUNK; + + // Set internal address pointer + myWire.beginTransmission(fm24.address); + myWire.write((uint8_t)(mem_addr >> 8)); // MSB + myWire.write((uint8_t)(mem_addr & 0xFF)); // LSB + if (myWire.endTransmission(true) != 0) { // STOP + return false; + } + + // Read data + if (chunk != (uint32_t)myWire.requestFrom((uint8_t)fm24.address, (uint8_t)chunk)) { + // Drain + while (myWire.available()) (void)myWire.read(); + return false; + } + + for (uint32_t i = 0; i < chunk; i++) { + if (!myWire.available()) return false; + buf[done + i] = (uint8_t)myWire.read(); + } + + done += chunk; + mem_addr = (uint16_t)(mem_addr + chunk); + yield(); + } + + return true; +} + +static bool Fm24_I2cWrite(uint16_t mem_addr, const uint8_t *buf, uint32_t len) { + + TwoWire& myWire = I2cGetWire(fm24.bus); + if (&myWire == nullptr) { return false; } + + uint32_t done = 0; + while (done < len) { + uint32_t chunk = len - done; + if (chunk > FM24CXX_TX_PAYLOAD_MAX) chunk = FM24CXX_TX_PAYLOAD_MAX; + + myWire.beginTransmission(fm24.address); + myWire.write((uint8_t)(mem_addr >> 8)); // MSB + myWire.write((uint8_t)(mem_addr & 0xFF)); // LSB + for (uint32_t i = 0; i < chunk; i++) { + myWire.write(buf[done + i]); + } + + if (myWire.endTransmission(true) != 0) { + return false; + } + + done += chunk; + mem_addr = (uint16_t)(mem_addr + chunk); + yield(); + } + + return true; +} + +static bool Fm24_WriteFill(uint16_t mem_addr, uint8_t fill, uint32_t len) { + uint8_t tmp[FM24CXX_TX_PAYLOAD_MAX]; + memset(tmp, fill, sizeof(tmp)); + + uint32_t done = 0; + while (done < len) { + uint32_t chunk = len - done; + if (chunk > sizeof(tmp)) chunk = sizeof(tmp); + + if (!Fm24_I2cWrite((uint16_t)(mem_addr + done), tmp, chunk)) { + return false; + } + done += chunk; + yield(); + } + return true; +} + +// ---------- Detect ---------- + +static void Fm24_Detect(void) { + if (fm24.detected) return; + if (!I2cEnabled(XI2C_98)) return; + + fm24.address = (uint8_t)FM24CXX_I2C_ADD; + fm24.total_bytes = (uint32_t)FM24CXX_CAPACITY; + fm24.block_size = (uint32_t)FM24CXX_BLOCK_SIZE; + Fm24_RecalcBlocks(); + + for (fm24.bus = 0; fm24.bus < 2; fm24.bus++) { + if (!I2cSetDevice(fm24.address, fm24.bus)) continue; + + uint8_t b = 0; + fm24.detected = Fm24_I2cRead(0x0000, &b, 1); + + if (fm24.detected) { + I2cSetActiveFound(fm24.address, "FM24CXX", fm24.bus); + AddLog(LOG_LEVEL_INFO, PSTR("FM24CXX: detected bus=%d addr=0x%02X size=%u block=%u blocks=%u"), + fm24.bus, fm24.address, + (unsigned)fm24.total_bytes, + (unsigned)fm24.block_size, + (unsigned)fm24.block_count); + return; + } + } + + AddLog(LOG_LEVEL_DEBUG, PSTR("FM24CXX: not detected (addr=0x%02X)"), (uint8_t)FM24CXX_I2C_ADD); +} + +// ---------- Tasmota console command handling ---------- + +#define D_PRFX_FRAM "Fram" +#define D_CMND_FRAM_INFO "Info" // General info (etc... {"FramInfo":{"bus":1,"addr":"0x50","size":65535,"block":1024,"blocks":64}}) +#define D_CMND_FRAM_READ_FORMAT "ReadFormat" // Read HEX values from block and format them in tasmota console (STREAM) +#define D_CMND_FRAM_READ "Read" // Read HEX values from block to result map (easy access from Berry) +#define D_CMND_FRAM_WRITE "Write" // Write HEX values to block. Unused block bytes are filled with zeros +#define D_CMND_FRAM_FORMAT "Erase" // Erase selected block or whole fram (if "all") - fill with zeros +#define D_CMND_FRAM_READ_STRING "ReadString" // Read block data as STRING - json output +#define D_CMND_FRAM_WRITE_STRING "WriteString" // Write data to block as STRING. + +static void CmndFramInfo(void); +static void CmndFramReadFormat(void); +static void CmndFramRead(void); +static void CmndFramWrite(void); +static void CmndFramErase(void); +static void CmndFramReadString(void); +static void CmndFramWriteString(void); + +const char kFramCommands[] PROGMEM = + D_PRFX_FRAM "|" D_CMND_FRAM_INFO "|" D_CMND_FRAM_READ_FORMAT "|" D_CMND_FRAM_READ "|" D_CMND_FRAM_WRITE "|" D_CMND_FRAM_FORMAT "|" D_CMND_FRAM_READ_STRING "|" D_CMND_FRAM_WRITE_STRING; + +void (*const FramCommand[])(void) PROGMEM = { + &CmndFramInfo, &CmndFramReadFormat, &CmndFramRead, &CmndFramWrite, &CmndFramErase, &CmndFramReadString, &CmndFramWriteString +}; + +static void CmndFramInfo(void) { + Fm24_Detect(); + if (!fm24.detected) { + Response_P(PSTR("{\"FramInfo\":\"not detected\"}")); + return; + } + + Response_P(PSTR("{\"FramInfo\":{\"bus\":%d,\"addr\":\"0x%02X\",\"size\":%u,\"block\":%u,\"blocks\":%u}}"), + fm24.bus, fm24.address, + (unsigned)fm24.total_bytes, + (unsigned)fm24.block_size, + (unsigned)fm24.block_count); +} + +static void CmndFramReadFormat(void) { + Fm24_Detect(); + if (!fm24.detected) { + Response_P(PSTR("{\"FramReadFormat\":\"not detected\"}")); + return; + } + + if (!XdrvMailbox.data_len) { ResponseCmndFailed(); return; } + + uint32_t block = (uint32_t)strtoul(XdrvMailbox.data, nullptr, 0); + uint32_t start = 0, len = 0; + if (!Fm24_GetBlockParams(block, &start, &len)) { ResponseCmndFailed(); return; } + + AddLog(LOG_LEVEL_INFO, PSTR("FM24CXX: READ block=%u start=0x%04X len=%u"), + (unsigned)block, (uint16_t)start, (unsigned)len); + + uint8_t tmp[16]; + for (uint32_t off = 0; off < len; off += sizeof(tmp)) { + uint32_t chunk = len - off; + if (chunk > sizeof(tmp)) chunk = sizeof(tmp); + + if (!Fm24_I2cRead((uint16_t)(start + off), tmp, chunk)) { + AddLog(LOG_LEVEL_ERROR, PSTR("FM24CXX: READ failed at 0x%04X"), (uint16_t)(start + off)); + ResponseCmndFailed(); + return; + } + Fm24_LogHexLine(start + off, tmp, chunk); + yield(); + } + + Response_P(PSTR("{\"FramReadFormat\":{\"block\":%u,\"bytes\":%u}}"), (unsigned)block, (unsigned)len); +} + +static void CmndFramRead(void) { + if (!fm24.detected) { + Response_P(PSTR("{\"FramRead\":\"not detected\"}")); + return; + } + + char *p = XdrvMailbox.data; + while (p && (*p == ' ' || *p == '\t' || *p == ',')) p++; + + if (!p || *p == 0) { + Response_P(PSTR("{\"FramRead\":\"usage: FramRead \"}")); + return; + } + + uint32_t block = (uint32_t)strtoul(p, nullptr, 0); + + uint32_t start = 0, len = 0; + if (!Fm24_GetBlockParams(block, &start, &len)) { + ResponseCmndFailed(); + return; + } + + // buffer protection + if (len > FM24CXX_JSON_MAX_BYTES) { + Response_P(PSTR("{\"FramRead\":{\"error\":\"too big\",\"block\":%u,\"bytes\":%u,\"max\":%u}}"), + (unsigned)block, (unsigned)len, (unsigned)FM24CXX_JSON_MAX_BYTES); + return; + } + + uint8_t *buf = (uint8_t*)malloc(len); + if (!buf) { ResponseCmndFailed(); return; } + + if (!Fm24_I2cRead((uint16_t)start, buf, len)) { + free(buf); + ResponseCmndFailed(); + return; + } + + char *hex = (char*)malloc((len * 2) + 1); + if (!hex) { free(buf); ResponseCmndFailed(); return; } + hex[len * 2] = 0; + + static const char kHex[] = "0123456789ABCDEF"; + for (uint32_t i = 0; i < len; i++) { + uint8_t v = buf[i]; + hex[i * 2 + 0] = kHex[v >> 4]; + hex[i * 2 + 1] = kHex[v & 0x0F]; + } + + Response_P(PSTR("{\"FramRead\":{\"block\":%u,\"bytes\":%u,\"data\":\"%s\"}}"), + (unsigned)block, (unsigned)len, hex); + + free(hex); + free(buf); +} + +static void CmndFramWrite(void) { + Fm24_Detect(); + if (!fm24.detected) { + Response_P(PSTR("{\"FramWrite\":\"not detected\"}")); + return; + } + + if (!XdrvMailbox.data_len) { ResponseCmndFailed(); return; } + + char *p = XdrvMailbox.data; + while (p && (*p == ' ' || *p == '\t' || *p == ',')) p++; + + char *save = nullptr; + char *a1 = strtok_r(p, " \t,", &save); + if (!a1) { ResponseCmndFailed(); return; } + + uint32_t block = (uint32_t)strtoul(a1, nullptr, 0); + uint32_t start = 0, len = 0; + if (!Fm24_GetBlockParams(block, &start, &len)) { ResponseCmndFailed(); return; } + + char *hex = save; + while (hex && (*hex == ' ' || *hex == '\t' || *hex == ',')) hex++; + + if (!hex || *hex == 0) { + Response_P(PSTR("{\"FramWrite\":\"write needs hex payload\"}")); + return; + } + + uint32_t max_write = len; + if (max_write > FM24CXX_MAX_WRITE_BYTES) max_write = FM24CXX_MAX_WRITE_BYTES; + + uint8_t *data = (uint8_t*)malloc(max_write); + if (!data) { ResponseCmndFailed(); return; } + + uint32_t n = Fm24_ParseHexBytes(hex, data, max_write); + if (n == 0) { + free(data); + Response_P(PSTR("{\"FramWrite\":\"no hex bytes parsed\"}")); + return; + } + + AddLog(LOG_LEVEL_INFO, PSTR("FM24CXX: WRITE block=%u start=0x%04X len=%u parsed=%u (fill rest with 0x00)"), + (unsigned)block, (uint16_t)start, (unsigned)len, (unsigned)n); + + if (!Fm24_I2cWrite((uint16_t)start, data, n)) { + free(data); + AddLog(LOG_LEVEL_ERROR, PSTR("FM24CXX: WRITE failed at 0x%04X"), (uint16_t)start); + ResponseCmndFailed(); + return; + } + free(data); + + if (n < len) { + if (!Fm24_WriteFill((uint16_t)(start + n), 0x00, (len - n))) { + AddLog(LOG_LEVEL_ERROR, PSTR("FM24CXX: FILL failed at 0x%04X"), (uint16_t)(start + n)); + ResponseCmndFailed(); + return; + } + } + + Response_P(PSTR("{\"FramWrite\":{\"block\":%u,\"bytes\":%u,\"filled\":%u}}"), + (unsigned)block, (unsigned)n, (unsigned)(len - (n < len ? n : len))); +} + +static void CmndFramErase(void) { + Fm24_Detect(); + if (!fm24.detected) { + Response_P(PSTR("{\"FramErase\":\"not detected\"}")); + return; + } + + if (!XdrvMailbox.data_len) { ResponseCmndFailed(); return; } + + char *p = XdrvMailbox.data; + while (p && (*p == ' ' || *p == '\t' || *p == ',')) p++; + + if (!strcasecmp(p, "all")) { + AddLog(LOG_LEVEL_INFO, PSTR("FM24CXX: FORMAT all blocks (%u), fill=0x00"), (unsigned)fm24.block_count); + + for (uint32_t b = 0; b < fm24.block_count; b++) { + uint32_t start = 0, len = 0; + if (!Fm24_GetBlockParams(b, &start, &len)) { ResponseCmndFailed(); return; } + + if (!Fm24_WriteFill((uint16_t)start, 0x00, len)) { + AddLog(LOG_LEVEL_ERROR, PSTR("FM24CXX: FORMAT failed at block=%u addr=0x%04X"), (unsigned)b, (uint16_t)start); + ResponseCmndFailed(); + return; + } + yield(); + } + + Response_P(PSTR("{\"FramErase\":\"all\"}")); + return; + } + + uint32_t block = (uint32_t)strtoul(p, nullptr, 0); + uint32_t start = 0, len = 0; + if (!Fm24_GetBlockParams(block, &start, &len)) { ResponseCmndFailed(); return; } + + AddLog(LOG_LEVEL_INFO, PSTR("FM24CXX: FORMAT block=%u start=0x%04X len=%u fill=0x00"), + (unsigned)block, (uint16_t)start, (unsigned)len); + + if (!Fm24_WriteFill((uint16_t)start, 0x00, len)) { + AddLog(LOG_LEVEL_ERROR, PSTR("FM24CXX: FORMAT failed at 0x%04X"), (uint16_t)start); + ResponseCmndFailed(); + return; + } + + Response_P(PSTR("{\"FramErase\":%u}"), (unsigned)block); +} + +static void CmndFramReadString(void) { + Fm24_Detect(); + if (!fm24.detected) { + Response_P(PSTR("{\"FramReadString\":\"not detected\"}")); + return; + } + + char *p = XdrvMailbox.data; + while (p && (*p == ' ' || *p == '\t' || *p == ',')) p++; + + if (!p || *p == 0) { + Response_P(PSTR("{\"FramReadString\":\"usage: FramReadString \"}")); + return; + } + + uint32_t block = (uint32_t)strtoul(p, nullptr, 0); + + uint32_t start = 0, len = 0; + if (!Fm24_GetBlockParams(block, &start, &len)) { ResponseCmndFailed(); return; } + + // chunked read + uint32_t max_raw = len; + if (max_raw > FM24CXX_JSON_MAX_STRING) max_raw = FM24CXX_JSON_MAX_STRING; + + char *raw = (char*)malloc(max_raw + 1); + if (!raw) { ResponseCmndFailed(); return; } + + uint32_t raw_len = 0; + bool zero_found = false; + + uint8_t tmp[FM24CXX_I2C_CHUNK]; + + for (uint32_t off = 0; off < len && raw_len < max_raw && !zero_found; ) { + uint32_t chunk = len - off; + if (chunk > sizeof(tmp)) chunk = sizeof(tmp); + + if (!Fm24_I2cRead((uint16_t)(start + off), tmp, chunk)) { + free(raw); + ResponseCmndFailed(); + return; + } + + for (uint32_t i = 0; i < chunk && raw_len < max_raw; i++) { + uint8_t c = tmp[i]; + if (c == 0x00) { zero_found = true; break; } + raw[raw_len++] = (char)c; + } + + off += chunk; + yield(); + } + + raw[raw_len] = 0; + + bool truncated = (!zero_found) && (raw_len == max_raw) && (max_raw < len); + + char *esc = Fm24_JsonEscapeAlloc(raw, raw_len); + if (!esc) { + free(raw); + ResponseCmndFailed(); + return; + } + + Response_P(PSTR("{\"FramReadString\":{\"block\":%u,\"bytes\":%u,\"truncated\":%u,\"data\":\"%s\"}}"), + (unsigned)block, (unsigned)raw_len, (unsigned)(truncated ? 1 : 0), esc); + + free(esc); + free(raw); +} + +static void CmndFramWriteString(void) { + Fm24_Detect(); + if (!fm24.detected) { + Response_P(PSTR("{\"FramWriteString\":\"not detected\"}")); + return; + } + + if (!XdrvMailbox.data_len) { ResponseCmndFailed(); return; } + + char *p = XdrvMailbox.data; + while (p && (*p == ' ' || *p == '\t' || *p == ',')) p++; + + char *save = nullptr; + char *a1 = strtok_r(p, " \t,", &save); + if (!a1) { ResponseCmndFailed(); return; } + + uint32_t block = (uint32_t)strtoul(a1, nullptr, 0); + + uint32_t start = 0, len = 0; + if (!Fm24_GetBlockParams(block, &start, &len)) { ResponseCmndFailed(); return; } + + char *str = save; + while (str && (*str == ' ' || *str == '\t' || *str == ',')) str++; + + if (!str || *str == 0) { + Response_P(PSTR("{\"FramWriteString\":\"write needs string payload\"}")); + return; + } + + // quote string support ("") + if (str[0] == '\"') { + str++; + char *endq = strrchr(str, '\"'); + if (endq) *endq = 0; + } + + uint32_t max_write = len; + if (max_write > FM24CXX_MAX_WRITE_BYTES) max_write = FM24CXX_MAX_WRITE_BYTES; + + uint32_t n = (uint32_t)strnlen(str, max_write); + bool truncated = (str[n] != 0); // strnlen hitol limit + + AddLog(LOG_LEVEL_INFO, PSTR("FM24CXX: WRITE_STRING block=%u start=0x%04X str_len=%u (fill rest with 0x00)"), + (unsigned)block, (uint16_t)start, (unsigned)n); + + if (n > 0) { + if (!Fm24_I2cWrite((uint16_t)start, (const uint8_t*)str, n)) { + AddLog(LOG_LEVEL_ERROR, PSTR("FM24CXX: WRITE_STRING failed at 0x%04X"), (uint16_t)start); + ResponseCmndFailed(); + return; + } + } + + if (n < len) { + if (!Fm24_WriteFill((uint16_t)(start + n), 0x00, (len - n))) { + AddLog(LOG_LEVEL_ERROR, PSTR("FM24CXX: WRITE_STRING fill failed at 0x%04X"), (uint16_t)(start + n)); + ResponseCmndFailed(); + return; + } + } + + Response_P(PSTR("{\"FramWriteString\":{\"block\":%u,\"bytes\":%u,\"filled\":%u,\"truncated\":%u}}"), + (unsigned)block, (unsigned)n, (unsigned)(len - (n < len ? n : len)), (unsigned)(truncated ? 1 : 0)); +} + +// ---------- Interface ---------- + +bool Xdrv93(uint32_t function) { + bool result = false; + + switch (function) { + case FUNC_INIT: + Fm24_Detect(); + break; + + case FUNC_COMMAND: + result = DecodeCommand(kFramCommands, FramCommand); + break; + + case FUNC_ACTIVE: + result = fm24.detected; + break; + } + + return result; +} + +#endif // USE_FM24CXX +#endif // USE_I2C