diff --git a/wled00/FX_2Dfcn.cpp b/wled00/FX_2Dfcn.cpp index a0cc3c4461..a96b637d79 100644 --- a/wled00/FX_2Dfcn.cpp +++ b/wled00/FX_2Dfcn.cpp @@ -71,29 +71,33 @@ void WS2812FX::setUpMatrix() { // allowed values are: -1 (missing pixel/no LED attached), 0 (inactive/unused pixel), 1 (active/used pixel) char fileName[32]; strcpy_P(fileName, PSTR("/2d-gaps.json")); bool isFile = WLED_FS.exists(fileName); - size_t gapSize = 0; int8_t *gapTable = nullptr; - if (isFile && requestJSONBufferLock(JSON_LOCK_LEDGAP)) { + if (isFile) { DEBUG_PRINT(F("Reading LED gap from ")); DEBUG_PRINTLN(fileName); - // read the array into global JSON buffer - if (readObjectFromFile(fileName, nullptr, pDoc)) { - // the array is similar to ledmap, except it has only 3 values: - // -1 ... missing pixel (do not increase pixel count) - // 0 ... inactive pixel (it does count, but should be mapped out (-1)) - // 1 ... active pixel (it will count and will be mapped) - JsonArray map = pDoc->as(); - gapSize = map.size(); - if (!map.isNull() && gapSize >= matrixSize) { // not an empty map - gapTable = static_cast(p_malloc(gapSize)); - if (gapTable) for (size_t i = 0; i < gapSize; i++) { - gapTable[i] = constrain(map[i], -1, 1); + // read the gap array directly from the file, a file with fewer entries than matrix positions is ignored + // the array is similar to ledmap, except it has only 3 values: + // -1 ... missing pixel (do not increase pixel count) + // 0 ... inactive pixel (it does count, but should be mapped out (-1)) + // 1 ... active pixel (it will count and will be mapped) + File f = WLED_FS.open(fileName, "r"); + if (f) { + gapTable = static_cast(p_malloc(matrixSize)); + if (gapTable) { + int value; + unsigned count = 0; + while (count < matrixSize && readNextIntFromFile(f, value)) { + gapTable[count++] = (int8_t)constrain(value, -1, 1); + } + if (count < matrixSize) { // incomplete gap file, ignore it so below loop does not read OOB + p_free(gapTable); + gapTable = nullptr; } } + f.close(); } DEBUG_PRINTLN(F("Gaps loaded.")); - releaseJSONBufferLock(); } unsigned x, y, pix=0; //pixel diff --git a/wled00/FX_fcn.cpp b/wled00/FX_fcn.cpp index be7e7863d7..d82b1891fc 100644 --- a/wled00/FX_fcn.cpp +++ b/wled00/FX_fcn.cpp @@ -1913,6 +1913,7 @@ uint8_t WS2812FX::getActiveSegmentsNum() const { uint16_t WS2812FX::getLengthTotal() const { unsigned len = Segment::maxWidth * Segment::maxHeight; // will be _length for 1D (see finalizeInit()) but should cover whole matrix for 2D if (isMatrix && _length > len) len = _length; // for 2D with trailing strip + if (isMatrix && customMappingSize > len) len = customMappingSize; // sparse matrix ledmap with gaps and trailing strip (see deserializeMap()) return len; } @@ -2058,9 +2059,9 @@ void WS2812FX::fixInvalidSegments() { if (isMatrix) { #ifndef WLED_DISABLE_2D if (_segments[i].start >= Segment::maxWidth * Segment::maxHeight) { - // 1D segment at the end of matrix - if (_segments[i].start >= _length || _segments[i].startY > 0 || _segments[i].stopY > 1) { _segments.erase(_segments.begin()+i); continue; } - if (_segments[i].stop > _length) _segments[i].stop = _length; + // 1D segment at the end of matrix (trailing strip; logical length may exceed physical _length for sparse matrix ledmaps) + if (_segments[i].start >= getLengthTotal() || _segments[i].startY > 0 || _segments[i].stopY > 1) { _segments.erase(_segments.begin()+i); continue; } + if (_segments[i].stop > getLengthTotal()) _segments[i].stop = getLengthTotal(); continue; } if (_segments[i].start >= Segment::maxWidth || _segments[i].startY >= Segment::maxHeight) { _segments.erase(_segments.begin()+i); continue; } @@ -2156,58 +2157,110 @@ bool WS2812FX::deserializeMap(unsigned n) { isMatrix = true; DEBUG_PRINTF_P(PSTR("LED map width=%d, height=%d\n"), Segment::maxWidth, Segment::maxHeight); } + releaseJSONBufferLock(); d_free(customMappingTable); - customMappingTable = static_cast(d_malloc(sizeof(uint16_t)*getLengthTotal())); // prefer DRAM for speed + customMappingTable = nullptr; + + if (isMatrix) { + // 2D set-up: read the file twice: first pass counts valid pixel entries (numPhy) + // then allocate matrixSize + trailingCount and fill it on the second pass including trailing pixels + // if entries are missing, they are appended (fallback) + const unsigned matrixSize = Segment::maxWidth * Segment::maxHeight; - if (customMappingTable) { - DEBUG_PRINTF_P(PSTR("ledmap allocated: %uB\n"), sizeof(uint16_t)*getLengthTotal()); + // count entries and physical pixels used in the matrix, any left-over physical pixels are trailing pixels + unsigned entries = 0; + unsigned numPhy = 0; File f = WLED_FS.open(fileName, "r"); - f.find("\"map\":["); - while (f.available()) { // f.position() < f.size() - 1 - char number[32]; - size_t numRead = f.readBytesUntil(',', number, sizeof(number)-1); // read a single number (may include array terminating "]" but not number separator ',') - number[numRead] = 0; - if (numRead > 0) { - char *end = strchr(number,']'); // we encountered end of array so stop processing if no digit found - bool foundDigit = (end == nullptr); - int i = 0; - if (end != nullptr) do { - if (number[i] >= '0' && number[i] <= '9') foundDigit = true; - if (foundDigit || &number[i++] == end) break; - } while (i < 32); - if (!foundDigit) break; - int index = atoi(number); - if (index < 0 || index > 65535) index = 0xFFFF; // prevent integer wrap around - customMappingTable[customMappingSize++] = index; - if (end != nullptr) break; // array closing ']' was in this chunk; stop before atoi() coerces trailing JSON keys into bogus entries - if (customMappingSize >= getLengthTotal()) break; - } else break; // there was nothing to read, stop + if (f && f.find("\"map\"")) { + int value; + while (entries++ < matrixSize && readNextIntFromFile(f, value)) { + if (value >= 0 && value < (int)_length) numPhy++; // valid physical pixel entry + } + f.seek(0); // go back to the start of the file (closing and re-opening is slow) + } + // we now know the max physical pixel used in the map, check if we have unmapped pixels left (_length is total physical) + if (entries > 0) { + const unsigned trailingCount = (_length > numPhy) ? _length - numPhy : 0; + const unsigned mapSize = matrixSize + trailingCount; + customMappingTable = static_cast(d_malloc(sizeof(uint16_t) * mapSize)); // prefer DRAM for speed + + if (customMappingTable) { + DEBUG_PRINTF_P(PSTR("ledmap allocated: %uB\n"), sizeof(uint16_t) * mapSize); + memset(customMappingTable, 0xFF, sizeof(uint16_t) * mapSize); // pre-fill with "-1" i.e. unmapped pixel + + // second pass: fill matrix entries from file + numPhy = 0; // reset + unsigned mapindex = 0; + if (f && f.find("\"map\"")) { // advance to "map", readNextIntFromFile discards any chars up to the first number + int value; + while (mapindex < mapSize && readNextIntFromFile(f, value)) { + if (value < 0 || value >= _length) value = 0xFFFF; // set out of range mappings to unused + customMappingTable[mapindex++] = (uint16_t)value; + //if (value < 0xFFFF) numPhy++; // count valid physical pixel entries (needed for auto-trailing only, see below) + } + } + + // TODO: this is a design choice: leave unmapped pixels black or append them as a strip? + // append any pixels missing in the LEDmap at the end in ascending order + // very simple walk-through search, users should map all pixels, this is a fallback + /* + if (numPhy < _length) { + for (unsigned p = 0; p < _length; p++) { + bool used = false; + // go through the whole map and check if this pixel index is not yet mapped + for (unsigned i = 0; i < mapSize; i++) { + if (customMappingTable[i] == p) { used = true; break; } + } + if (!used) customMappingTable[mapindex++] = (uint16_t)p; // append the unmapped pixel + if (mapindex >= mapSize) break; // safety check, should not happen + } + } + */ + customMappingSize = mapSize; + currentLedmap = n; + } else { + DEBUG_PRINTLN(F("ERROR LED map allocation error.")); + } + } + f.close(); // all done, close the file + } else { + // 1D set-up: allocate strip length and fill with entries from file + // partial maps leave indices beyond customMappingSize unmapped (-1) TODO: see note above about appending unmapped pixels + const unsigned mapSize = getLengthTotal(); + customMappingTable = static_cast(d_malloc(sizeof(uint16_t) * mapSize)); // prefer DRAM for speed + + if (customMappingTable) { + memset(customMappingTable, 0xFF, sizeof(uint16_t) * mapSize); // pre-fill with "-1" i.e. unmapped pixel + DEBUG_PRINTF_P(PSTR("ledmap allocated: %uB\n"), sizeof(uint16_t)*mapSize); + File f = WLED_FS.open(fileName, "r"); + if (f && f.find("\"map\":[")) { + int value; + unsigned mapindex = 0; + while (mapindex < mapSize && readNextIntFromFile(f, value)) { + if (value < 0 || value >= _length) value = 0xFFFF; // prevent integer wrap around + customMappingTable[mapindex++] = (uint16_t)value; + } + customMappingSize = mapSize; + currentLedmap = n; + f.close(); + } + } else { + DEBUG_PRINTLN(F("ERROR LED map allocation error.")); } - currentLedmap = n; - f.close(); + } - #ifdef WLED_DEBUG + #ifdef WLED_DEBUG + if (customMappingSize) { DEBUG_PRINT(F("Loaded ledmap:")); for (unsigned i=0; i 0); diff --git a/wled00/const.h b/wled00/const.h index 04ff8ded61..78e9382e2f 100644 --- a/wled00/const.h +++ b/wled00/const.h @@ -505,7 +505,7 @@ static_assert(WLED_MAX_BUSSES <= 32, "WLED_MAX_BUSSES exceeds hard limit"); #define JSON_LOCK_SERVEJSON 17 #define JSON_LOCK_NOTIFY 18 #define JSON_LOCK_PRESET_NAME 19 -#define JSON_LOCK_LEDGAP 20 +//#define JSON_LOCK_LEDGAP 20 // unused #define JSON_LOCK_LEDMAP_ENUM 21 #define JSON_LOCK_REMOTE 22 #define JSON_LOCK_OTA 23 diff --git a/wled00/fcn_declare.h b/wled00/fcn_declare.h index 6201a19192..c9d128e0f3 100644 --- a/wled00/fcn_declare.h +++ b/wled00/fcn_declare.h @@ -118,6 +118,7 @@ inline bool writeObjectToFileUsingId(const String &file, uint16_t id, const Json inline bool writeObjectToFile(const String &file, const char* key, const JsonDocument* content) { return writeObjectToFile(file.c_str(), key, content); }; inline bool readObjectFromFileUsingId(const String &file, uint16_t id, JsonDocument* dest, const JsonDocument* filter = nullptr) { return readObjectFromFileUsingId(file.c_str(), id, dest); }; inline bool readObjectFromFile(const String &file, const char* key, JsonDocument* dest, const JsonDocument* filter = nullptr) { return readObjectFromFile(file.c_str(), key, dest); }; +bool readNextIntFromFile(File &f, int &value); // helper for reading ledmaps bool copyFile(const char* src_path, const char* dst_path); bool backupFile(const char* filename); bool restoreFile(const char* filename); diff --git a/wled00/file.cpp b/wled00/file.cpp index 5a169d6450..fb49518302 100644 --- a/wled00/file.cpp +++ b/wled00/file.cpp @@ -363,6 +363,49 @@ bool readObjectFromFile(const char* file, const char* key, JsonDocument* dest, c return true; } +// helper to read comma-separated integers from a JSON array +// reads until it finds "-" or a number char, converts the number, reads until ',' (discarded) or ']' (kept for next call) +// returns false when the array terminator ']' or EOF is reached without a valid number +// sets value to -1 if not a number, discards garbage values following a valid number +bool readNextIntFromFile(File &f, int &value) { + value = 0; + bool foundDigit = false; + bool negative = false; + while (f.available()) { + char c = (char)f.peek(); + if (c >= '0' && c <= '9') { + if (value < (0x7FFFFFFF / 10)) value = value * 10 + (c - '0'); // saturate instead of overflowing if too large + foundDigit = true; + } else if (c == '-' && !foundDigit) { + negative = true; // leading minus, return negative value of number + } else if (c == ',') { + f.read(); // consume separator comma + if (!foundDigit) { + value = -1; // invalid input, make it -1 + return true; // not end of file yet + } + break; // number complete + } else if (c == ']') { + if (foundDigit) return true; // leave ']' available for the next call to terminate with "false" + f.read(); // consume array terminator (support multiple arrays in a file) + return false; + } else if (foundDigit) { + // number followed by a char/whitespace - malformed, skip everything up to the next ',' or ']' + while (f.available()) { + char d = (char)f.peek(); + if (d == ',') { f.read(); return true; } // consume the ',' + if (d == ']') { return true; } // leave ']' for the next call to terminate the array + f.read(); + } + return true; // EOF but we have a number, let the next call return false + } + f.read(); // consume the peeked character + } + if (!foundDigit) return false; + if (negative) value = -value; + return true; +} + void updateFSInfo() { #ifdef ARDUINO_ARCH_ESP32 #if WLED_FS == LITTLEFS || ESP_IDF_VERSION_MAJOR >= 4