SSD1306-based OLED Displays come with a 128x32 and a 128x64 pixel resolution and typically use I2C. Displays are monochrome and come in different colors: white, blue, yellow.
SSD1306 is very similar to SH1106, and you can often use the same C++ libraries and ESPHome components for both.
SSD1306 is more capable than its successor SH1106. SH1106 is a newer design tailored to cut cost by removing hardware acceleration for scrolling and animations that is not needed for these types of display in most use cases anyway.
Be aware that typical displays of this kind are physically very small. The 0.96 inch 128x64 display measures 2.4x1.2cm. That’s ok though: you can display a lot of information, and these displays can be perfectly integrated in small and portable devices.
Dual Color
SSD1306-based OLED displays are strictly monochrome despite some vendors claiming to sell dual-color OLEDs.
In reality, these are split-screens with a small yellow stripe at top. The remainder of the display is blue. You cannot freely change the color of a pixel.
Split-color displays can be very useful when you incorporate the hardware design into your screen layout. The upper part could i.e. display special warning messages or a progress bar.
128x64 And 128x32
The two most commonly found OLED resolutions with breakout boards that use the SSD1306 are 128x64 (0.96” and 1.3” sizes) and 128x32 (0.91”).
Software Support
There are many libraries available for SSD1306 OLED displays that are compatible with Arduino as well as ESP microcontrollers.
Interface
The majority of breakout boards use the efficient two-wire I2C interface.
The SSD1306 does support SPI, too, yet due to the small amounts of data that need to be transferred in a small monochrome display, using a high-sped SPI interface with its need for four wires and additional GPIOs does not make much sense.
Caveats
There are a few caveats you should be aware of:
- SPI Or I2C? Even though SSD1306 supports SPI, most breakout boards use the I2C interface to save GPIOs. Make sure you know which interface is supported by your particular display breakout board, and use the library examples for this interface (or adjust the code appropriately).
- I2C Hardware Address: When your display uses I2C, the default I2C hardware address for most breakout boards (regardless of resolution) is 0x3c. If this address does not work, try 0x3d.
- platformio: When you use platformio as your IDE, move all functions above the setup() function as c++ requires forward function declaration: in platformio, functions must have been declared before they can be used (in Arduino IDE they can be located anywhere). Some libraries were originally developed in Arduino IDE and come with example code that places functions after (below) the code that calls them. Here are the very few (but essential) details to convert an .ino source code file to .c so that it compiles correctly in platformio.
Programming
The by far easiest way of using SSD1306 OLED displays is via ESPHome, however this platform is targeting primarily ESP32 microcontrollers.
Alternately, you can use C++ libraries to program the firmware manually.
ESPHome
Using ESPHome is the simplest approach, as ESPHome includes a native SSD1306 OLED Display component that supports most monochrome OLED drivers, including the SSD1306. Here are the supported OLED drivers:
- SSD1306: 128x32, 128x64, 96x16, 72x40, 64x48
- SSD1305: 128x32, 128x64
- SH1107: 128x64, 128x128
- SH1106: 128x32, 128x64, 96x16, 64x48
Here is an example configuration:
# define the I2C pins that you use
# to connect the display to your microcontroller
i2c:
sda: GPIO21
scl: GPIO22
display:
- platform: ssd1306_i2c
model: "SSD1306_128X64" # 0.96/1.3" 128x64 OLED display
address: 0x3C # default address, use 0x3D if default fails
rotation: 180 # rotate content if needed
update_interval: 3000ms # 1s is default (1000ms)
lambda: |-
it.print(0, 0, id(lato400), "Hello World!");
# you need at least one font to output text
font:
- file:
type: gfonts
family: Lato
weight: 400
id: lato400
size: 20
To compile and upload the sample configuration, simply follow these simple steps.
OLED displays are crisp and clear. If texts appear unsharp like in the picture, you may have used a font that was scaled up or down. You get the best results with pixel fonts specifically designed for a given size.
You can now use all the graphics commands supported by the ESPHome Display Component to draw images, shapes, lines, arcs, etc.
C++
For direct programming (using the Arduino Framework), two popular libraries support the SH1106:
Why Adafruit Library?
The US company Adafruit produces and sells electronic components and provides extensive support for it, including state-of-the-art C++ libraries.
Their libraries are popular because of good quality, documentation, and especially modular design: they consist of a hardware specific library for the particular display driver you need, and a hardware neutral core graphics library that provides the drawing commands:
- Specific Driver Library: targets a specific family of display drivers, i.e. SH110x drivers
- Adafruit GFX: core grafics library which is hardware independent and provides the drawing commands.
This way, source code remains compatible to most displays. All you need to do is change the hardware neutral library to target your display.
Why u8g2 Library
The u8g2 library is another de-facto standard: it targets all monochrome displays, including OLED display drivers like SH1106.
With this library, your source code remains compatible to a variety of monochrome displays: all you need to do is use the appropriate display class in your code to initialize your display.
Oli Kraus has created a universal library for color displays, too: ucglib.
Caveats: Using ArduinoIDE Code In platformio
You simply do not know most of the time which IDE was used to program a given library or example code that you want to use. A lot of the code you find in the Internet is many years old. Often, it was created using ArduinoIDE.
ArduinoIDE is a beginners-friendly (but rather limited) C++ editor. In order to simplify its user experience, it lets the user get away with many bad programming habits that a more professional IDE (like platformio) would flag down.
If examples do not compile right in platformio, you have two options:
- Try with ArduinoIDE: the code may compile just fine in ArduinoIDE. If so, it was probably created in ArduinoIDE in the first place, and requires some of the ArduinoIDE leniency.
- Fix it: check the code for signs of ArduinoIDE specifics, and turn the code into clean and solid C++ source code that works fine in other IDEs such as platformio.
These are the most common issues with code that was created in ArduinoIDE and fails to compile in platformio:
.ino
is.cpp
: ArduinoIDE uses the file extension.ino
(as in Arduino) for C++ source code files. The official extension is.cpp
(as in C++). You can simply rename the file extension.- Add
#include <Arduino.h>
: while not always necessary, it enables ArduinoIDE-specific features - Use
setup()
andloop()
: define bothsetup()
andloop()
. If you don’t needloop()
, define it anyway, and leave it empty. - Method Order: make sure you define every method in your code before you call the method. Code written in ArduinoIDE typically moves method definitions to the bottom of the source code. This is only allowed in ArduinoIDE. For use in platformio and other professional IDEs, move the method definitions up so that they are defined when code wants to call them.
Using u8g2 Library
The U8G2 library is a versatile and robust option that supports a wide range of monochrome display drivers.
Selecting the OLED Driver
In the U8G2 C++ example, the display interface and resolution are selected through the class. For a 128x64 SSD1306 OLED display connected via I2C, you can use one of the following classes:
U8X8_SSD1306_128X64_NONAME_HW_I2C
: For hardware I2C.U8X8_SSD1306_128X64_NONAME_SW_I2C
: For software I2C (allows custom I2C GPIOs).
Once you’ve configured the appropriate class, the rest of the code is straightforward. I successfully tested this approach on ESP32, ESP32-S2, and ESP32-C3.
Tip: Ensure the I2C GPIOs in your code match your hardware setup; otherwise, the display will remain dark.
C++ Source Code
#include <Arduino.h>
#include <U8x8lib.h>
#ifdef U8X8_HAVE_HW_SPI
#include <SPI.h>
#endif
#ifdef U8X8_HAVE_HW_I2C
#include <Wire.h>
#endif
// use hardware I2C (if you know the hardware i2c GPIOs for your board):
//U8X8_SSD1306_128X64_NONAME_HW_I2C u8x8(U8X8_PIN_NONE);
// use software I2C (and define GPIOs yourself)
// pins below are for ESP32S, adjust as needed for other mc models:
#define SDA 21
#define SCL 22
U8X8_SSD1306_128X64_NONAME_SW_I2C u8x8(SCL, SDA);
void setup(void)
{
u8x8.begin();
u8x8.setFlipMode(1);
}
void pre(void)
{
u8x8.setFont(u8x8_font_amstrad_cpc_extended_f);
u8x8.clear();
u8x8.inverse();
u8x8.print(" U8x8 Library ");
u8x8.setFont(u8x8_font_chroma48medium8_r);
u8x8.noInverse();
u8x8.setCursor(0,1);
}
void draw_bar(uint8_t c, uint8_t is_inverse)
{
uint8_t r;
u8x8.setInverseFont(is_inverse);
for( r = 0; r < u8x8.getRows(); r++ )
{
u8x8.setCursor(c, r);
u8x8.print(" ");
}
}
void draw_ascii_row(uint8_t r, int start)
{
int a;
uint8_t c;
for( c = 0; c < u8x8.getCols(); c++ )
{
u8x8.setCursor(c,r);
a = start + c;
if ( a <= 255 )
u8x8.write(a);
}
}
void loop(void)
{
int i;
uint8_t c, r, d;
pre();
u8x8.print("github.com/");
u8x8.setCursor(0,2);
u8x8.print("olikraus/u8g2");
delay(2000);
u8x8.setCursor(0,3);
u8x8.print("Tile size:");
u8x8.print((int)u8x8.getCols());
u8x8.print("x");
u8x8.print((int)u8x8.getRows());
delay(2000);
pre();
for( i = 19; i > 0; i-- )
{
u8x8.setCursor(3,2);
u8x8.print(i);
u8x8.print(" ");
delay(150);
}
draw_bar(0, 1);
for( c = 1; c < u8x8.getCols(); c++ )
{
draw_bar(c, 1);
draw_bar(c-1, 0);
delay(50);
}
draw_bar(u8x8.getCols()-1, 0);
pre();
u8x8.setFont(u8x8_font_amstrad_cpc_extended_f);
for( d = 0; d < 8; d ++ )
{
for( r = 1; r < u8x8.getRows(); r++ )
{
draw_ascii_row(r, (r-1+d)*u8x8.getCols() + 32);
}
delay(400);
}
draw_bar(u8x8.getCols()-1, 1);
for( c = u8x8.getCols()-1; c > 0; c--)
{
draw_bar(c-1, 1);
draw_bar(c, 0);
delay(50);
}
draw_bar(0, 0);
pre();
u8x8.drawString(0, 2, "Small");
u8x8.draw2x2String(0, 5, "Scale Up");
delay(3000);
pre();
u8x8.drawString(0, 2, "Small");
u8x8.setFont(u8x8_font_px437wyse700b_2x2_r);
u8x8.drawString(0, 5, "2x2 Font");
delay(3000);
pre();
u8x8.drawString(0, 1, "3x6 Font");
u8x8.setFont(u8x8_font_inb33_3x6_n);
for(i = 0; i < 100; i++ )
{
u8x8.setCursor(0, 2);
u8x8.print(i); // Arduino Print function
delay(10);
}
for(i = 0; i < 100; i++ )
{
u8x8.drawString(0, 2, u8x8_u16toa(i, 5)); // U8g2 Build-In functions
delay(10);
}
pre();
u8x8.drawString(0, 2, "Weather");
u8x8.setFont(u8x8_font_open_iconic_weather_4x4);
for(c = 0; c < 6; c++ )
{
u8x8.drawGlyph(0, 4, '@'+c);
delay(300);
}
pre();
u8x8.print("print \\n\n");
delay(500);
u8x8.println("println");
delay(500);
u8x8.println("done");
delay(1500);
pre();
u8x8.fillDisplay();
for( r = 0; r < u8x8.getRows(); r++ )
{
u8x8.clearLine(r);
delay(100);
}
delay(1000);
}
Using Adafruit Library
The Adafruit library comes with easier-to-use graphics commands, thanks to its well-designed Adafruit GFX library.
To use this library, you need the hardware-specific driver library for your SSD1306 driver: Adafruit SSD1306 library.
C++ Example Code using Adafruit GFX
#include <SPI.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#define SCREEN_WIDTH 128 // OLED display width, in pixels
#define SCREEN_HEIGHT 32 // OLED display height, in pixels
// Declaration for an SSD1306 display connected to I2C (SDA, SCL pins)
// The pins for I2C are defined by the Wire-library.
// On an arduino UNO: A4(SDA), A5(SCL)
// On an arduino MEGA 2560: 20(SDA), 21(SCL)
// On an arduino LEONARDO: 2(SDA), 3(SCL), ...
#define OLED_RESET -1 // Reset pin # (or -1 if sharing Arduino reset pin)
#define SCREEN_ADDRESS 0x3C ///< See datasheet for Address; 0x3D for 128x64, 0x3C for 128x32
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
#define NUMFLAKES 10 // Number of snowflakes in the animation example
#define LOGO_HEIGHT 16
#define LOGO_WIDTH 16
static const unsigned char PROGMEM logo_bmp[] =
{ 0b00000000, 0b11000000,
0b00000001, 0b11000000,
0b00000001, 0b11000000,
0b00000011, 0b11100000,
0b11110011, 0b11100000,
0b11111110, 0b11111000,
0b01111110, 0b11111111,
0b00110011, 0b10011111,
0b00011111, 0b11111100,
0b00001101, 0b01110000,
0b00011011, 0b10100000,
0b00111111, 0b11100000,
0b00111111, 0b11110000,
0b01111100, 0b11110000,
0b01110000, 0b01110000,
0b00000000, 0b00110000 };
void testdrawline() {
int16_t i;
display.clearDisplay(); // Clear display buffer
for(i=0; i<display.width(); i+=4) {
display.drawLine(0, 0, i, display.height()-1, SSD1306_WHITE);
display.display(); // Update screen with each newly-drawn line
delay(1);
}
for(i=0; i<display.height(); i+=4) {
display.drawLine(0, 0, display.width()-1, i, SSD1306_WHITE);
display.display();
delay(1);
}
delay(250);
display.clearDisplay();
for(i=0; i<display.width(); i+=4) {
display.drawLine(0, display.height()-1, i, 0, SSD1306_WHITE);
display.display();
delay(1);
}
for(i=display.height()-1; i>=0; i-=4) {
display.drawLine(0, display.height()-1, display.width()-1, i, SSD1306_WHITE);
display.display();
delay(1);
}
delay(250);
display.clearDisplay();
for(i=display.width()-1; i>=0; i-=4) {
display.drawLine(display.width()-1, display.height()-1, i, 0, SSD1306_WHITE);
display.display();
delay(1);
}
for(i=display.height()-1; i>=0; i-=4) {
display.drawLine(display.width()-1, display.height()-1, 0, i, SSD1306_WHITE);
display.display();
delay(1);
}
delay(250);
display.clearDisplay();
for(i=0; i<display.height(); i+=4) {
display.drawLine(display.width()-1, 0, 0, i, SSD1306_WHITE);
display.display();
delay(1);
}
for(i=0; i<display.width(); i+=4) {
display.drawLine(display.width()-1, 0, i, display.height()-1, SSD1306_WHITE);
display.display();
delay(1);
}
delay(2000); // Pause for 2 seconds
}
void testdrawrect(void) {
display.clearDisplay();
for(int16_t i=0; i<display.height()/2; i+=2) {
display.drawRect(i, i, display.width()-2*i, display.height()-2*i, SSD1306_WHITE);
display.display(); // Update screen with each newly-drawn rectangle
delay(1);
}
delay(2000);
}
void testfillrect(void) {
display.clearDisplay();
for(int16_t i=0; i<display.height()/2; i+=3) {
// The INVERSE color is used so rectangles alternate white/black
display.fillRect(i, i, display.width()-i*2, display.height()-i*2, SSD1306_INVERSE);
display.display(); // Update screen with each newly-drawn rectangle
delay(1);
}
delay(2000);
}
void testdrawcircle(void) {
display.clearDisplay();
for(int16_t i=0; i<max(display.width(),display.height())/2; i+=2) {
display.drawCircle(display.width()/2, display.height()/2, i, SSD1306_WHITE);
display.display();
delay(1);
}
delay(2000);
}
void testfillcircle(void) {
display.clearDisplay();
for(int16_t i=max(display.width(),display.height())/2; i>0; i-=3) {
// The INVERSE color is used so circles alternate white/black
display.fillCircle(display.width() / 2, display.height() / 2, i, SSD1306_INVERSE);
display.display(); // Update screen with each newly-drawn circle
delay(1);
}
delay(2000);
}
void testdrawroundrect(void) {
display.clearDisplay();
for(int16_t i=0; i<display.height()/2-2; i+=2) {
display.drawRoundRect(i, i, display.width()-2*i, display.height()-2*i,
display.height()/4, SSD1306_WHITE);
display.display();
delay(1);
}
delay(2000);
}
void testfillroundrect(void) {
display.clearDisplay();
for(int16_t i=0; i<display.height()/2-2; i+=2) {
// The INVERSE color is used so round-rects alternate white/black
display.fillRoundRect(i, i, display.width()-2*i, display.height()-2*i,
display.height()/4, SSD1306_INVERSE);
display.display();
delay(1);
}
delay(2000);
}
void testdrawtriangle(void) {
display.clearDisplay();
for(int16_t i=0; i<max(display.width(),display.height())/2; i+=5) {
display.drawTriangle(
display.width()/2 , display.height()/2-i,
display.width()/2-i, display.height()/2+i,
display.width()/2+i, display.height()/2+i, SSD1306_WHITE);
display.display();
delay(1);
}
delay(2000);
}
void testfilltriangle(void) {
display.clearDisplay();
for(int16_t i=max(display.width(),display.height())/2; i>0; i-=5) {
// The INVERSE color is used so triangles alternate white/black
display.fillTriangle(
display.width()/2 , display.height()/2-i,
display.width()/2-i, display.height()/2+i,
display.width()/2+i, display.height()/2+i, SSD1306_INVERSE);
display.display();
delay(1);
}
delay(2000);
}
void testdrawchar(void) {
display.clearDisplay();
display.setTextSize(1); // Normal 1:1 pixel scale
display.setTextColor(SSD1306_WHITE); // Draw white text
display.setCursor(0, 0); // Start at top-left corner
display.cp437(true); // Use full 256 char 'Code Page 437' font
// Not all the characters will fit on the display. This is normal.
// Library will draw what it can and the rest will be clipped.
for(int16_t i=0; i<256; i++) {
if(i == '\n') display.write(' ');
else display.write(i);
}
display.display();
delay(2000);
}
void testdrawstyles(void) {
display.clearDisplay();
display.setTextSize(1); // Normal 1:1 pixel scale
display.setTextColor(SSD1306_WHITE); // Draw white text
display.setCursor(0,0); // Start at top-left corner
display.println(F("Hello, world!"));
display.setTextColor(SSD1306_BLACK, SSD1306_WHITE); // Draw 'inverse' text
display.println(3.141592);
display.setTextSize(2); // Draw 2X-scale text
display.setTextColor(SSD1306_WHITE);
display.print(F("0x")); display.println(0xDEADBEEF, HEX);
display.display();
delay(2000);
}
void testscrolltext(void) {
display.clearDisplay();
display.setTextSize(2); // Draw 2X-scale text
display.setTextColor(SSD1306_WHITE);
display.setCursor(10, 0);
display.println(F("scroll"));
display.display(); // Show initial text
delay(100);
// Scroll in various directions, pausing in-between:
display.startscrollright(0x00, 0x0F);
delay(2000);
display.stopscroll();
delay(1000);
display.startscrollleft(0x00, 0x0F);
delay(2000);
display.stopscroll();
delay(1000);
display.startscrolldiagright(0x00, 0x07);
delay(2000);
display.startscrolldiagleft(0x00, 0x07);
delay(2000);
display.stopscroll();
delay(1000);
}
void testdrawbitmap(void) {
display.clearDisplay();
display.drawBitmap(
(display.width() - LOGO_WIDTH ) / 2,
(display.height() - LOGO_HEIGHT) / 2,
logo_bmp, LOGO_WIDTH, LOGO_HEIGHT, 1);
display.display();
delay(1000);
}
#define XPOS 0 // Indexes into the 'icons' array in function below
#define YPOS 1
#define DELTAY 2
void testanimate(const uint8_t *bitmap, uint8_t w, uint8_t h) {
int8_t f, icons[NUMFLAKES][3];
// Initialize 'snowflake' positions
for(f=0; f< NUMFLAKES; f++) {
icons[f][XPOS] = random(1 - LOGO_WIDTH, display.width());
icons[f][YPOS] = -LOGO_HEIGHT;
icons[f][DELTAY] = random(1, 6);
Serial.print(F("x: "));
Serial.print(icons[f][XPOS], DEC);
Serial.print(F(" y: "));
Serial.print(icons[f][YPOS], DEC);
Serial.print(F(" dy: "));
Serial.println(icons[f][DELTAY], DEC);
}
for(;;) { // Loop forever...
display.clearDisplay(); // Clear the display buffer
// Draw each snowflake:
for(f=0; f< NUMFLAKES; f++) {
display.drawBitmap(icons[f][XPOS], icons[f][YPOS], bitmap, w, h, SSD1306_WHITE);
}
display.display(); // Show the display buffer on the screen
delay(200); // Pause for 1/10 second
// Then update coordinates of each flake...
for(f=0; f< NUMFLAKES; f++) {
icons[f][YPOS] += icons[f][DELTAY];
// If snowflake is off the bottom of the screen...
if (icons[f][YPOS] >= display.height()) {
// Reinitialize to a random position, just off the top
icons[f][XPOS] = random(1 - LOGO_WIDTH, display.width());
icons[f][YPOS] = -LOGO_HEIGHT;
icons[f][DELTAY] = random(1, 6);
}
}
}
}
void setup() {
Serial.begin(9600);
// SSD1306_SWITCHCAPVCC = generate display voltage from 3.3V internally
if(!display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS)) {
Serial.println(F("SSD1306 allocation failed"));
for(;;); // Don't proceed, loop forever
}
// Show initial display buffer contents on the screen --
// the library initializes this with an Adafruit splash screen.
display.display();
delay(1000); // Pause for 2 seconds
// Clear the buffer
display.clearDisplay();
// Draw a single pixel in white
display.drawPixel(10, 10, SSD1306_WHITE);
// Show the display buffer on the screen. You MUST call display() after
// drawing commands to make them visible on screen!
display.display();
delay(2000);
// display.display() is NOT necessary after every single drawing command,
// unless that's what you want...rather, you can batch up a bunch of
// drawing operations and then update the screen all at once by calling
// display.display(). These examples demonstrate both approaches...
testdrawline(); // Draw many lines
testdrawrect(); // Draw rectangles (outlines)
testfillrect(); // Draw rectangles (filled)
testdrawcircle(); // Draw circles (outlines)
testfillcircle(); // Draw circles (filled)
testdrawroundrect(); // Draw rounded rectangles (outlines)
testfillroundrect(); // Draw rounded rectangles (filled)
testdrawtriangle(); // Draw triangles (outlines)
testfilltriangle(); // Draw triangles (filled)
testdrawchar(); // Draw characters of the default font
testdrawstyles(); // Draw 'stylized' characters
testscrolltext(); // Draw scrolling text
testdrawbitmap(); // Draw a small bitmap image
// Invert and restore display, pausing in-between
display.invertDisplay(true);
delay(1000);
display.invertDisplay(false);
delay(1000);
testanimate(logo_bmp, LOGO_WIDTH, LOGO_HEIGHT); // Animate bitmaps
}
void loop() {
}
This is the platformio.ini I used to compile the source code in platform.io for an ESP32 S2 Mini:
[env:lolin_s2_mini]
platform = espressif32
board = lolin_s2_mini
framework = arduino
lib_deps = adafruit/Adafruit SSD1306@^2.5.10
Data Sheets
Slow Website?
This website is very fast, and pages should appear instantly. If this site is slow for you, then your routing may be messed up, and this issue does not only affect done.land, but potentially a few other websites and downloads as well. Here are simple steps to speed up your Internet experience and fix issues with slow websites and downloads..
Comments
Please do leave comments below. I am using utteran.ce, an open-source and ad-free light-weight commenting system.
Here is how your comments are stored
Whenever you leave a comment, a new github issue is created on your behalf.
-
All comments become trackable issues in the Github Issues section, and I (and you) can follow up on them.
-
There is no third-party provider, no disrupting ads, and everything remains transparent inside github.
Github Users Yes, Spammers No
To keep spammers out and comments attributable, all you do is log in using your (free) github account and grant utteranc.es the permission to submit issues on your behalf.
If you don’t have a github account yet, go get yourself one - it’s free and simple.
If for any reason you do not feel comfortable with letting the commenting system submit issues for you, then visit Github Issues directly, i.e. by clicking the red button Submit Issue at the bottom of each page, and submit your issue manually. You control everything.
Discussions
For chit-chat and quick questions, feel free to visit and participate in Discussions. They work much like classic forums or bulletin boards. Just keep in mind: your valued input isn’t equally well trackable there.
(content created May 05, 2024)