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
This commit is contained in:
TheHexaMaster
2026-02-16 10:19:58 +01:00
committed by GitHub
parent 809e8c4ef9
commit 48df14ffa1
3 changed files with 713 additions and 1 deletions

View File

@@ -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.

View File

@@ -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)

View File

@@ -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 <hexamaster@icloud.com>
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 <http://www.gnu.org/licenses/>.
*/
#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 <block>\"}"));
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 <block>\"}"));
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