Arduino OLED Dashboard — Progress Bars, Gauges & Live Sensor UI
A dashboard is what people expect when they see an OLED on a robot or weather box: numbers that update, a bar that fills, maybe a needle gauge. You do not need bitmaps for this. Adafruit GFX primitives — lines, rectangles, circles — are enough, and they cost almost no flash compared with animation frames.
Below is the toolkit I reuse: layout grid, progress bar helper, simple gauge, thrashing-free redraw with
millis(), and a full example that fakes two sensors so you can run it with zero extra hardware.
Design rules for 128×64
- 8×8 font: you get 21 characters wide × 8 rows — but leave margins or it looks cramped.
- One job per region. Top title, middle values, bottom bars.
- Do not clear+redraw 1000 times/sec. Update 5–15 Hz unless something changed.
- Prefer integer math on Uno; floats are fine for demos, slower in tight loops.
- Invert sparingly. A filled bar reads clearer than fancy anti-aliasing you cannot afford.
Layout sketch (coordinates)
// 128 x 64 canvas mental map
// y=0..9 title strip
// y=12..38 big numbers / gauge
// y=42..62 bars or status line
Draw the title once into a static layout, or redraw everything each tick — at 10 Hz both are fine.
The expensive part is display() I2C transfer, not a few GFX calls.
Progress bar helper
void drawBar(Adafruit_SSD1306 &d, int x, int y, int w, int h, int value, int maxV) {
if (maxV < 1) maxV = 1;
if (value < 0) value = 0;
if (value > maxV) value = maxV;
d.drawRect(x, y, w, h, SSD1306_WHITE);
int inner = w - 4;
int fill = (int)((long)value * inner / maxV);
if (fill > 0) {
d.fillRect(x + 2, y + 2, fill, h - 4, SSD1306_WHITE);
}
}
Usage: drawBar(display, 0, 50, 128, 10, humidity, 100); — value 0–100 maps to width.
Simple needle gauge (arc-ish with lines)
True arcs are awkward in GFX. For teaching, a semicircle outline plus a needle line is enough:
void drawGauge(Adafruit_SSD1306 &d, int cx, int cy, int r, int value, int maxV) {
// outline
d.drawCircle(cx, cy, r, SSD1306_WHITE);
d.fillCircle(cx, cy, r - 8, SSD1306_BLACK); // hollow look
d.drawCircle(cx, cy, r - 8, SSD1306_WHITE);
// map value to angle 180°..0° (left to right)
float t = (float)value / (float)maxV; // 0..1
float ang = 3.14159265f * (1.0f - t); // pi .. 0
int x2 = cx + (int)((r - 10) * cos(ang));
int y2 = cy - (int)((r - 10) * sin(ang));
d.drawLine(cx, cy, x2, y2, SSD1306_WHITE);
d.fillCircle(cx, cy, 2, SSD1306_WHITE);
}
Include #include <math.h>. On Uno, calling this at 50 Hz with floats is still OK for one gauge.
Avoid flicker
- Build the full frame in the buffer (
clearDisplay→ draw all → onedisplay()). - Never call
display()after every single line. - Gate updates:
if (millis() - last >= 100) { redraw(); last = millis(); } - If only a number changes, you may clear a small rect and redraw that region — optional optimization.
Full dashboard sketch (simulated sensors)
No extra sensors required. It synthesizes “temperature” and “load” so you can study the UI. Swap in
analogRead or DHT code later.
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <math.h>
Adafruit_SSD1306 display(128, 64, &Wire, -1);
void drawBar(int x, int y, int w, int h, int value, int maxV) {
if (maxV < 1) maxV = 1;
value = constrain(value, 0, maxV);
display.drawRect(x, y, w, h, SSD1306_WHITE);
int fill = (int)((long)value * (w - 4) / maxV);
if (fill > 0) display.fillRect(x + 2, y + 2, fill, h - 4, SSD1306_WHITE);
}
void drawGauge(int cx, int cy, int r, int value, int maxV) {
display.drawCircle(cx, cy, r, SSD1306_WHITE);
float t = (float)constrain(value, 0, maxV) / (float)maxV;
float ang = 3.14159265f * (1.0f - t);
int x2 = cx + (int)((r - 6) * cos(ang));
int y2 = cy - (int)((r - 6) * sin(ang));
display.drawLine(cx, cy, x2, y2, SSD1306_WHITE);
display.fillCircle(cx, cy, 2, SSD1306_WHITE);
}
unsigned long lastDraw = 0;
int tempC = 24;
int loadPct = 10;
void setup() {
if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) for (;;);
}
void loop() {
// fake sensor motion
tempC = 20 + (millis() / 500) % 15;
loadPct = 10 + (int)(40 + 40 * sin(millis() / 400.0));
if (millis() - lastDraw < 100) return; // 10 Hz UI
lastDraw = millis();
display.clearDisplay();
// title
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE);
display.setCursor(0, 0);
display.println(F("LAB DASHBOARD"));
display.drawFastHLine(0, 9, 128, SSD1306_WHITE);
// left: temperature number
display.setCursor(0, 14);
display.print(F("Temp"));
display.setTextSize(2);
display.setCursor(0, 26);
display.print(tempC);
display.setTextSize(1);
display.print(F("C"));
// right: gauge for load
drawGauge(96, 36, 22, loadPct, 100);
display.setCursor(82, 14);
display.print(F("Load"));
// bottom bars
display.setCursor(0, 46);
display.print(F("Heat"));
drawBar(32, 46, 96, 8, tempC - 20, 15);
display.setCursor(0, 56);
display.print(F("CPU"));
drawBar(32, 56, 96, 8, loadPct, 100);
display.display();
}
Hooking real sensors
- Potentiometer:
loadPct = map(analogRead(A0), 0, 1023, 0, 100); - Battery divider: reuse the math from battery OLED meter.
- DHT / BME: read every 2 s; keep UI at 10 Hz with last known values.
- Current sensor: average 16 samples like the battery article to calm the bar.
Multi-page dashboards
When one screen is not enough, combine this with the
OLED menu pattern: Main shows live bars;
Select opens a detail page with bigger digits. Keep each page’s draw function separate
(drawPageLive(), drawPageDetail()) so you do not drown in flags.
Performance notes
- Uno + I2C SSD1306 full refresh is typically fine at 10–20 FPS for UI.
- Heavy
sin/cosper frame is OK for one gauge; cache needle endpoints if you add more. - Never allocate
Stringobjects every frame — print withprint(value). - SPI OLEDs refresh faster; UI code stays the same. See SPI vs I2C.
Making it look “designed”
- Consistent left margin (2–4 px).
- One horizontal rule under the title.
- Align bar tracks to the same x.
- Use size-2 font only for the primary number.
- Leave a blank column so text does not collide with the gauge.
If you want decorative icons (battery, wifi, warning), export tiny bitmaps from the animation tool and
drawBitmap them in the title strip — keep them 8×8 or 16×16.
Troubleshooting UI bugs
- Flicker: multiple
display()calls per loop. - Bar always full: maxV wrong or value not constrained.
- Gauge needle inverted: angle mapping flipped — swap the
1.0 - tterm. - Numbers tear: you are printing without clearing old digits — clearDisplay each frame or wipe a rect.
FAQ
Can I animate the bar smoothly?
Yes — approach the target with shown += (target - shown) / 4 each frame for a cheap ease.
Is this better than bitmaps for dashboards?
For numbers and bars, yes. Bitmaps shine for logos and character animation, not for live values.
Will this fit on Uno with a DHT library?
Usually yes — UI code is small. Watch SRAM if you also store animation frames.
Related
Add icons to your dashboard header
Sketch 8×8 status glyphs in the free tool and drop them beside the title text.
Open oledanimationmaker.com →