Browse Source

Some Inkplate class methods adjustments

v0.9.4
Guy Turcotte 2 years ago
parent
commit
9fdef6f6bb
  1. 88
      CHANGES.md
  2. 93
      README.md
  3. 1
      examples/Basic_Inkplate_Functionality/Inkplate_basic_BW/.gitignore
  4. 3
      examples/Others/Inkplate_Mandelbrot_set/.gitignore
  5. 3
      examples/Others/Inkplate_Mandelbrot_set/CMakeLists.txt
  6. 8
      examples/Others/Inkplate_Mandelbrot_set/partitions.csv
  7. 39
      examples/Others/Inkplate_Mandelbrot_set/platformio.ini
  8. 1145
      examples/Others/Inkplate_Mandelbrot_set/sdkconfig
  9. 6
      examples/Others/Inkplate_Mandelbrot_set/src/CMakeLists.txt
  10. 110
      examples/Others/Inkplate_Mandelbrot_set/src/main.cpp
  11. 3
      examples/Others/Inkplate_Maze_Generator/.gitignore
  12. 3
      examples/Others/Inkplate_Maze_Generator/CMakeLists.txt
  13. 8
      examples/Others/Inkplate_Maze_Generator/partitions.csv
  14. 39
      examples/Others/Inkplate_Maze_Generator/platformio.ini
  15. 1145
      examples/Others/Inkplate_Maze_Generator/sdkconfig
  16. 6
      examples/Others/Inkplate_Maze_Generator/src/CMakeLists.txt
  17. 182
      examples/Others/Inkplate_Maze_Generator/src/main.cpp
  18. 3
      examples/Others/Inkplate_Peripheral_Mode/.gitignore
  19. 3
      examples/Others/Inkplate_Peripheral_Mode/CMakeLists.txt
  20. 8
      examples/Others/Inkplate_Peripheral_Mode/partitions.csv
  21. 39
      examples/Others/Inkplate_Peripheral_Mode/platformio.ini
  22. 1145
      examples/Others/Inkplate_Peripheral_Mode/sdkconfig
  23. 6
      examples/Others/Inkplate_Peripheral_Mode/src/CMakeLists.txt
  24. 432
      examples/Others/Inkplate_Peripheral_Mode/src/main.cpp
  25. 3
      examples/Others/Inkplate_VariPass_Graphs/.gitignore
  26. 3
      examples/Others/Inkplate_VariPass_Graphs/CMakeLists.txt
  27. 8
      examples/Others/Inkplate_VariPass_Graphs/partitions.csv
  28. 39
      examples/Others/Inkplate_VariPass_Graphs/platformio.ini
  29. 1145
      examples/Others/Inkplate_VariPass_Graphs/sdkconfig
  30. 78
      examples/Others/Inkplate_VariPass_Graphs/src/Inkplate_VariPass_Graphs.ino
  31. 18
      include/graphical/inkplate.hpp
  32. 4
      src/graphical/graphics.cpp

88
CHANGES.md

@ -0,0 +1,88 @@
# Modifications done to the Arduino Inkplate Source code
As you can expect, the ESP-IDF framework is quit different than the Arduino framework. The porting effort of the Inkplate source code done here is not just a transformation to make it runs, but also getting it inline with the available packages available with the ESP-IDF. Many aspects must be transformed in this regard.
The code is also being transformed to be closer to C++ functionalities, targetting a stronger usage of the language elements that will sustain the author's software developement. A bit less C and more C++...
The source code is divided in two major groups: the low-level device drivers group, under the `inkplate_platform` class, and the *mid-level* graphical group, under the `inkplate` class. The coupling between these groups is minimal: one can take the `inkplate-platform` and build on top of it without the graphical portion. As the graphical group rely on the drivers, it is inline with the Arduino counterpart. The coupling between the two groups is light as it is done through aggregation instead of inheritence.
Here is a list of the changes being done:
## Global changes
Some of these changes have been done partially and will be completed in subsequent versions.
- Give the overall drivers source code a transformation to use what is called *modern C++*, alligned with C++11 and beyond
- All .h files are renamed .hpp
- macro definitions (`#define`) are reduced to a bare minimum
- `enum class` are used everywhere possible in the drivers group. This reinforce source code documentation and compiler support for finding coding errors.
- source code filenames are all in lower_case. A lot easier visually to digg into the source folder and easier to remember filenames.
- all driver and support classes are using the following naming conventions:
- class names are in ChamelCase
- constants are in UPPER_CASE
- variables and method names are in lower_case
- all graphical libraries and class remains with their Arduino naming convention
- classes inheritence are reduced to insure independance of usage of the drivers group. By design, the grapphical portion remains as defined for the Arduino framework with some internal transformation to be more inline with C++.
- A TAG is added in the protected/private portion of driver classes in support of the ESP-IDF Logging mechanism (ESP_LOGx defines)
- min() definition replaced with std::min()
- _swap...() definitions replaced with std::swap()
- String replaced with std::string (No PROGMEM support required with the ESP32)
## Wire class and multi-threading support
A Wire class that mimics the equivalent Arduino class has been added to the library.
Access control to the I2C interface has been added to allow for interrupt based devices access and multi-threading. In the library, all code sequences that require the use of I2C are calling class methods `Wire::enter()` and `Wire::leave()` to reserve and free the interface.
## defines.hpp
The content is now at a bare minimum: `DisplayMode` enum class that defines both `INKPLATE_1BIT` and `INKPLATE_3BIT` modes. Also, `BLACK` and `WHITE` color values for INKPLATE_1BIT mode.
As the DisplayMode is an enum class, it is then required to access the values as follow:
- DisplayMode::INKPLATE_1BIT
- DisplayMode::INKPLATE_3BIT
## graphics (.hpp, .cpp)
The class is now the owner of the _partial and DMemory4Bit frame buffers. As such, methods that where defined in Inkplate.hpp are relocated into that class, namely:
- clearDisplay()
- display()
- preloadScreen()
- partialUpdate()
They select the appropriate frame buffer and call the e_ink class methods.
This change is transparent for the user application.
## SdCard
- The module is used only to initialize the ESP-IDF drivers (SPI and FAT filesystem are used). The card can then be accessed through the standard C++/C capabilities, as supplied through the ESP-IDF framework. All filenames located on the card must be prefixed with `/sdcard/`.
## Image (image.hpp, image.cpp)
- the `drawXXXXFromWeb(WiFiClient *s, ...)` methods are no longer available. WiFiCLient doesn't exists in an ESP-IDF context.
- the `bool drawXXXXFromSd(SdFile *p, ...)` methods are renamed `bool drawXXXXFromFile(FILE *p, ...)`. This allow for accessing files located in a SPIFFS partition (using `/spiffs/` filename prefix) as well as SDCard files (using `/sdcard/` filename prefix) or any other file system integrated with ESP-IDF.
- functions-like related to image / pixel manipulation present in `defines.h` have been migrated here. Namely: RED, BLUE, GREEN, READ32, READ16, ROWSIZE, RGB8BIT, RGB3BIT. They are now inline functions with appropriate types named red, blue, green, read32, read16, rowSize, rgb8Bit, rgb3Bit.
## mcp23017 (.hpp, .cpp)
This class is implementing a generic MCP23017 driver. It is instanciated in the inkplate_platform.hpp, depending on the type of Inkplate device:
- as mcp_int (for all Inkplate devices)
- as mcp_ext (for the Inkplate-10, and Inkplate-6Plus).
Each class that uses the MCPs are independant from each other in terms of initialzing and accessing the MCP. This is the case for EInk, Battery, TouchKeys, Backlight. They all uses `Wire::enter()` and `Wire::leave()` to reserve access to the I2C bus.
## battery, touch_keys (.hpp, .cpp)
Those classes implement basic access to the battery and touch keys state.
## system class renamed InkplatePlatform
This name reflect more what it is. This class will is currently under eavy changes.
## FrameBuffer classes
A hierarchy of frame buffer classes has been added. These allow for flexible adaptation to the different geometry of devices and pixel sizes.

93
README.md

@ -1,100 +1,13 @@
# ESP-IDF-InkPlate Version 0.9.0
# ESP-IDF-InkPlate Library Version 0.9.0
A porting effort to the ESP-IDF framework for the e-Radionica InkPlate software that can be find [here](https://github.com/e-radionicacom/Inkplate-Arduino-library).
--> Work in progress. Not ready yet. <--
Look in the `examples` folder for ready-made applications. This library is conform to the PlatformIO IDE extension. All examples can be compiled using PlatformIO.
This project is a working example. You can build it using PlatformIO.
Information about the modifications is located in file **CHANGES.md**
## Modifications done to the Arduino Inkplate Source code
As you can expect, the ESP-IDF framework is quit different than the Arduino framework. The porting effort of the Inkplate source code done here is not just a transformation to make it runs, but also getting it inline with the available packages available with the ESP-IDF. Many aspects must be transformed in this regard.
The code is also being transformed to be closer to C++ functionalities, targetting a stronger usage of the language elements that will sustain the author's software developement. A bit less C and more C++...
The source code is divided in two major groups: the low-level device drivers group, under the `inkplate_platform` class, and the *mid-level* graphical group, under the `inkplate` class. The coupling between these groups is minimal: one can take the `inkplate-platform` and build on top of it without the graphical portion. As the graphical group rely on the drivers, it is inline with the Arduino counterpart. The coupling between the two groups is light as it is done through aggregation instead of inheritence.
Here is a list of the changes being done:
### Global changes
Some of these changes have been done partially and will be completed in subsequent versions.
- Give the overall drivers source code a transformation to use what is called *modern C++*, alligned with C++11 and beyond
- All .h files are renamed .hpp
- macro definitions (`#define`) are reduced to a bare minimum
- `enum class` are used everywhere possible in the drivers group. This reinforce source code documentation and compiler support for finding coding errors.
- source code filenames are all in lower_case. A lot easier visually to digg into the source folder.
- all driver and support classes are using the following naming conventions:
- class names are in ChamelCase
- constants are in UPPER_CASE
- variables and method names are in lower_case
- all graphical libraries and class remains with their Arduino naming convention
- classes inheritence are reduced to insure independance of usage of the drivers group. By design, the grapphical portion remains as defined for the Arduino framework with some internal transformation to be more inline with C++.
- A TAG is added in the protected/private portion of driver classes in support of the ESP-IDF Logging mechanism (ESP_LOGx defines)
- min() definition replaced with std::min()
- _swap...() definitions replaced with std::swap()
- String replaced with std::string (No PROGMEM support required with the ESP32)
### Wire class and multi-threading support
A Wire class that mimics the equivalent Arduino class has been added to the library.
Access control to the I2C interface has been added to allow for interrupt based devices access and multi-threading. In the library, all code sequences that require the use of I2C are calling class methods `Wire::enter()` and `Wire::leave()` to reserve and free the interface.
### defines.hpp
The content is now at a bare minimum: `DisplayMode` enum class that defines both `INKPLATE_1BIT` and `INKPLATE_3BIT` modes. Also, `BLACK` and `WHITE` color values for INKPLATE_1BIT mode.
As the DisplayMode is an enum class, it is then required to access the values as follow:
- DisplayMode::INKPLATE_1BIT
- DisplayMode::INKPLATE_3BIT
### graphics (.hpp, .cpp)
The class is now the owner of the _partial and DMemory4Bit frame buffers. As such, methods that where defined in Inkplate.hpp are relocated into that class, namely:
- clearDisplay()
- display()
- preloadScreen()
- partialUpdate()
They select the appropriate frame buffer and call the e_ink class methods.
This change is transparent for the user application.
### SdCard
- The module is used only to initialize the ESP-IDF drivers (SPI and FAT filesystem are used). The card can then be accessed through the standard C++/C capabilities, as supplied through the ESP-IDF framework. All filenames located on the card must be prefixed with `/sdcard/`.
### Image (image.hpp, image.cpp)
- the `drawXXXXFromWeb(WiFiClient *s, ...)` methods are no longer available. WiFiCLient doesn't exists in an ESP-IDF context.
- the `bool drawXXXXFromSd(SdFile *p, ...)` methods are renamed `bool drawXXXXFromFile(FILE *p, ...)`. This allow for accessing files located in a SPIFFS partition (using `/spiffs/` filename prefix) as well as SDCard files (using `/sdcard/` filename prefix) or any other file system integrated with ESP-IDF.
- functions-like related to image / pixel manipulation present in `defines.h` have been migrated here. Namely: RED, BLUE, GREEN, READ32, READ16, ROWSIZE, RGB8BIT, RGB3BIT. They are now inline functions with appropriate types named red, blue, green, read32, read16, rowSize, rgb8Bit, rgb3Bit.
### mcp23017 (.hpp, .cpp)
This class is implementing a generic MCP23017 driver. It is instanciated in the inkplate_platform.hpp, depending on the type of Inkplate device:
- as mcp_int (for all Inkplate devices)
- as mcp_ext (for the Inkplate-10, and Inkplate-6Plus).
Each class that uses the MCPs are independant from each other in terms of initialzing and accessing the MCP. This is the case for EInk, Battery, TouchKeys, Backlight. They all uses `Wire::enter()` and `Wire::leave()` to reserve access to the I2C bus.
### battery, touch_keys (.hpp, .cpp)
Those classes implement basic access to the battery and touch keys state.
### system class renamed InkplatePlatform
This name reflect more what it is. This class will is currently under eavy changes.
### FrameBuffer classes
A hierarchy of frame buffer classes has been added. These allow for flexible adaptation to the different geometry of devices and pixel sizes.
## ESP-IDF configuration specifics for InkPlate devices

1
examples/Basic_Inkplate_Functionality/Inkplate_basic_BW/.gitignore

@ -1,2 +1,3 @@
.pio
.vscode
build

3
examples/Others/Inkplate_Mandelbrot_set/.gitignore

@ -0,0 +1,3 @@
.pio
.vscode
build

3
examples/Others/Inkplate_Mandelbrot_set/CMakeLists.txt

@ -0,0 +1,3 @@
cmake_minimum_required(VERSION 3.16.0)
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
project(Inkplate_basic_BW)

8
examples/Others/Inkplate_Mandelbrot_set/partitions.csv

@ -0,0 +1,8 @@
# ESP-IDF Partition Table
# Name, Type, SubType, Offset, Size, Flags
nvs, data, nvs, 0x9000, 0x4000,
otadata, data, ota, 0xd000, 0x2000,
phy_init, data, phy, 0xf000, 0x1000,
factory, 0, 0, 0x10000, 0x150000,
ota_0, 0, ota_0, 0x160000, 0x150000,
ota_1, 0, ota_1, 0x2B0000, 0x150000,
Can't render this file because it has a wrong number of fields in line 2.

39
examples/Others/Inkplate_Mandelbrot_set/platformio.ini

@ -0,0 +1,39 @@
[platformio]
default_envs = inkplate-6
[env:inkplate-6]
build_flags =
-D INKPLATE_6
-D DEBUGGING=0
[env:inkplate-6-debug]
build_flags =
-D INKPLATE_6
-D DEBUGGING=1
[env:inkplate-10]
build_flags =
-D INKPLATE_10
-D DEBUGGING=0
[env:inkplate-10-debug]
build_flags =
-D INKPLATE_10
-D DEBUGGING=1
[env]
platform = espressif32
board = esp-wrover-kit
framework = espidf
monitor_speed = 115200
upload_speed = 230400
monitor_filters = colorize
board_build.f_cpu = 240000000L
board_build.partitions = partitions.csv
lib_deps = https://github.com/turgu1/ESP-IDF-InkPlate.git
build_flags =
-std=gnu++17
-D CONFIG_SPIRAM_CACHE_WORKAROUND
build_unflags =
-std=gnu++11

1145
examples/Others/Inkplate_Mandelbrot_set/sdkconfig

File diff suppressed because it is too large

6
examples/Others/Inkplate_Mandelbrot_set/src/CMakeLists.txt

@ -0,0 +1,6 @@
# This file was automatically generated for projects
# without default 'CMakeLists.txt' file.
FILE(GLOB_RECURSE app_sources ${CMAKE_SOURCE_DIR}/src/*.*)
idf_component_register(SRCS ${app_sources})

110
examples/Others/Inkplate_Mandelbrot_set/src/main.cpp

@ -0,0 +1,110 @@
/*
Inkplate_Mandelbrot_set sketch for e-radionica.com Inkplate devices
Select "Inkplate 6(ESP32)" from Tools -> Board menu.
Don't have "Inkplate 6(ESP32)" option? Follow our tutorial and add it:
https://e-radionica.com/en/blog/add-inkplate-6-to-arduino-ide/
This example renders the mandelbrot set to coordiantes to Inkplate.
Due to the nature of Mandelbrot set, it is quite slow on low powered MCUs, so please be patient :)
Want to learn more about Inkplate? Visit www.inkplate.io
Looking to get support? Write on our forums: http://forum.e-radionica.com/en/
15 July 2020 by e-radionica.com
*/
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "logging.hpp"
#include "inkplate.hpp"
#define MAXITERATIONS 150
static const char * TAG = "Mandelbrot";
Inkplate display(DisplayMode::INKPLATE_1BIT);
// Takes a long time to render, cca 3 minutes
// Explore different positions to draw
// Some interesting ones can be found here http://www.cuug.ab.ca/dewara/mandelbrot/Mandelbrowser.html
double xFrom = -0.7423, xTo = -0.8463;
double yFrom = 0.1092, yTo = 0.2102;
struct complex
{
double re;
double im;
};
void delay(int msec) { vTaskDelay(msec / portTICK_PERIOD_MS); }
void addComplex(struct complex *z, struct complex *c)
{
z->re += c->re;
z->im += c->im;
}
void squareComplex(struct complex *z)
{
double re = z->re;
double im = z->im;
z->re = re * re - im * im;
z->im = 2 * re * im;
}
double modulusComplexSqr(struct complex *z)
{
return z->re * z->re + z->im * z->im;
}
uint8_t colorAt(double x, double y)
{
struct complex z = {0.0, 0.0};
struct complex c = {x, y};
int i;
for (i = 0; i < MAXITERATIONS && modulusComplexSqr(&z) <= 4.0; ++i)
{
squareComplex(&z);
addComplex(&z, &c);
}
return i == MAXITERATIONS;
}
void mandelbrot_task(void * param)
{
display.begin();
display.clearDisplay();
display.display();
for(;;) {
display.clearDisplay();
for (int j = 0; j < display.height(); ++j)
{
for (int i = 0; i < display.width(); ++i)
display.drawPixel(
i, j, colorAt(xFrom + (double)i * (xTo - xFrom) / 800.0, yFrom + (double)j * (yTo - yFrom) / 600.0));
// for whole set:
// display.drawPixel(i, j, colorAt(-2.0 + (3.0 * (double)i / 800.0), -1.0 + 2.0 * (double)j / 600.0));
ESP_LOGI(TAG, "%d", j);
}
display.display();
delay(5000);
}
}
#define STACK_SIZE 10000
extern "C" {
void app_main()
{
TaskHandle_t xHandle = NULL;
xTaskCreate(mandelbrot_task, "mainTask", STACK_SIZE, (void *) 1, tskIDLE_PRIORITY, &xHandle);
configASSERT(xHandle);
}
} // extern "C"

3
examples/Others/Inkplate_Maze_Generator/.gitignore

@ -0,0 +1,3 @@
.pio
.vscode
build

3
examples/Others/Inkplate_Maze_Generator/CMakeLists.txt

@ -0,0 +1,3 @@
cmake_minimum_required(VERSION 3.16.0)
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
project(Inkplate_Maze_Generator)

8
examples/Others/Inkplate_Maze_Generator/partitions.csv

@ -0,0 +1,8 @@
# ESP-IDF Partition Table
# Name, Type, SubType, Offset, Size, Flags
nvs, data, nvs, 0x9000, 0x4000,
otadata, data, ota, 0xd000, 0x2000,
phy_init, data, phy, 0xf000, 0x1000,
factory, 0, 0, 0x10000, 0x150000,
ota_0, 0, ota_0, 0x160000, 0x150000,
ota_1, 0, ota_1, 0x2B0000, 0x150000,
Can't render this file because it has a wrong number of fields in line 2.

39
examples/Others/Inkplate_Maze_Generator/platformio.ini

@ -0,0 +1,39 @@
[platformio]
default_envs = inkplate-6
[env:inkplate-6]
build_flags =
-D INKPLATE_6
-D DEBUGGING=0
[env:inkplate-6-debug]
build_flags =
-D INKPLATE_6
-D DEBUGGING=1
[env:inkplate-10]
build_flags =
-D INKPLATE_10
-D DEBUGGING=0
[env:inkplate-10-debug]
build_flags =
-D INKPLATE_10
-D DEBUGGING=1
[env]
platform = espressif32
board = esp-wrover-kit
framework = espidf
monitor_speed = 115200
upload_speed = 230400
monitor_filters = colorize
board_build.f_cpu = 240000000L
board_build.partitions = partitions.csv
lib_deps = https://github.com/turgu1/ESP-IDF-InkPlate.git
build_flags =
-std=gnu++17
-D CONFIG_SPIRAM_CACHE_WORKAROUND
build_unflags =
-std=gnu++11

1145
examples/Others/Inkplate_Maze_Generator/sdkconfig

File diff suppressed because it is too large

6
examples/Others/Inkplate_Maze_Generator/src/CMakeLists.txt

@ -0,0 +1,6 @@
# This file was automatically generated for projects
# without default 'CMakeLists.txt' file.
FILE(GLOB_RECURSE app_sources ${CMAKE_SOURCE_DIR}/src/*.*)
idf_component_register(SRCS ${app_sources})

182
examples/Others/Inkplate_Maze_Generator/src/main.cpp

@ -0,0 +1,182 @@
/*
Inkplate_Maze_Generator sketch for e-radionica.com Inkplate 6
Select "Inkplate 6(ESP32)" from Tools -> Board menu.
Don't have "Inkplate 6(ESP32)" option? Follow our tutorial and add it:
https://e-radionica.com/en/blog/add-inkplate-6-to-arduino-ide/
This example renders a random maze every time!
You can write on it with a whiteboard marker or a graphite pen to solve it.
Just be sure not to use pernament markers!
Want to learn more about Inkplate? Visit www.inkplate.io
Looking to get support? Write on our forums: http://forum.e-radionica.com/en/
15 July 2020 by e-radionica.com
*/
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "logging.hpp"
#include "inkplate.hpp"
static const char * TAG = "Maze";
// Initialise Inkplate object
Inkplate display(DisplayMode::INKPLATE_1BIT);
// Here we define one cell size
const int cellSize = 10;
// Calculate screen width and height
int w, h;
char * maze;
// Move direction difference array
int dx[] = {-1, 0, 0, 1};
int dy[] = {0, -1, 1, 0};
void delay(int msec) { vTaskDelay(msec / portTICK_PERIOD_MS); }
int random(int a)
{
// ? -> a
// r -> RAND_MAX
long long r = std::rand();
return ((a * r) / RAND_MAX);
}
// Display the maze
void showMaze(const char *maze, int width, int height)
{
for (int y = 0; y < height; ++y)
for (int x = 0; x < width; ++x)
if (maze[x + y * width] == 1)
for (int i = 0; i < 4; ++i)
{
int xx = x + dx[i];
int yy = y + dy[i];
if (0 <= xx && xx < w && 0 <= yy && yy < h && maze[yy * width + xx] == 1)
display.drawLine(3 + x * cellSize + cellSize / 2, 3 + y * cellSize + cellSize / 2,
3 + x * cellSize + cellSize / 2 + (dx[i] * cellSize / 2),
3 + y * cellSize + cellSize / 2 + (dy[i] * cellSize / 2), BLACK);
}
display.display();
}
// Carve the maze starting at x, y.
void carveMaze(char *maze, int width, int height, int x, int y)
{
int x1, y1;
int x2, y2;
int dx, dy;
int dir, count;
dir = random(4);
count = 0;
while (count < 4)
{
dx = 0;
dy = 0;
switch (dir)
{
case 0:
dx = 1;
break;
case 1:
dy = 1;
break;
case 2:
dx = -1;
break;
default:
dy = -1;
break;
}
x1 = x + dx;
y1 = y + dy;
x2 = x1 + dx;
y2 = y1 + dy;
if (x2 > 0 && x2 < width && y2 > 0 && y2 < height && maze[y1 * width + x1] == 1 && maze[y2 * width + x2] == 1)
{
maze[y1 * width + x1] = 0;
maze[y2 * width + x2] = 0;
x = x2;
y = y2;
dir = random(4);
count = 0;
}
else
{
dir = (dir + 1) % 4;
count += 1;
}
}
}
// Generate maze in matrix maze with size width, height.
void generateMaze(char *maze, int width, int height)
{
int x, y;
// Initialize the maze.
for (x = 0; x < width * height; x++)
{
maze[x] = 1;
}
maze[1 * width + 1] = 0;
// Seed the random number generator.
srand(time(0));
// Carve the maze.
for (y = 1; y < height; y += 2)
{
for (x = 1; x < width; x += 2)
{
carveMaze(maze, width, height, x, y);
}
}
// Set up the entry and exit.
maze[0 * width + 1] = 0;
maze[(height - 1) * width + (width - 2)] = 0;
}
void maze_task(void * param)
{
// Initialise Inkplate
display.begin();
display.clearDisplay();
w = (display.width() - 10) / cellSize;
h = (display.height() - 10) / cellSize;
maze = new char[w * h];
// Generate and display the maze
generateMaze(maze, w, h);
showMaze(maze, w, h);
for (;;) {
ESP_LOGI(TAG, "Completed...");
delay(5000);
}
}
#define STACK_SIZE 10000
extern "C" {
void app_main()
{
TaskHandle_t xHandle = NULL;
xTaskCreate(maze_task, "mainTask", STACK_SIZE, (void *) 1, tskIDLE_PRIORITY, &xHandle);
configASSERT(xHandle);
}
} // extern "C"

3
examples/Others/Inkplate_Peripheral_Mode/.gitignore

@ -0,0 +1,3 @@
.pio
.vscode
build

3
examples/Others/Inkplate_Peripheral_Mode/CMakeLists.txt

@ -0,0 +1,3 @@
cmake_minimum_required(VERSION 3.16.0)
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
project(Inkplate_basic_BW)

8
examples/Others/Inkplate_Peripheral_Mode/partitions.csv

@ -0,0 +1,8 @@
# ESP-IDF Partition Table
# Name, Type, SubType, Offset, Size, Flags
nvs, data, nvs, 0x9000, 0x4000,
otadata, data, ota, 0xd000, 0x2000,
phy_init, data, phy, 0xf000, 0x1000,
factory, 0, 0, 0x10000, 0x150000,
ota_0, 0, ota_0, 0x160000, 0x150000,
ota_1, 0, ota_1, 0x2B0000, 0x150000,
Can't render this file because it has a wrong number of fields in line 2.

39
examples/Others/Inkplate_Peripheral_Mode/platformio.ini

@ -0,0 +1,39 @@
[platformio]
default_envs = inkplate-6
[env:inkplate-6]
build_flags =
-D INKPLATE_6
-D DEBUGGING=0
[env:inkplate-6-debug]
build_flags =
-D INKPLATE_6
-D DEBUGGING=1
[env:inkplate-10]
build_flags =
-D INKPLATE_10
-D DEBUGGING=0
[env:inkplate-10-debug]
build_flags =
-D INKPLATE_10
-D DEBUGGING=1
[env]
platform = espressif32
board = esp-wrover-kit
framework = espidf
monitor_speed = 115200
upload_speed = 230400
monitor_filters = colorize
board_build.f_cpu = 240000000L
board_build.partitions = partitions.csv
lib_deps = https://github.com/turgu1/ESP-IDF-InkPlate.git
build_flags =
-std=gnu++17
-D CONFIG_SPIRAM_CACHE_WORKAROUND
build_unflags =
-std=gnu++11

1145
examples/Others/Inkplate_Peripheral_Mode/sdkconfig

File diff suppressed because it is too large

6
examples/Others/Inkplate_Peripheral_Mode/src/CMakeLists.txt

@ -0,0 +1,6 @@
# This file was automatically generated for projects
# without default 'CMakeLists.txt' file.
FILE(GLOB_RECURSE app_sources ${CMAKE_SOURCE_DIR}/src/*.*)
idf_component_register(SRCS ${app_sources})

432
examples/Others/Inkplate_Peripheral_Mode/src/main.cpp

@ -0,0 +1,432 @@
/*
Inkplate_Peripheral_Mode sketch for e-radionica.com Inkplate 6
Select "Inkplate 6(ESP32)" from Tools -> Board menu.
Don't have "Inkplate 6(ESP32)" option? Follow our tutorial and add it:
https://e-radionica.com/en/blog/add-inkplate-6-to-arduino-ide/
Using this sketch, you don't have to program and control e-paper using Arduino code.
Instead, you can send UART command (explained in documentation that can be found inside folder of this sketch).
This give you flexibility that you can use this Inkplate 6 on any platform!
Because it uses UART, it's little bit slower and it's not recommended to send bunch of
drawPixel command to draw some image. Instead, load bitmaps and pictures on SD card and load image from SD.
If we missed some function, you can modify this and make yor own.
Also, every Inkplate comes with this peripheral mode right from the factory.
Learn more about Peripheral Mode in this update:
https://www.crowdsupply.com/e-radionica/inkplate-6/updates/successfully-funded-also-third-party-master-controllers-and-partial-updates
UART settings are: 115200 baud, standard parity, ending with "\n\r" (both)
You can send commands via USB port or by directly connecting to ESP32 TX and RX pins.
Don't forget you need to send #L(1)* after each command to show it on the display
(equal to display.display()).
Want to learn more about Inkplate? Visit www.inkplate.io
Looking to get support? Write on our forums: http://forum.e-radionica.com/en/
15 July 2020 by e-radionica.com
*/
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "logging.hpp"
#include <inkplate.hpp>
#include <iostream>
#include <cstdio>
static const char * TAG = "PeripheralMode";
Inkplate display(DisplayMode::INKPLATE_1BIT);
#define BUFFER_SIZE 1000
char commandBuffer[BUFFER_SIZE + 1];
char strTemp[2001];
int hexToChar(char c)
{
if (c >= '0' && c <= '9')
return c - '0';
if (c >= 'A' && c <= 'F')
return c - 'A' + 10;
if (c >= 'a' && c <= 'f')
return c - 'a' + 10;
return -1;
}
void peripheral_task(void * param)
{
display.begin();
memset(commandBuffer, 0, BUFFER_SIZE);
for (;;) {
if (int c = getchar())
{
for (int i = 0; i < (BUFFER_SIZE - 1); i++)
{
commandBuffer[i] = commandBuffer[i + 1];
}
commandBuffer[BUFFER_SIZE - 1] = Serial.read();
}
char *s = NULL;
char *e = NULL;
for (int i = 0; i < BUFFER_SIZE; i++)
{
if (commandBuffer[i] == '#' && s == NULL)
s = &commandBuffer[i];
if (commandBuffer[i] == '*' && e == NULL)
e = &commandBuffer[i];
}
if (s != NULL && e != NULL)
{
if ((e - s) > 0)
{
int x, x1, x2, y, y1, y2, x3, y3, l, c, w, h, r, n, rx, ry, xc, yc;
char b;
char temp[150];
switch (*(s + 1))
{
case '?':
std::cout << "OK" << std::flush;
break;
case '0':
sscanf(s + 3, "%d,%d,%d", &x, &y, &c);
// sprintf(temp, "display.drawPixel(%d, %d, %d)\n\r", x, y, c);
// Serial.print(temp);
display.drawPixel(x, y, c);
break;
case '1':
sscanf(s + 3, "%d,%d,%d,%d,%d", &x1, &y1, &x2, &y2, &c);
// sprintf(temp, "display.drawLine(%d, %d, %d, %d, %d)\n\r", x1, y1, x2, y2, c);
// Serial.print(temp);
display.drawLine(x1, y1, x2, y2, c);
break;
case '2':
sscanf(s + 3, "%d,%d,%d,%d", &x, &y, &l, &c);
// sprintf(temp, "display.drawFastVLine(%d, %d, %d, %d)\n\r", x, y, l, c);
// Serial.print(temp);
display.drawFastVLine(x, y, l, c);
break;
case '3':
sscanf(s + 3, "%d,%d,%d,%d", &x, &y, &l, &c);
// sprintf(temp, "display.drawFastHLine(%d, %d, %d, %d)\n\r", x, y, l, c);
// Serial.print(temp);
display.drawFastHLine(x, y, l, c);
break;
case '4':
sscanf(s + 3, "%d,%d,%d,%d,%d", &x, &y, &w, &h, &c);
// sprintf(temp, "display.drawRect(%d, %d, %d, %d, %d)\n\r", x, y, w, h, c);
// Serial.print(temp);
display.drawRect(x, y, w, h, c);
break;
case '5':
sscanf(s + 3, "%d,%d,%d,%d", &x, &y, &r, &c);
// sprintf(temp, "display.drawCircle(%d, %d, %d, %d)\n\r", x, y, r, c);
// Serial.print(temp);
display.drawCircle(x, y, r, c);
break;
case '6':
sscanf(s + 3, "%d,%d,%d,%d,%d,%d,%d", &x1, &y1, &x2, &y2, &x3, &y3, &c);
// sprintf(temp, "display.drawTriangle(%d, %d, %d, %d, %d, %d, %d)\n\r", x1, y1, x2, y2, x3, y3, c);
// Serial.print(temp);
display.drawTriangle(x1, y1, x2, y2, x3, y3, c);
break;
case '7':
sscanf(s + 3, "%d,%d,%d,%d,%d,%d", &x, &y, &w, &h, &r, &c);
// sprintf(temp, "display.drawRoundRect(%d, %d, %d, %d, %d, %d)\n\r", x, y, w, h, r, c);
// Serial.print(temp);
display.drawRoundRect(x, y, w, h, r, c);
break;
case '8':
sscanf(s + 3, "%d,%d,%d,%d,%d", &x, &y, &w, &h, &c);
// sprintf(temp, "display.fillRect(%d, %d, %d, %d, %d)\n\r", x, y, w, h, c);
// Serial.print(temp);
display.fillRect(x, y, w, h, c);
break;
case '9':
sscanf(s + 3, "%d,%d,%d,%d", &x, &y, &r, &c);
// sprintf(temp, "display.fillCircle(%d, %d, %d, %d)\n\r", x, y, r, c);
// Serial.print(temp);
display.fillCircle(x, y, r, c);
break;
case 'A':
sscanf(s + 3, "%d,%d,%d,%d,%d,%d,%d", &x1, &y1, &x2, &y2, &x3, &y3, &c);
// sprintf(temp, "display.fillTriangle(%d, %d, %d, %d, %d, %d, %d)\n\r", x1, y1, x2, y2, x3, y3, c);
// Serial.print(temp);
display.fillTriangle(x1, y1, x2, y2, x3, y3, c);
break;
case 'B':
sscanf(s + 3, "%d,%d,%d,%d,%d,%d", &x, &y, &w, &h, &r, &c);
// sprintf(temp, "display.fillRoundRect(%d, %d, %d, %d, %d, %d)\n\r", x, y, w, h, r, c);
// Serial.print(temp);
display.fillRoundRect(x, y, w, h, r, c);
break;
case 'C':
sscanf(s + 3, "\"%2000[^\"]\"", strTemp);
n = strlen(strTemp);
for (int i = 0; i < n; i++)
{
strTemp[i] = toupper(strTemp[i]);
}
for (int i = 0; i < n; i += 2)
{
strTemp[i / 2] = (hexToChar(strTemp[i]) << 4) | (hexToChar(strTemp[i + 1]) & 0x0F);
}
strTemp[n / 2] = 0;
// Serial.print("display.print(\"");
// Serial.print(strTemp);
// Serial.println("\");");
display.print(strTemp);
break;
case 'D':
sscanf(s + 3, "%d", &c);
// sprintf(temp, "display.setTextSize(%d)\n", c);
// Serial.print(temp);
display.setTextSize(c);
break;
case 'E':
sscanf(s + 3, "%d,%d", &x, &y);
// sprintf(temp, "display.setCursor(%d, %d)\n", x, y);
// Serial.print(temp);
display.setCursor(x, y);
break;
case 'F':
sscanf(s + 3, "%c", &b);
// sprintf(temp, "display.setTextWrap(%s)\n", b == 'T' ? "True" : "False");
// Serial.print(temp);
if (b == 'T')
display.setTextWrap(true);
if (b == 'F')
display.setTextWrap(false);
break;
case 'G':
sscanf(s + 3, "%d", &c);
c &= 3;
// sprintf(temp, "display.setRotation(%d)\n", c);
// Serial.print(temp);
display.setRotation(c);
break;
case 'H':
sscanf(s + 3, "%d,%d,\"%149[^\"]\"", &x, &y, strTemp);
n = strlen(strTemp);
for (int i = 0; i < n; i++)
{
strTemp[i] = toupper(strTemp[i]);
}
for (int i = 0; i < n; i += 2)
{
strTemp[i / 2] = (hexToChar(strTemp[i]) << 4) | (hexToChar(strTemp[i + 1]) & 0x0F);
}
strTemp[n / 2] = 0;
r = display.sdCardInit();
if (r)
{
r = display.drawBitmapFromFile(strTemp, x, y);
std::cout << "#H(" << r << ")*" << std::endl << std::flush;
// sprintf(temp, "display.drawBitmap(%d, %d, %s)\n", x, y, strTemp);
// Serial.print(temp);
}
else
{
std::cout << "#H(-1)*" << std::endl << std::flush;
}
break;
case 'I':
sscanf(s + 3, "%d", &c);
// sprintf(temp, "display.setDisplayMode(%s)\n", c == 0 ? "INKPLATE_1BIT" : "INKPLATE_3BIT");
// Serial.print(temp);
if (((DisplayMode) c) == DisplayMode::INKPLATE_1BIT)
display.selectDisplayMode(DisplayMode::INKPLATE_1BIT);
if (((DisplayMode) c) == DisplayMode::INKPLATE_3BIT)
display.selectDisplayMode(DisplayMode::INKPLATE_3BIT);
break;
case 'J':
sscanf(s + 3, "%c", &b);
if (b == '?')
{
// if (0 == 0) {
// Serial.println("#J(0)*");
//} else {
// Serial.println("#J(1)*");
//}
if (display.getDisplayMode() == DisplayMode::INKPLATE_1BIT)
{
std::cout << "#J(0)*" << std::endl << std::flush;
}
if (display.getDisplayMode() == DisplayMode::INKPLATE_3BIT)
{
std::cout << "#J(1)*" << std::endl << std::flush;
}
}
break;
case 'K':
sscanf(s + 3, "%c", &b);
if (b == '1')
{
// Serial.print("display.clearDisplay();\n");
display.clearDisplay();
}
break;
case 'L':
sscanf(s + 3, "%c", &b);
if (b == '1')
{
// Serial.print("display.display();\n");
display.display();
}
break;
case 'M':
sscanf(s + 3, "%d,%d,%d", &y1, &x2, &y2);
// sprintf(temp, "display.partialUpdate(%d, %d, %d);\n", y1, x2, y2);
// Serial.print(temp);
display.partialUpdate();
break;
case 'N':
sscanf(s + 3, "%c", &b);
if (b == '?')
{
std::cout << "#N("
<< display.readTemperature(), DEC)
<< ")*"
<< std::endl << std::flush;
}
break;
case 'O':
sscanf(s + 3, "%d", &c);
if (c >= 0 && c <= 2)
{
std::cout << "#O("
<< display.readTouchpad(c)
<< ")*"
<< std::endl << std::flush;
}
break;
case 'P':
sscanf(s + 3, "%c", &b);
if (b == '?')
{
std::cout << "#P("
<< display.readBattery()
<< ")*"
<< std::endl << std::flush;
}
break;
case 'Q':
sscanf(s + 3, "%d", &c);
c &= 1;
// if (c == 0) Serial.print("display.einkOff();\n");
// if (c == 1) Serial.print("display.einkOn();\n");
if (c == 0)
display.einkOff();
if (c == 1)
display.einkOn();
break;
case 'R':
sscanf(s + 3, "%c", &b);
if (b == '?')
{
std::cout << "#R("
<< display.getPanelState()
<< ")*"
<< std::endl << std::flush;
}
break;
case 'S':
sscanf(s + 3, "%d,%d,\"%149[^\"]\"", &x, &y, strTemp);
n = strlen(strTemp);
for (int i = 0; i < n; i++)
{
strTemp[i] = toupper(strTemp[i]);
}
for (int i = 0; i < n; i += 2)
{
strTemp[i / 2] = (hexToChar(strTemp[i]) << 4) | (hexToChar(strTemp[i + 1]) & 0x0F);
}
strTemp[n / 2] = 0;
r = display.sdCardInit();
if (r)
{
r = display.drawImage(strTemp, x, y);
std::cout << "#H("
<< r
<< ")*"
<< std::endl << std::flush;
// sprintf(temp, "display.drawBitmap(%d, %d, %s)\n", x, y, strTemp);
// Serial.print(temp);
}
else
{
std::cout << "#H(-1)*" << std::endl << std::flush;
}
break;
case 'T':
int t;
sscanf(s + 3, "%d,%d,%d,%d,%d,%d", &x1, &y1, &x2, &y2, &c, &t);
// sprintf(temp, "display.drawLine(%d, %d, %d, %d, %d)\n\r", x1, y1, x2, y2, c);
// Serial.print(temp);
display.drawThickLine(x1, y1, x2, y2, c, t);
break;
case 'U':
sscanf(s + 3, "%d,%d,%d,%d,%d", &rx, &ry, &xc, &yc, &c);
// sprintf(temp, "display.drawLine(%d, %d, %d, %d, %d)\n\r", x1, y1, x2, y2, c);
// Serial.print(temp);
display.drawElipse(rx, ry, xc, yc, c);
break;
case 'V':
sscanf(s + 3, "%d,%d,%d,%d,%d", &rx, &ry, &xc, &yc, &c);
// sprintf(temp, "display.drawLine(%d, %d, %d, %d, %d)\n\r", x1, y1, x2, y2, c);
// Serial.print(temp);
display.fillElipse(rx, ry, xc, yc, c);
break;
}
*s = 0;
*e = 0;
}
}
}
}
#define STACK_SIZE 10000
extern "C" {
void app_main()
{
TaskHandle_t xHandle = NULL;
xTaskCreate(peripheral_task, "mainTask", STACK_SIZE, (void *) 1, tskIDLE_PRIORITY, &xHandle);
configASSERT(xHandle);
}
} // extern "C"

3
examples/Others/Inkplate_VariPass_Graphs/.gitignore

@ -0,0 +1,3 @@
.pio
.vscode
build

3
examples/Others/Inkplate_VariPass_Graphs/CMakeLists.txt

@ -0,0 +1,3 @@
cmake_minimum_required(VERSION 3.16.0)
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
project(Inkplate_basic_BW)

8
examples/Others/Inkplate_VariPass_Graphs/partitions.csv

@ -0,0 +1,8 @@
# ESP-IDF Partition Table
# Name, Type, SubType, Offset, Size, Flags
nvs, data, nvs, 0x9000, 0x4000,
otadata, data, ota, 0xd000, 0x2000,
phy_init, data, phy, 0xf000, 0x1000,
factory, 0, 0, 0x10000, 0x150000,
ota_0, 0, ota_0, 0x160000, 0x150000,
ota_1, 0, ota_1, 0x2B0000, 0x150000,
Can't render this file because it has a wrong number of fields in line 2.

39
examples/Others/Inkplate_VariPass_Graphs/platformio.ini

@ -0,0 +1,39 @@
[platformio]
default_envs = inkplate-6
[env:inkplate-6]
build_flags =
-D INKPLATE_6
-D DEBUGGING=0
[env:inkplate-6-debug]
build_flags =
-D INKPLATE_6
-D DEBUGGING=1
[env:inkplate-10]
build_flags =
-D INKPLATE_10
-D DEBUGGING=0
[env:inkplate-10-debug]
build_flags =
-D INKPLATE_10
-D DEBUGGING=1
[env]
platform = espressif32
board = esp-wrover-kit
framework = espidf
monitor_speed = 115200
upload_speed = 230400
monitor_filters = colorize
board_build.f_cpu = 240000000L
board_build.partitions = partitions.csv
lib_deps = https://github.com/turgu1/ESP-IDF-InkPlate.git
build_flags =
-std=gnu++17
-D CONFIG_SPIRAM_CACHE_WORKAROUND
build_unflags =
-std=gnu++11

1145
examples/Others/Inkplate_VariPass_Graphs/sdkconfig

File diff suppressed because it is too large

78
examples/Others/Inkplate_VariPass_Graphs/src/Inkplate_VariPass_Graphs.ino

@ -0,0 +1,78 @@
/*
Inkplate_VariPass_Graphs example for e-radionica Inkplate6
For this example you will need a micro USB cable, Inkplate6, and an available WiFi connection.
Select "Inkplate 6(ESP32)" from Tools -> Board menu.
Don't have "Inkplate 6(ESP32)" option? Follow our tutorial and add it:
https://e-radionica.com/en/blog/add-inkplate-6-to-arduino-ide/
This example will show you how you can use the API on the VariPass website to download and display
a sensor graph on the e-paper display.
VariPass is a website which allows you to host various online "variables" which you can write to
and read from using the VariPass API. This allows you to store sensor logs and later retrieve them
for graphing, analysis, etc.
This example uses an already public variable as an example. The graph is fed every minute with data
from Thorinair's (https://github.com/Thorinair/) geiger counter, so each startup of the Inkplate will
display updated values.
To learn more about VariPass and how you can use it for your own projects, please visit: https://varipass.org/
If you want to easily integrate the read/write functionality in your project, use the official library:
https://github.com/Thorinair/VariPass-for-ESP8266-ESP32
Want to learn more about Inkplate? Visit www.inkplate.io
Looking to get support? Write on our forums: http://forum.e-radionica.com/en/
23 July 2020 by e-radionica.com
*/
#include "Inkplate.h" //Include Inkplate library to the sketch
#include "WiFi.h" //Include library for WiFi
Inkplate display(INKPLATE_1BIT); // Create an object on Inkplate library and also set library into 1 Bit mode (BW)
const char *ssid = "e-radionica.com"; // Your WiFi SSID
const char *password = "croduino"; // Your WiFi password
void setup()
{
display.begin(); // Init Inkplate library (you should call this function ONLY ONCE)
display.clearDisplay(); // Clear frame buffer of display
display.display(); // Put clear image on display
display.print("Connecting to WiFi...");
display.partialUpdate();
// Connect to the WiFi network.
WiFi.mode(WIFI_MODE_STA);
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED)
{
delay(500);
display.print(".");
display.partialUpdate();
}
display.println("\nWiFi OK! Downloading...");
display.partialUpdate();
// Use a HTTP get request to fetch the graph from VariPass.
// The API expects a few parameters in the URL to allow it to work.
// action - Should be set to "sgraph" or "graph" in order to generate a compatible image.
// id - ID of the variable. It is enough to specify just the ID if the variable is public,
// but a "key" parameter should also be specified if not.
// width - Width of the generated graph, here set to half the Inkplate's width.
// height - Height of the generated graph, here set to half the Inkplate's height.
// eink - Should be set to true to generate a BW 1 bit bitmap better suitable for Inkplate.
// For more detailed explanation and more parameters, please visit the docs page: https://varipass.org/docs/
if (!display.drawBitmapFromWeb("https://api.varipass.org/?action=sgraph&id=kbg3eQfA&width=400&height=300&eink=true",
200, 150))
{
display.println("Image open error");
display.partialUpdate();
}
display.partialUpdate();
WiFi.mode(WIFI_OFF);
}
void loop()
{
// Nothing...
}

18
include/graphical/inkplate.hpp

@ -29,14 +29,22 @@ class Inkplate : public Graphics
Inkplate(DisplayMode mode);
void begin(void) { inkplate_platform.setup(); }
void begin() { inkplate_platform.setup(); }
uint8_t readPowerGood();
inline void einkOn() { e_ink.turn_on(); }
inline void einkOff() { e_ink.turn_off(); }
inline bool joinAP(const char * ssid, const char * pass) { return network_client.joinAP(ssid, pass); }
inline void disconnect() { network_client.disconnect(); }
inline PanelState getPanelState() { e_ink.get_panel_state(); }
inline double readBattery() { return battery.read_level(); }
uint8_t readPowerGood() { e_ink.read_power_good(); }
inline void disconnect() { network_client.disconnect(); }
inline bool isConnected() { return network_client.isConnected(); }
inline int _getRotation() { return Graphics::getRotation(); };
inline int _getRotation() { return Graphics::getRotation(); }
inline bool joinAP(const char * ssid, const char * pass) {
return network_client.joinAP(ssid, pass); }
};
#endif

4
src/graphical/graphics.cpp

@ -28,6 +28,10 @@ Graphics::Graphics(int16_t w, int16_t h) :
if ((_partial == nullptr) || (DMemory4Bit == nullptr)) {
ESP_LOGE(TAG, "Unable to allocate PSRAM memory for buffers.");
}
else {
_partial->clear();
DMemory4Bit->clear();
}
};
void Graphics::setRotation(uint8_t x)

Loading…
Cancel
Save