In this tutorial you will learn how to perform a fast partial refresh of an e-Paper display. The main advantages of e-Paper displays are their low power consumption and great readability. Apart from the limited color range their biggest disadvantage is the slow refresh rate.
It typically takes 2-4 seconds for a black-and-white e-Paper to refresh the content of the entire display and for color displays it is much longer (>20 seconds). The complete refresh also comes with a lot of flickering, which is irritating.
Luckily, you can perform a partial refresh, which is not only much faster (0.3 seconds) but also avoids the display flickering. For any practical application that updates the display content more than once every 10 minutes or so, you definitely want to use a partial refresh.
However, implementing a partial refresh for an e-Paper can be quite tricky. This tutorial shows you, how to refresh a single or multiple display areas, how to combine partial refresh with deep sleep and how to partially refresh individual pixels for plotting.
Let’s get started with the required parts.
Required Parts
For the required parts, I listed an older ESP32 lite, which has been deprecated but you can still get it for cheap. There is a successor model with improved specs. But any other ESP32, ESP8266 or Arduino with sufficient memory will work as well.
2.9″ e-Paper Display
ESP32 lite
USB Data Cable
Dupont Wire Set
Breadboard
Makerguides.com is a participant in the Amazon Services LLC Associates Program, an affiliate advertising program designed to provide a means for sites to earn advertising fees by advertising and linking to products on Amazon.com. As an Amazon Associate we earn from qualifying purchases.
E-paper Display
For this project, I am using a 2.9″ e-Paper display module, with 296×128 pixels resolution and an embedded controller with an SPI interface.
Note that most e-Paper display modules with SPI have a little switch or jumper that allows you to switch from 4-wire SPI to 3-wire SPI. We are going to use the default 4-wire SPI here.
The display module runs on 3.3V or 5V, has a very low sleep current of 0.01µA and consumes only about 26.4mW while refreshing/updating its content. That means, you can run an e-Paper display for a long time even on a small battery.
For more information on e-Paper displays have a look at our tutorial Interfacing Arduino To An E-ink Display.
Connecting and Testing the e-Paper
First, let us connect and test the function of the e-Paper. The following picture shows the complete wiring for power and SPI.
And here is the table with all the connections for convenience. Note that you can power the display with 3.3V or 5V but the ESP32-lite has only a 3.3V output and the SPI data lines must be 3.3V!
e-Paper display | ESP32 lite |
---|---|
CS/SS | 5 |
SCL/SCK | 18 |
SDA/DIN/MOSI | 23 |
BUSY | 15 |
RES/RST | 2 |
DC | 0 |
VCC | 3.3V |
GND | G |
Install GxEPD2 library
Before we can draw or write on the e-Paper display we need to install two libraries. The Adafruit_GFX library is a core graphics library that provides a common set of graphics primitives (text, points, lines, circles, etc.). And the GxEPD2 library provides the graphics driver software to control an E-Paper Display via SPI.
Just install the libraries the usual way. After the installation they should appear in the Library Manager as follows.
Test code
Here is some simple test code that shows the text “Makerguides” on the display. Have a look at complete code first, and then we discuss it.
#define ENABLE_GxEPD2_GFX 0 #include "GxEPD2_BW.h" //CS(SS)=5, SCL(SCK)=18, SDA(MOSI)=23, BUSY=15, RES(RST)=2, DC=0 GxEPD2_BW<GxEPD2_290_BS, GxEPD2_290_BS::HEIGHT> epd(GxEPD2_290_BS(5, 0, 2, 15)); void setup() { epd.init(115200, true, 50, false); epd.setRotation(1); epd.setTextColor(GxEPD_BLACK); epd.setTextSize(2); epd.setFullWindow(); epd.fillScreen(GxEPD_WHITE); epd.setCursor(20, 20); epd.print("Makerguides"); epd.display(); epd.hibernate(); } void loop() {}
Let’s break down the code into its components to understand how it works.
Constants and Libraries
The code starts by defining a constant ENABLE_GxEPD2_GFX
as 0. You can set it to 1, which according to the documentation enables the base class GxEPD2_GFX to pass references or pointers to the display instance as parameter. But it uses ~1.2k more code and we don’t need it, so it is set to 0.
#define ENABLE_GxEPD2_GFX 0
Next we include the GxEPD2_BW.h
header file for the black and white (BW) e-Paper display. If you have a 3-color display you would include GxEPD2_3C.h
, or GxEPD2_4C.h
for a 4-color display, and GxEPD2_7C.h
for a 7-color display, instead.
#include "GxEPD2_BW.h"
Display Object
The next line is important. It creates the display object and it depends on the display type or brand. I tried a WeAct and a WaveShare display and the following line works for me for both displays.
GxEPD2_BW<GxEPD2_290_BS, GxEPD2_290_BS::HEIGHT> epd(GxEPD2_290_BS(5, 0, 2, 15));
The Readme for GxEPD2 library lists all the supported displays and you can find the specifics in the header files, e.g. GxEPD2.h.
Setup function
All the real work happens in the setup function. First, we set up the display parameters such as communication speed, rotation, font, text color, and screen fill color. If you see issues with the refresh of the display, you may have to play with the parameters for the init()
function.
void setup() { epd.init(115200, true, 50, false); epd.setRotation(1); epd.setTextColor(GxEPD_BLACK); epd.setTextSize(2); epd.setFullWindow(); epd.fillScreen(GxEPD_WHITE); epd.setCursor(20, 20); epd.print("Makerguides"); epd.display(); epd.hibernate(); }
Then we fill the screen with white, position the cursor, write the text and finally put the display driver into hibernation mode. That powers of the display and puts the display controller into deep-sleep mode (not the ESP32, only the display!).
Loop Function
The loop()
function is empty in this example as the display content is created in the setup()
function and does not need to be updated continuously.
void loop() {}
Upload and Run Code
Now, we are ready to upload and run the code. Select the board you have in the board manager. In my case it is the WEMOS LOLIN32 Lite that was listed in the Required Parts:
Then press upload and after some flickering, your display should show the following text:
Full Refresh
Just to demonstrate how bad a complete refresh of an e-Paper display looks like, we are going to use the following code. It prints the text “msec: ” and next to it the milliseconds since the start of the microcontroller.
#define ENABLE_GxEPD2_GFX 0 #include "GxEPD2_BW.h" GxEPD2_BW<GxEPD2_290_BS, GxEPD2_290_BS::HEIGHT> epd(GxEPD2_290_BS(5, 0, 2, 15)); void setup() { epd.init(115200, true, 50, false); epd.setRotation(1); epd.setTextSize(2); epd.setTextColor(GxEPD_BLACK); } void loop() { epd.fillScreen(GxEPD_WHITE); epd.setCursor(40, 60); epd.print("msec: "); epd.setCursor(150, 60); epd.print(millis()); epd.display(); epd.hibernate(); }
The display content is updated and refreshed in the main loop as fast as we can go. The following short video clip shows how this looks like. As you can see there is a lot of flickering and the refresh of the display takes almost 3 seconds.
Just unusable for any application with frequent content updates.
Partial Refresh of Single Area
Instead of a performing full refresh of the complete display every time we want to updated the millisecond value, we can instead perform a partial refresh for the updates. The following code performs a full refresh once, at the start, and then only refreshes the section of the screen that shows the milliseconds value:
#define ENABLE_GxEPD2_GFX 0 #include "GxEPD2_BW.h" GxEPD2_BW<GxEPD2_290_BS, GxEPD2_290_BS::HEIGHT> epd(GxEPD2_290_BS(5, 0, 2, 15)); void drawFull(const void* pv) { epd.setFullWindow(); epd.setCursor(40, 60); epd.print("msec: "); } void drawPartial(const void* pv) { uint16_t x = 120, y = 50, w = 130, h = 34; epd.setPartialWindow(x, y, w, h); epd.setCursor(x + 30, y + 10); epd.print(millis()); epd.drawRect(x, y, w, h, GxEPD_BLACK); } void setup() { epd.init(115200, true, 50, false); epd.setRotation(1); epd.setTextSize(2); epd.setTextColor(GxEPD_BLACK); epd.drawPaged(drawFull, 0); } void loop() { epd.drawPaged(drawPartial, 0); epd.hibernate(); }
The following video clip shows how the partial refresh looks like. As you can see the flickering is gone and the update of the milliseconds is much faster (< 1 sec).
There is a full refresh when the code starts, which has the usual flickering and slow refresh time, but that happens only once.
Draw paged
There are different ways to implement the complete or partial refresh. Using the drawPaged()
function is the probably the most elegant and easiest one. It takes a drawCallback
function and a parameter vector pv to perform a paged redraw:
void drawPaged(void (*drawCallback)(const void*), const void* pv)
Within the drawCallback
function, we either call setFullWindow()
to perform a complete refresh or setPartialWindow(x, y, w, h)
for a partial refresh.
Using drawPaged()
also has the advantage that it handles drawing operations in a memory-efficient way, which is particularly beneficial for larger displays and when RAM is limited. It draws the content in multiple smaller steps (pages) instead of refreshing the entire display at once.
drawFull function
If you look at the drawFull()
function, you can see that we are calling setFullWindow()
, set the cursor position and then print the static text "msec: "
.
void drawFull(const void* pv) { epd.setFullWindow(); epd.setCursor(40, 60); epd.print("msec: "); }
In the setup function, we initiate the display, set the display properties, and then call drawPaged(drawFull, 0)
to perform the full display refresh.
void setup() { epd.init(115200, true, 50, false); epd.setRotation(1); epd.setTextSize(2); epd.setTextColor(GxEPD_BLACK); epd.drawPaged(drawFull, 0); }
The parameter vector pv is set to 0, since we are not passing on any parameters. But you could provide font, color, text or other information that is relevant to the drawFull
function here.
drawPartial function
The main difference of the drawPartial()
compared to the drawFull()
is that setPartialWindow(x, y, w, h)
is called to restrict the display area to be refreshed.
void drawPartial(const void* pv) { uint16_t x = 120, y = 50, w = 130, h = 34; epd.setPartialWindow(x, y, w, h); epd.setCursor(x + 30, y + 10); epd.print(millis()); epd.drawRect(x, y, w, h, GxEPD_BLACK); }
After that, we position the cursor, print the milliseconds text and draw a rectangle to mark the refresh window.
Finally, the drawPartial()
is called via drawPaged(drawPartial, 0)
in the main loop, which causes a repeated refresh of the displayed milliseconds.
void loop() { epd.drawPaged(drawPartial, 0); epd.hibernate(); }
Note that anything you draw outside of the partial window area will not be displayed. In drawPartial()
we set the cursor to be within the area and the print the millisecond value within the area.
To show the area of the partial refresh we draw a rectangle around it. However, that is not entirely accurate! More about that in the next section.
Alignment of Refresh Window
Due to addressing limitations of e-paper display controllers the coordinates of the refresh window need to be aligned with a multiples of 8. Specifically,
- x and w should be multiple of 8, for display rotation 0 or 2,
- y and h should be multiple of 8, for display rotation 1 or 3,
The setPartialWindow(x, y, w, h)
function allows you to provide arbitrary values but internally aligns it as required. That means that the actual refresh window might be larger than specified.
This is important, because it means that sections of the screen will be updated that you may not have intended. The image below shows the intended refresh area (black rectangle) and the actual refresh area (white filled rectangle).
As you can see, the height of the actual refresh window is larger than that of the specified window. If you design the layout for your content you have to take this into account. Especially, since the drawRect()
function completely fills the background of the actual refresh window with white. That means it will erase content that is outside of the specified window but is inside of the actual refresh window.
If you want to replicate the above experiment, here is the code. It just inverts the background color and text of the full redraw to make the actual refresh window visible.
#define ENABLE_GxEPD2_GFX 0 #include "GxEPD2_BW.h" GxEPD2_BW<GxEPD2_290_BS, GxEPD2_290_BS::HEIGHT> epd(GxEPD2_290_BS(5, 0, 2, 15)); void drawFull(const void* pv) { epd.setFullWindow(); epd.fillScreen(GxEPD_BLACK); epd.setCursor(40, 60); epd.print("msec: "); } void drawPartial(const void* pv) { uint16_t x = 120, y = 50, w = 130, h = 34; epd.setTextColor(GxEPD_BLACK); epd.setPartialWindow(x, y, w, h); epd.setCursor(x + 30, y + 10); epd.print(millis()); epd.drawRect(x, y, w, h, GxEPD_BLACK); } void setup() { epd.init(115200, true, 50, false); epd.setRotation(1); epd.setTextSize(2); epd.setTextColor(GxEPD_WHITE); epd.drawPaged(drawFull, 0); } void loop() { epd.drawPaged(drawPartial, 0); epd.hibernate(); }
Partial Refresh with Deep Sleep
e-Paper displays are often used in battery-powered project, where the microcontroller typically goes into deep-sleep. How to combine the deep-sleep of an ESP32 with the partial refresh of an e-Paper display, is the topic of this section.
The main difficulty is, that we want to perform a complete refresh only at the first start (reset) of the ESP32, but whenever we wake up from deep-sleep we want to perform only a partial refresh. The following code achieves that. Have a quick look first, and then we dive into the details.
#define ENABLE_GxEPD2_GFX 0 #include "GxEPD2_BW.h" GxEPD2_BW<GxEPD2_290_BS, GxEPD2_290_BS::HEIGHT> epd(GxEPD2_290_BS(5, 0, 2, 15)); RTC_DATA_ATTR bool initial = true; void drawFull(const void* pv) { epd.setFullWindow(); epd.setCursor(40, 60); epd.print("msec: "); initial = false; } void drawPartial(const void* pv) { uint16_t x = 120, y = 50, w = 130, h = 34; epd.setPartialWindow(x, y, w, h); epd.setCursor(x + 30, y + 10); epd.print(millis()); epd.setCursor(x + 100, y + 10); epd.print(random(10)); epd.drawRect(x, y, w, h, GxEPD_BLACK); } void setup() { epd.init(115200, initial, 50, false); epd.setRotation(1); epd.setTextSize(2); epd.setTextColor(GxEPD_BLACK); if (initial) epd.drawPaged(drawFull, 0); } void loop() { epd.drawPaged(drawPartial, 0); epd.hibernate(); esp_sleep_enable_timer_wakeup(1000); esp_deep_sleep_start(); }
The code is fundamentally the same as in the example before, with a few important changes.
Firstly, we define a variable initial
that is stored in RTC memory, which means its value is preserved in deep-sleep mode.
RTC_DATA_ATTR bool initial = true;
The initial
variable is set to true
and after the first complete refresh via drawFull()
set to false
. Which means initial=true
after a reset, but false
after deep-sleep.
Next, we need to let the display know that it should not perform a full refresh after deep-sleep. The init(serial_diag_bitrate, initial, reset_duration pulldown_rst_mode)
function has the parameter initial
for that:
epd.init(115200, initial, 50, false);
We are almost done. In the setup
function, we only perform a full redraw if initial==true
. Which means only after a reset but not after waking-up from deep-sleep.
void setup() { ... if (initial) epd.drawPaged(drawFull, 0); }
Finally, in the loop function, we put the ESP32 to sleep, after having performed a partial refresh.
void loop() { epd.drawPaged(drawPartial, 0); epd.hibernate(); esp_sleep_enable_timer_wakeup(1000); esp_deep_sleep_start(); }
According to the documentation for the init() function, you just have to make sure that the display power supply is maintained during deep-sleep. Calling hibernate
, as shown above, is fine. The following video clip shows how this looks like.
Since millis() resets after during deep-sleep and shows a constant value of 151 milliseconds, I also display a random number (0..9) after the msec value to make the refresh visible.
Partial Refresh of Multiple Areas
Sometimes we want to partially refresh multiple areas at different locations and times. For instance, you may want to refresh the display of a clock every second but the display of a temperature measurement only every 5 minutes.
Extending the above code example to partially refresh multiple areas is easy. Have a look at the following complete code that refreshes the displayed millisecond value in two different areas.
#define ENABLE_GxEPD2_GFX 0 #include "GxEPD2_BW.h" GxEPD2_BW<GxEPD2_290_BS, GxEPD2_290_BS::HEIGHT> epd(GxEPD2_290_BS(5, 0, 2, 15)); void drawText(int16_t x, int16_t y, const char *text) { int16_t x1, y1; uint16_t w, h; epd.getTextBounds(text, x, y, &x1, &y1, &w, &h); epd.setPartialWindow(x1, y1, w, h); epd.setCursor(x, y); epd.print(text); epd.drawRect(x1, y1, w, h, GxEPD_BLACK); } void drawFull(const void* pv) { epd.setFullWindow(); epd.setCursor(40, 60); epd.print("msec: "); } void drawPartial1(const void* pv) { char text[16]; sprintf(text, "1: %8d", millis()); drawText(120, 60, text); } void drawPartial2(const void* pv) { char text[16]; sprintf(text, "2: %8d", millis()); drawText(120, 40, text); } void setup() { epd.init(115200, true, 50, false); epd.setRotation(1); epd.setTextColor(GxEPD_BLACK); epd.setTextSize(2); epd.drawPaged(drawFull, 0); } void loop() { epd.drawPaged(drawPartial1, 0); epd.drawPaged(drawPartial2, 0); epd.hibernate(); }
As you can see, we simply define two functions drawPartial1()
and drawPartial2()
that are called in the loop function to perform the partial refresh of two different display areas.
void loop() { epd.drawPaged(drawPartial1, 0); epd.drawPaged(drawPartial2, 0); epd.hibernate(); }
To make things a bit more interesting and convenient, I added a function drawText()
that calculates the bounding box for a given text, sets the refresh window to the same dimensions, prints the text and draws the bounding box:
void drawText(int16_t x, int16_t y, const char *text) { int16_t x1, y1; uint16_t w, h; epd.getTextBounds(text, x, y, &x1, &y1, &w, &h); epd.setPartialWindow(x1, y1, w, h); epd.setCursor(x, y); epd.print(text); epd.drawRect(x1, y1, w, h, GxEPD_BLACK); }
If you use this code make sure that the text is always of the same length. A text that shrinks between updates might leave display artifacts, since the bounding box and refresh area is getting smaller. In the code above, I use the format specifier "%8d"
to make sure that text is of constant length despite changing numbers. The clip below shows the code in action:
Note that the refresh takes now twice as long, since we are performing two independent updates. You cannot use two different refresh windows (setPartialWindow
) in the same draw function. But you could specify a larger window that encompasses both pieces of content you want to update. In this case, you also wouldn’t have to worry about different text lengths too much.
Partial Refresh using Screen Buffer
As a last example, I wanted to implement a simple data plotter, for instance, to monitor temperature over time. The following picture shows how the plotter is going to look like. It has a fixed title “Temperature”, a fixed x-axis, and dynamically changing, simulated temperature values.
Ideally, we would simply plot individual pixels (data points) with a 1-pixel partial refresh window. However, this doesn’t work, since the dimensions of the refresh window are limited to multiples of 8 (see above). If you try this, you will find that the larger refresh window erases some of the previously plotted pixels and parts of the axis.
To get around this problem, we are drawing on a so called “canvas” first. A canvas is essentially a screen buffer that we can update in the background. The partial refresh then takes a small area of the canvas and uses it to update the display.
The following code contains the complete implementation of this little temperature data plotter.
#define ENABLE_GxEPD2_GFX 0 #include "GxEPD2_BW.h" // W, H flipped due to setRotation(1) const int W = GxEPD2_290_BS::HEIGHT; const int H = GxEPD2_290_BS::WIDTH; const uint16_t WHITE = GxEPD_WHITE; const uint16_t BLACK = GxEPD_BLACK; GxEPD2_BW<GxEPD2_290_BS, GxEPD2_290_BS::HEIGHT> epd(GxEPD2_290_BS(5, 0, 2, 15)); GFXcanvas1 canvas(W, H); void initCanvas() { canvas.setTextColor(BLACK); canvas.fillScreen(WHITE); canvas.setTextSize(2); canvas.setCursor(10, 10); canvas.print("Temperature"); for (int i = 0; i < 10; i++) { int x = 10 + i * W / 10, y = H / 2; canvas.setTextSize(1); canvas.setCursor(x - 2, y + 10); canvas.print(i); canvas.drawLine(x, y - 5, x, y + 5, BLACK); } canvas.drawLine(10, H / 2, W - 10, H / 2, BLACK); } void drawCanvas() { epd.drawBitmap(0, 0, canvas.getBuffer(), W, H, WHITE, BLACK); } void drawFull(const void* pv) { epd.setFullWindow(); drawCanvas(); } void drawPartial(const void* pv) { static int16_t x = 10; x += 1; if (x > W - 10) x = 10; int16_t y = (H / 2) + (x / 10.0) * sin(x / 10.0); canvas.writePixel(x, y, BLACK); epd.setPartialWindow(x, y, 1, 1); drawCanvas(); } void setup() { initCanvas(); epd.init(115200, true, 50, false); epd.setRotation(1); epd.drawPaged(drawFull, 0); } void loop() { epd.drawPaged(drawPartial, 0); epd.hibernate(); }
First, we define helper constants for the display dimensions and the fore- and background colors. Note that width and hight are flipped, since we rotate the display (setRotation(1)
) in the setup
function.
// W, H flipped due to setRotation(1) const int W = GxEPD2_290_BS::HEIGHT; const int H = GxEPD2_290_BS::WIDTH; const uint16_t WHITE = GxEPD_WHITE; const uint16_t BLACK = GxEPD_BLACK;
canvas object
The next important bit is, where we create the canvas object with the same dimensions as the display. Since my e-Paper display has only two colors (black, white), we create a canvas with 1-bit depths (GFXcanvas1
).
GFXcanvas1 canvas(W, H);
If your display has more colors or gray levels, you need to create a canvas that matches it. Have a look at the Adafruit GFX Graphics Library documentation for details.
initCanvas and drawCanvas
The initCanvas()
function simply write the title and draws the x-axis onto the canvas but does not display anything. The actual display of the graphics is done by drawCanvas()
, which copies the canvas as a bitmap to the display.
drawFull and drawPartial
The drawFull()
function performs a complete refresh, while the drawPartial()
function performs a partial refresh. During the full refresh we draw the title and axis. During the partial refresh, we draw the individual data points.
The following formula creates the simulated temperature data y
, and x
is the simulated time.
y = (H / 2) + (x / 10.0) * sin(x / 10.0);
The important bit is, where we write the data point (x,y
) as a pixel to the canvas, then specify a refresh window just around that pixel and draw the canvas:
void drawPartial(const void* pv) { ... canvas.writePixel(x, y, BLACK); epd.setPartialWindow(x, y, 1, 1); drawCanvas(); }
Since the partial refresh window is set to (x,y,1,1) we don’t refresh the entire display but only a small area around the pixel (remember the 8-multiplier limitation). However, since this area is pulled from the canvas it contains what already has been drawn and does not erase existing content on the display. The following clip shows the plotter in action:
You can see that the curve crosses the x-axis without erasing it and there is no flickering. While the refresh is not very fast (around 1 sec) it would be fast enough for a temperature plotter.
Note that deep-sleep cannot be easily added to this code. We would have to store the entire canvas in RTC memory, which is usually not big enough (8K) for that. You could store only the data points but then the partial redraw function would be a bit more complex, since you have to redraw the entire curve. Alternatively, there is also the option to store the canvas on SD card or other, larger external memory.
Recommendation
The Waveshare Manual provides the following recommendations when operating an e-Paper display (shortened and rephrased by me):
Full refresh: The e-Paper display will flicker several times during the refresh process (the number of flickers depends on the refresh time), and the flicker is to remove any afterimages to achieve the best display effect.
Partial refresh: In this case the display has no flickering effect during the refresh process. After several partial refreshing operations, a full refresh operation should be performed to remove the residual image. Otherwise the residual image may become permanent.
It is recommended to set the refresh interval of the e-ink screen to at least 180 seconds (except for products that support the partial refresh).
After a refresh operation, it is recommended to power off or hibernate the display. This will prolong the life of the display and reduces power consumption.
When using a three-color e-ink screen, it is recommended to update the display screen at least once every 24 hours.
e-Paper displays are recommended for indoor use and not for outdoor use. If you want to use the display outdoors, place it in a shaded area and completely cover the white glue part of the e-Paper screen’s connection ribbon with 3M tape.
Conclusions
In this tutorial you learned, how to perform partial refreshes of an e-Paper display to avoid flickering and achieve faster refresh times. Specifically, we looked at how to perform a partial refresh for single and multiple areas, in combination with deep-sleep and pixel-wise content updates.
You could easily extend the little plotter example by adding a real temperature sensor such as the BME280 sensor to the project. Have a look at the Weather Station on e-Paper Display tutorial, for more details. Note that you could use separate canvases for temperature, humidity and air pressure, and then switch between canvases to display different data.
If you have any questions, feel free to ask in the comment section. Happy tinkering ; )
Stefan is a professional software developer and researcher. He has worked in robotics, bioinformatics, image/audio processing and education at Siemens, IBM and Google. He specializes in AI and machine learning and has a keen interest in DIY projects involving Arduino and 3D printing.