diff --git a/workspace/__ardulib__/Graphics/BitmapReader.h b/workspace/__ardulib__/Graphics/BitmapReader.h new file mode 100644 index 000000000..7c1163a75 --- /dev/null +++ b/workspace/__ardulib__/Graphics/BitmapReader.h @@ -0,0 +1,159 @@ + +#ifndef BITMAP_READER_H +#define BITMAP_READER_H + +#include +#include "XGraphics.h" + +enum BitmapReturnCode { // It is invisible to the enduser but very useful for debugging. + BITMAP_SUCCESS, + BITMAP_ERROR_FILE_OPEN, + BITMAP_ERROR_WRONG_FILE_FORMAT, + BITMAP_ERROR_WRONG_BMP +}; + +struct BitmapHeader { + BitmapHeader() + : bfOffBits(0) + , biSize(0) + , biWidth(0) + , biHeight(0) + , isFlipped(false) + , biPlanes(0) + , biBitCount(0) + , biCompression(0) {} + + uint32_t bfOffBits; // The offset, i.e. starting address, of the byte where the bitmap image data (pixel array) can be found. + uint8_t biSize; // The size of the header. + int16_t biWidth; // The bitmap width in pixels. + int16_t biHeight; // The bitmap height in pixels. Can be < 0. + bool isFlipped; // A BMP is stored bottom-to-top. + uint8_t biPlanes; // The number of color planes, must be 1. Other values used for WIN icons. + uint8_t biBitCount; // the number of bits per pixel, which is the color depth of the image. Typical values are 1, 4, 8, 16, 24 and 32. + uint8_t biCompression; // the compression method being used. Typical values are [0,6]. +}; + +class BitmapReader { +public: + ~BitmapReader(); + + void linkSDDevice(SDClass* sd); + + BitmapReturnCode readBitmap(char* bitmapFSPath); + BitmapReturnCode fillScanlineBuffer(char* bitmapFSPath, int16_t scanline, BBox imageBBox, uint16_t* buffer, size_t bufferSize); + +private: + SDClass* _sd; + File _file; + + BitmapHeader _bitmapHeader; + + uint16_t _readLE16(); + uint32_t _readLE32(); +}; + +BitmapReader::~BitmapReader() { + if (_file) + _file.close(); +} + +void BitmapReader::linkSDDevice(SDClass* sd) { + _sd = sd; +} + +BitmapReturnCode BitmapReader::readBitmap(char* bitmapFSPath) { + + if (!(_file = _sd->open(bitmapFSPath, O_READ))) { // Open a new BMP file. + _file.close(); + return BITMAP_ERROR_FILE_OPEN; + } + + if (_readLE16() != 0x4D42) { // Check BMP signature. + _file.close(); + return BITMAP_ERROR_WRONG_FILE_FORMAT; + } + + _readLE32(); // Skip reading bfSize. + _readLE32(); // Skip reading bfReserved. + + _bitmapHeader.bfOffBits = _readLE32(); // Read bfOffBits. + _bitmapHeader.biSize = _readLE32(); // Read biSize/bV4Size/bV5Size. + + _bitmapHeader.biWidth = _readLE32(); // Read biWidth/bV4Width/bV5Width. + _bitmapHeader.biHeight = _readLE32(); // Read biHeight/bV4Height/bV5Height. + if (_bitmapHeader.biHeight < 0) { + _bitmapHeader.biHeight = -_bitmapHeader.biHeight; + _bitmapHeader.isFlipped = true; + } + + _bitmapHeader.biPlanes = _readLE16(); // Read biPlanes/bV4Planes/bV5Planes. + _bitmapHeader.biBitCount = _readLE16(); // Read biBitCount/bV4BitCount/bV5BitCount. + _bitmapHeader.biCompression = _readLE32(); // Read biCompression/bV4V4Compression/bV5Compression. + + _file.close(); + + // Check for only straightforward case. + if (_bitmapHeader.biSize != 40 || _bitmapHeader.biPlanes != 1 || _bitmapHeader.biBitCount != 24 || _bitmapHeader.biCompression != 0) { + _bitmapHeader = BitmapHeader(); // Reset bitmap. + return BITMAP_ERROR_WRONG_BMP; + } + + return BITMAP_SUCCESS; +} + +BitmapReturnCode BitmapReader::fillScanlineBuffer(char* bitmapFSPath, int16_t scanline, BBox imageBBox, uint16_t* buffer, size_t bufferSize) { + if (!(_file = _sd->open(bitmapFSPath, O_READ))) { + _file.close(); + return BITMAP_ERROR_FILE_OPEN; + } + + uint16_t imageLine = scanline - imageBBox.pivot.y; // Calculate current line of the image. + + if (imageLine > _bitmapHeader.biHeight - 1) // Tile vert. If more than one BMP in the image. + imageLine = imageLine - _bitmapHeader.biHeight * (imageLine / _bitmapHeader.biHeight); + + uint16_t bitmapArrayLine = _bitmapHeader.isFlipped ? imageLine : _bitmapHeader.biHeight - imageLine - 1; // Calculate current Bitmap line. + + uint8_t emptyBytesCount = _bitmapHeader.biWidth % 4; // The amount of bytes at the BMP scanline must be a multiple of 4 bytes. + uint32_t bitmapLineStart = _bitmapHeader.bfOffBits + bitmapArrayLine * 3 * _bitmapHeader.biWidth + emptyBytesCount * bitmapArrayLine; // The number of starting byte of the line. + + for (int16_t x = imageBBox.pivot.x, c = 0; x < imageBBox.pivot.x + imageBBox.width; x++, c++) { + + if ((c == 0) || (c % (_bitmapHeader.biWidth - 1) == 0)) // Tile horizontally. If we are at frame. + if (!(_file.seek(bitmapLineStart))) // Return to start. + continue; // If for some case miss the byte, do not read it. + + uint16_t color = ((_file.read() >> 3) | ((_file.read() & 0xFC) << 3) | ((_file.read() & 0xF8) << 8)); // in BMP a pixel color is hold as BGR888. + if (x >= 0 && x < bufferSize) + buffer[x] = color; + } + + _file.close(); + return BITMAP_SUCCESS; +} + +uint16_t BitmapReader::_readLE16() { +#if !defined(ESP32) && !defined(ESP8266) && (__BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__) + // Read directly into result. BMP data and variable both little-endian. + uint16_t result; + _file.read(&result, sizeof result); + return result; +#else + // Big-endian or unknown. Byte-by-byte read will perform reversal if needed. + return _file.read() | ((uint16_t)_file.read() << 8); +#endif +} + +uint32_t BitmapReader::_readLE32() { +#if !defined(ESP32) && !defined(ESP8266) && (__BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__) + // Read directly into result. BMP data and variable both little-endian. + uint32_t result; + _file.read(&result, sizeof result); + return result; +#else + // Big-endian or unknown. Byte-by-byte read will perform reversal if needed. + return _file.read() | ((uint16_t)_file.read() << 8) | ((uint16_t)_file.read() << 16) | ((uint16_t)_file.read() << 24); +#endif +} + +#endif // BITMAP_READER_H diff --git a/workspace/__ardulib__/Graphics/ImageSD.h b/workspace/__ardulib__/Graphics/ImageSD.h new file mode 100644 index 000000000..734bb8563 --- /dev/null +++ b/workspace/__ardulib__/Graphics/ImageSD.h @@ -0,0 +1,26 @@ + +#ifndef IMAGE_SD_H +#define IMAGE_SD_H + +#include "BitmapReader.h" +#include "XGraphics.h" + +class ImageSD : public XGraphics { +private: + BitmapReader _bitmapReader; + BBox _imageBBox; + + char* _bitmapFSPath; + +public: + ImageSD(XGraphics* parent, SDClass* sd); + + bool linkBitmapFSPath(char* bitmapFSPath); + + void setImagePosition(int16_t x, int16_t y, int16_t w, int16_t h); + void renderScanline(XRenderer* renderer, int16_t scanline, uint16_t* buffer, size_t bufferSize); +}; + +#include "ImageSD.inl" + +#endif // IMAGE_SD_H diff --git a/workspace/__ardulib__/Graphics/ImageSD.inl b/workspace/__ardulib__/Graphics/ImageSD.inl new file mode 100644 index 000000000..a0b9bee88 --- /dev/null +++ b/workspace/__ardulib__/Graphics/ImageSD.inl @@ -0,0 +1,25 @@ + +ImageSD::ImageSD(XGraphics* parent, SDClass* sd) + : XGraphics(parent) { + _bitmapReader.linkSDDevice(sd); +} + +bool ImageSD::linkBitmapFSPath(char* bitmapFSPath) { + _bitmapFSPath = bitmapFSPath; + BitmapReturnCode res = _bitmapReader.readBitmap(_bitmapFSPath); + return res == BITMAP_SUCCESS ? 0 : 1; +} + +void ImageSD::setImagePosition(int16_t x, int16_t y, int16_t w, int16_t h) { + _imageBBox.pivot = XVector2(x, y); + _imageBBox.width = w; + _imageBBox.height = h; +} + +void ImageSD::renderScanline(XRenderer* renderer, int16_t scanline, uint16_t* buffer, size_t bufferSize) { + + if (scanline < _imageBBox.pivot.y || scanline > _imageBBox.pivot.y + _imageBBox.height - 1) + return; + + _bitmapReader.fillScanlineBuffer(_bitmapFSPath, scanline, _imageBBox, buffer, bufferSize); +} diff --git a/workspace/__lib__/xod-dev/sd/project.xod b/workspace/__lib__/xod-dev/sd/project.xod new file mode 100644 index 000000000..fa9756cd8 --- /dev/null +++ b/workspace/__lib__/xod-dev/sd/project.xod @@ -0,0 +1,9 @@ +{ + "authors": [ + "XOD" + ], + "description": "Nodes to work with SDcards", + "license": "AGPL-3.0", + "name": "sd", + "version": "0.33.0" +} diff --git a/workspace/__lib__/xod-dev/sd/sd-device/patch.cpp b/workspace/__lib__/xod-dev/sd/sd-device/patch.cpp new file mode 100644 index 000000000..20913d1b6 --- /dev/null +++ b/workspace/__lib__/xod-dev/sd/sd-device/patch.cpp @@ -0,0 +1,31 @@ + +{{#global}} +#include +#include +{{/global}} + +struct State { + uint8_t mem[sizeof(SDClass)]; + SDClass* sd; +}; + +using Type = SDClass*; + +{{ GENERATED_CODE }} + +void evaluate(Context ctx) { + + auto state = getState(ctx); + + if (isSettingUp()) { + state->sd = new (state->mem) SDClass(); + } + + auto csPin = getValue(ctx); + + if (!state->sd->begin(csPin)) { + raiseError(ctx); + return; + } + emitValue(ctx, state->sd); +} diff --git a/workspace/__lib__/xod-dev/sd/sd-device/patch.xodp b/workspace/__lib__/xod-dev/sd/sd-device/patch.xodp new file mode 100644 index 000000000..3867d1db1 --- /dev/null +++ b/workspace/__lib__/xod-dev/sd/sd-device/patch.xodp @@ -0,0 +1,36 @@ +{ + "description": "Constructs an SDcard device.", + "nodes": [ + { + "description": "CS (chip select) pin the SD card reader is connected to. Also known as SS (slave select).", + "id": "ByHVa_XtL", + "label": "CS", + "position": { + "units": "slots", + "x": -12, + "y": -2 + }, + "type": "xod/patch-nodes/input-port" + }, + { + "id": "HklSN6uQYU", + "position": { + "units": "slots", + "x": -12, + "y": -1 + }, + "type": "xod/patch-nodes/not-implemented-in-xod" + }, + { + "description": "SDcard device.", + "id": "Sy6VT_7tL", + "label": "DEV", + "position": { + "units": "slots", + "x": -12, + "y": 0 + }, + "type": "xod/patch-nodes/output-self" + } + ] +} diff --git a/workspace/__lib__/xod-dev/sd/sd-log/patch.xodp b/workspace/__lib__/xod-dev/sd/sd-log/patch.xodp new file mode 100644 index 000000000..3481ed0b0 --- /dev/null +++ b/workspace/__lib__/xod-dev/sd/sd-log/patch.xodp @@ -0,0 +1,169 @@ +{ + "description": "Quickstart node. Appends lines of text to a file on SD card. Possible errors: — Can't open a file — Initialization failed or no SD card — Can't write data to SD card.", + "links": [ + { + "id": "ByDS8YQt8", + "input": { + "nodeId": "rkrMUKmtU", + "pinKey": "S1lM0JKmFL" + }, + "output": { + "nodeId": "HJSB8FQY8", + "pinKey": "rksccsp-W" + } + }, + { + "id": "H1OSIKmFU", + "input": { + "nodeId": "HJSB8FQY8", + "pinKey": "Hkqu9oaWb" + }, + "output": { + "nodeId": "r1xHXUtmF8", + "pinKey": "__out__" + } + }, + { + "id": "S18z8FmYL", + "input": { + "nodeId": "rkrMUKmtU", + "pinKey": "SkMTJY7YU" + }, + "output": { + "nodeId": "BJIk8KmFL", + "pinKey": "Sy6VT_7tL" + } + }, + { + "id": "S1FQLY7F8", + "input": { + "nodeId": "rkrMUKmtU", + "pinKey": "B1bfAJY7FL" + }, + "output": { + "nodeId": "B1bBXIYQYI", + "pinKey": "__out__" + } + }, + { + "id": "SkgcmUK7KL", + "input": { + "nodeId": "rkrMUKmtU", + "pinKey": "S1MCJFXt8" + }, + "output": { + "nodeId": "rkrm8KmKL", + "pinKey": "__out__" + } + }, + { + "id": "SydNLY7YI", + "input": { + "nodeId": "BJIk8KmFL", + "pinKey": "ByHVa_XtL" + }, + "output": { + "nodeId": "By4E8K7K8", + "pinKey": "__out__" + } + }, + { + "id": "ryPo8t7YI", + "input": { + "nodeId": "H14i8t7YL", + "pinKey": "__in__" + }, + "output": { + "nodeId": "rkrMUKmtU", + "pinKey": "ryl1xK7KI" + } + } + ], + "nodes": [ + { + "description": "File name to append to.", + "id": "B1bBXIYQYI", + "label": "FILE", + "position": { + "units": "slots", + "x": 3, + "y": 0 + }, + "type": "xod/patch-nodes/input-string" + }, + { + "id": "BJIk8KmFL", + "position": { + "units": "slots", + "x": 0, + "y": 1 + }, + "type": "@/sd-device" + }, + { + "description": "CS (chip select) pin the SD card reader is connected to. Also known as SS (slave select).", + "id": "By4E8K7K8", + "label": "CS", + "position": { + "units": "slots", + "x": 0, + "y": 0 + }, + "type": "xod/patch-nodes/input-port" + }, + { + "description": "Fires when write is done.", + "id": "H14i8t7YL", + "label": "DONE", + "position": { + "units": "slots", + "x": 3, + "y": 2 + }, + "type": "xod/patch-nodes/output-pulse" + }, + { + "boundLiterals": { + "BkeKcj6ZZ": "\"\\n\"" + }, + "id": "HJSB8FQY8", + "position": { + "units": "slots", + "x": 4, + "y": -1 + }, + "type": "xod/core/concat" + }, + { + "description": "Line to append.", + "id": "r1xHXUtmF8", + "label": "LINE", + "position": { + "units": "slots", + "x": 4, + "y": -2 + }, + "type": "xod/patch-nodes/input-string" + }, + { + "id": "rkrMUKmtU", + "position": { + "units": "slots", + "x": 2, + "y": 1 + }, + "type": "@/write-file" + }, + { + "description": "Perform file open, write, flush, close cycle.", + "id": "rkrm8KmKL", + "label": "W", + "position": { + "units": "slots", + "x": 5, + "y": 0 + }, + "type": "xod/patch-nodes/input-pulse" + } + ] +} diff --git a/workspace/__lib__/xod-dev/sd/write-file/patch.cpp b/workspace/__lib__/xod-dev/sd/write-file/patch.cpp new file mode 100644 index 000000000..e963a82aa --- /dev/null +++ b/workspace/__lib__/xod-dev/sd/write-file/patch.cpp @@ -0,0 +1,38 @@ + +struct State {}; + +{{ GENERATED_CODE }} + +void evaluate(Context ctx) { + + auto sd = getValue(ctx); + emitValue(ctx, sd); + + if (!isInputDirty(ctx)) + return; + + char filename[24] = { 0 }; + dump(getValue(ctx), filename); + + File file = sd->open(filename, O_WRITE | O_CREAT | O_APPEND); + + if (!file) { + // Failed to open the file. Maybe, SD card gone, try to reinit next time + raiseError(ctx); // Can't open file + return; + } + + XString line = getValue(ctx); + size_t lastWriteSize; + for (auto it = line.iterate(); it; ++it) { + lastWriteSize = file.print(*it); + if (lastWriteSize == 0) { + raiseError(ctx); // No bytes written + return; + } + } + + file.flush(); + file.close(); + emitValue(ctx, 1); +} diff --git a/workspace/__lib__/xod-dev/sd/write-file/patch.xodp b/workspace/__lib__/xod-dev/sd/write-file/patch.xodp new file mode 100644 index 000000000..5225ade2d --- /dev/null +++ b/workspace/__lib__/xod-dev/sd/write-file/patch.xodp @@ -0,0 +1,80 @@ +{ + "description": "Writes a text data to a file on SD card. Possible errors: — Can't open a file — Initialization failed or no SD card — Can't write data to SD card.", + "nodes": [ + { + "description": "File name to write to.", + "id": "B1bfAJY7FL", + "label": "FILE", + "position": { + "units": "slots", + "x": 2, + "y": 1 + }, + "type": "xod/patch-nodes/input-string" + }, + { + "id": "HJuTkYQYU", + "position": { + "units": "slots", + "x": 1, + "y": 2 + }, + "type": "xod/patch-nodes/not-implemented-in-xod" + }, + { + "description": "Perform file open, write, flush, close cycle.", + "id": "S1MCJFXt8", + "label": "DO", + "position": { + "units": "slots", + "x": 4, + "y": 1 + }, + "type": "xod/patch-nodes/input-pulse" + }, + { + "description": "Data to write.", + "id": "S1lM0JKmFL", + "label": "DATA", + "position": { + "units": "slots", + "x": 3, + "y": 1 + }, + "type": "xod/patch-nodes/input-string" + }, + { + "description": "SDcard device.", + "id": "SkMTJY7YU", + "label": "DEV", + "position": { + "units": "slots", + "x": 1, + "y": 1 + }, + "type": "@/input-sd-device" + }, + { + "description": "Fires when write is done.", + "id": "ryl1xK7KI", + "label": "DONE", + "position": { + "units": "slots", + "x": 2, + "y": 3 + }, + "type": "xod/patch-nodes/output-pulse" + }, + { + "description": "SDcard device.", + "id": "ryqyZKXFI", + "label": "DEV'", + "position": { + "units": "slots", + "x": 1, + "y": 3 + }, + "type": "@/output-sd-device" + } + ] +} diff --git a/workspace/__lib__/xod/common-hardware/sd-log/patch.xodp b/workspace/__lib__/xod/common-hardware/sd-log/patch.xodp index 4fa55b45e..c4e881ad5 100644 --- a/workspace/__lib__/xod/common-hardware/sd-log/patch.xodp +++ b/workspace/__lib__/xod/common-hardware/sd-log/patch.xodp @@ -34,6 +34,15 @@ }, "type": "xod/patch-nodes/input-pulse" }, + { + "id": "Hycz91D5U", + "position": { + "units": "slots", + "x": 7, + "y": 1 + }, + "type": "xod/patch-nodes/deprecated" + }, { "description": "Line to append", "id": "SJ1CkNggf", diff --git a/workspace/__lib__/xod/graphics/sd-image/patch.cpp b/workspace/__lib__/xod/graphics/sd-image/patch.cpp new file mode 100644 index 000000000..829881493 --- /dev/null +++ b/workspace/__lib__/xod/graphics/sd-image/patch.cpp @@ -0,0 +1,59 @@ + +// clang-format off +{{#global}} +#include +{{/global}} +// clang-format on + +struct State { + uint8_t mem[sizeof(ImageSD)]; + ImageSD* imageSD; + int16_t x, y, w, h; + + char bitmapFSPath[24]; // A 24 chars maximum filepath. +}; + +// clang-format off +{{ GENERATED_CODE }} +// clang-format on + +void evaluate(Context ctx) { + auto state = getState(ctx); + + auto gfx = getValue(ctx); + auto sd = getValue(ctx); + + int16_t x = (int16_t)getValue(ctx); + int16_t y = (int16_t)getValue(ctx); + int16_t w = (int16_t)getValue(ctx); + int16_t h = (int16_t)getValue(ctx); + + auto path = getValue(ctx); + + if (isSettingUp()) { + state->imageSD = new (state->mem) ImageSD(gfx, sd); + } + + if (isInputDirty(ctx)) { + emitValue(ctx, state->imageSD); // If upstream is ok pass it. + } + + if (isSettingUp() || x != state->x || y != state->y || w != state->w || h != state->h || isInputDirty(ctx)) { + state->x = x; + state->y = y; + state->w = w; + state->h = h; + state->imageSD->setImagePosition(x, y, w, h); + + memset(state->bitmapFSPath, '\0', 24); + dump(path, state->bitmapFSPath); + + if (state->imageSD->linkBitmapFSPath(state->bitmapFSPath)) { + raiseError(ctx); // Failed to load BMP file or a file has wrong format/version. + return; + } + + emitValue(ctx, state->imageSD); // Pass only is everthing is ok. + } + +} diff --git a/workspace/__lib__/xod/graphics/sd-image/patch.xodp b/workspace/__lib__/xod/graphics/sd-image/patch.xodp new file mode 100644 index 000000000..b8aad3d81 --- /dev/null +++ b/workspace/__lib__/xod/graphics/sd-image/patch.xodp @@ -0,0 +1,93 @@ +{ + "nodes": [ + { + "id": "B1Egvulc8", + "label": "W", + "position": { + "units": "slots", + "x": 5, + "y": 0 + }, + "type": "xod/patch-nodes/input-number" + }, + { + "id": "B1Z2yB_lq8", + "label": "GFX", + "position": { + "units": "slots", + "x": 0, + "y": 0 + }, + "type": "@/input-graphics" + }, + { + "id": "BkG3ySOe5I", + "position": { + "units": "slots", + "x": 0, + "y": 1 + }, + "type": "xod/patch-nodes/not-implemented-in-xod" + }, + { + "id": "H12Jr_lcI", + "label": "X", + "position": { + "units": "slots", + "x": 3, + "y": 0 + }, + "type": "xod/patch-nodes/input-number" + }, + { + "id": "HySh1rdx9L", + "label": "GFX'", + "position": { + "units": "slots", + "x": 0, + "y": 2 + }, + "type": "@/output-graphics" + }, + { + "id": "SJJSw_gcL", + "label": "FILE", + "position": { + "units": "slots", + "x": 2, + "y": 0 + }, + "type": "xod/patch-nodes/input-string" + }, + { + "id": "r1gh1S_gcL", + "label": "Y", + "position": { + "units": "slots", + "x": 4, + "y": 0 + }, + "type": "xod/patch-nodes/input-number" + }, + { + "id": "r1lmDOlqI", + "label": "SD", + "position": { + "units": "slots", + "x": 1, + "y": 0 + }, + "type": "xod-dev/sd/input-sd-device" + }, + { + "id": "rJeNgwueqU", + "label": "H", + "position": { + "units": "slots", + "x": 6, + "y": 0 + }, + "type": "xod/patch-nodes/input-number" + } + ] +}