// simple project using Arduino UNO 128x64 SSD1306 OLED display to show a charging indicator
// this is meant to run on the Seeed XIAO board, but it also runs on the Arduino UNO
// created by upir, 2022
// youtube channel: https://www.youtube.com/upir_upir
// FULL TUTORIAL: https://youtu.be/4GfPQoIRqW8
// Links from the video:
// Seeed XIAO: https://s.click.aliexpress.com/e/_DnOVFHH
// Seeed XIAO expansion board: https://s.click.aliexpress.com/e/_DmZeeQ3
// Seeed XIAO documentation: https://wiki.seeedstudio.com/XIAO_BLE/
// Arduino UNO: https://s.click.aliexpress.com/e/_AXDw1h
// Arduino breadboard prototyping shield: https://s.click.aliexpress.com/e/_ApbCwx
// 128x64 SSD1306 OLED Display: https://s.click.aliexpress.com/e/_DCKdvnh
// Transparent OLED display: https://s.click.aliexpress.com/e/_Dns6eLz
// XBMP online generator: https://xbm.jazzychad.net/
// Related videos:
// Turbo pressure gauge with Arduino and OLED display - https://youtu.be/JXmw1xOlBdk
// Arduino Car Cluster with OLED Display - https://youtu.be/El5SJelwV_0
// Knob over OLED Display - https://youtu.be/SmbcNx7tbX8
// Arduino + OLED = 3D ? - https://youtu.be/kBAcaA7NAlA
#include <Arduino.h>
#include <U8g2lib.h>
#include <Wire.h>
U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, /* reset=*/ U8X8_PIN_NONE); // initalize the display
static const unsigned char bitmap_upir_logo[] PROGMEM = { // upir logo bitmap, created using the XMBP online generator (link above)
0xa8,0xeb,0xa8,0xa2,0xa8,0x69,0x98,0xa8};
// This is the battery bitmap
static const unsigned char battery_bitmap[] PROGMEM = {
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0xfe, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0x03, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0x07, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00,
0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00,
0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7f, 0x00, 0x00,
0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x7f, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x7f, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7f, 0x00, 0x00,
0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x7f, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x7f, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7f, 0x00, 0x00,
0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x7f, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x7f, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7f, 0x00, 0x00,
0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x7f, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x7f, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7f, 0x00, 0x00,
0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x7f, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x7f, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7f, 0x00, 0x00,
0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x7f, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x7f, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00,
0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00,
0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x07, 0x00, 0x00,
0x00, 0x00, 0xfe, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00 };
const int NUM_PARTICLES = 20; // number of particles - more than 20 particles will not fit into Arduino UNO RAM
float particle_x[NUM_PARTICLES]; // particle X position
float particle_y[NUM_PARTICLES]; // particle Y position
int particle_speed_x[NUM_PARTICLES]; // particle X speed
int particle_speed_y[NUM_PARTICLES]; // particle Y speed
float particle_size[NUM_PARTICLES]; // particle size (radius)
int init_spread = 15; // how much could be new particle away from the center
int time_value = 0; // time value to animate some elements and update the charge value slower
int charge_value = 0; // charging value displayed in the middle of the screen
char charge_value_buffer[10]; // charging integer value converted to C style string
unsigned long millis_time; // fps
unsigned long millis_time_last; // fps
int fps; // actual FPS value
char fps_buffer[10]; // buffer for converting FPS to string
// This is the area inside the battery bitmap, in pixels
#define CHARGE_AREA_START_X 20
#define CHARGE_AREA_START_Y 18
#define CHARGE_AREA_WIDTH 83
#define CHARGE_AREA_HEIGHT 28
const uint8_t BATTERY_BOX_START_X = 100;
const uint8_t BATTERY_BOX_START_Y = 2;
const uint8_t BATTERY_BOX_WIDTH = 23;
const uint8_t BATTERY_BOX_HEIGHT = 6;
const uint8_t BATTERY_CHG_OFFSET = 5;
// A variable to test our code.
uint8_t percent = 100;
// This is the battery fill mode
// BATTERY_MODE_SOLID = 1 means the battery bitmap will be filled with
// a solid rectangle according to the battery charge.
// BATTERY_MODE_SOLID = 0 means the battery will be filled with a
// rectangle subdivided in 3. If the battery level is 66% or more, it will
// draw 3 rectangles. If it's 33% or more, it will draw 2 rectangles. It will
// draw 1 rectagle if the battery is less than 33%.
#define BATTERY_MODE_SOLID 1
// This is the main function, and the one you will want to transplant to your
// code. Pass the battery charge percent and it will draw the battery with
// the corresponding charge level.
void showBatteryLevel(uint8_t percent)
{
uint8_t width;
if (percent > 100) {
percent = 100;
}
//u8g2.clearBuffer();
//u8g2.drawXBM(0, 0, 128, 64, battery_bitmap);
if (BATTERY_MODE_SOLID)
{
width = (percent * BATTERY_BOX_WIDTH) / 100;
u8g2.drawBox(BATTERY_BOX_START_X, BATTERY_BOX_START_Y, width, BATTERY_BOX_HEIGHT);
u8g2.drawBox(126,2,2,5);
u8g2.drawFrame(BATTERY_BOX_START_X-2,0,27,10);
}
else
{
uint8_t bars;
if (percent >= 66)
{
// Show three bars
bars = 3;
} else if (percent >= 33)
{
// Show two bars
bars = 2;
} else if (percent > 0)
{
// Show one bar
bars = 1;
} else {
// Show nothing
bars = 0;
}
uint8_t offset = BATTERY_BOX_START_X;
for (uint8_t i = 0; i < bars; i++)
{
u8g2.drawBox(offset, BATTERY_BOX_START_Y, 15, BATTERY_BOX_HEIGHT);
offset += BATTERY_CHG_OFFSET;
}
}
//u8g2.sendBuffer();
}
/**
*@brief
* (BattVoltage-BattMin)/(BattMax - BattMin) * 100 = BatteryPercentage
Since we know the battery percentage(lets say 50%, for example) and
battery bar will be 30 pixels long, we can figure out how many pixels
the battery level should be at 50%.
(batteryPercent*MaxBatteryBar)/100 = 15(battery bars pixels)
(50*30)/100=15 pixels. After the calculation, we can draw the
correct length of the battery bar to the display .
*/
void draw(void)
{
// graphic commands to redraw the complete screen should be placed here
u8g2.setFont(u8g_font_unifont);
//u8g2.setFont(u8g_font_osb21);
//u8g2.drawFrame(99,0,27,10);
u8g2.drawBox(101,2,23,6);
//u8g2.drawBox(126,3,2,4);
}
void setup(void)
{
u8g2.begin(); // initialize u8g2 drawing
u8g2.setFont(u8g2_font_chargen_92_tr); // set U8G font
for (int i=0; i < NUM_PARTICLES; i++)
{ // initialize all the particles
particle_x[i] = random(64-init_spread,64+init_spread); // set the position to the center of the screen +- spread
particle_y[i] = random(32-init_spread,32+init_spread); // set the position to the center of the screen +- spread
particle_speed_x[i] = random(-5, 6) * 3; // set some random speed for X
particle_speed_y[i] = random(-5, 6) * 3; // set some random speed for Y
particle_size[i] = 8; // set a big enough radius
}
}
void loop(void)
{
time_value = time_value+1; // increment the time value
if (time_value % 10 == 0) { // only perform some calculations every Nth frame
// increment the charge value
charge_value = charge_value + 1;
if (charge_value > 100) {
charge_value = 0; // set charge value back to 0
}
sprintf(charge_value_buffer, "%d%%", charge_value); // convert charge value to string with the % symbol
}
u8g2.clearBuffer(); // clear U8G2 drawing buffer
u8g2.setDrawColor(1); // white color
// Some test code to scan all the battery levels
showBatteryLevel(percent);
if (percent)
percent--;
else
percent = 100;
// draw particles --------------
for (int i=0; i < NUM_PARTICLES; i++) { // go over all the particles
particle_x[i] = particle_x[i] + particle_speed_x[i]/10.0; // update the X position based on the speed
particle_y[i] = particle_y[i] + particle_speed_y[i]/10.0; // update the Y position based on the speed
particle_size[i] = particle_size[i] * 0.95; // make the size slightly smaller
u8g2.drawDisc(particle_x[i], particle_y[i], particle_size[i]); // draw the particle
// if the particle is outside the screen or small enough, re-initialize it again
if ((particle_x[i] > 128) || (particle_x[i] < 0) || (particle_y[i] > 64) || (particle_y[i] < 0) || (particle_size[i] < 0.5)) {
particle_x[i] = random(64-init_spread,64+init_spread); // set the position to the center of the screen +- spread
particle_y[i] = random(32-init_spread,32+init_spread); // set the position to the center of the screen +- spread
particle_speed_x[i] = random(-5, 6) * 3; // set some random speed for X
particle_speed_y[i] = random(-5, 6) * 3; // set some random speed for Y
particle_size[i] = 8; // set a big enough radius
}
}
// draw big circle in the middle --------------
byte radius = round(25.0 + sin(time_value / 10.0)*2.0); // radius for the big circle in the middle
u8g2.drawDisc(64, 32, radius); // draw the big circle
// draw upir logo --------------
u8g2.drawXBMP(128-16, 64-4, 16, 4, bitmap_upir_logo); // draw upir logo in the corner of the screen
// draw FPS --------------
u8g2.setFont(u8g2_font_6x10_tf); // set very small font
itoa(fps, fps_buffer, 10); // convert FPS number to string
u8g2.drawStr(0, 10, fps_buffer); // draw the FPS number
// draw big value in the center --------------
u8g2.setDrawColor(0); // black color
u8g2.setFont(u8g2_font_chargen_92_tr); // set U8G font
byte string_width = u8g2.getStrWidth(charge_value_buffer); // calculate the string width
u8g2.drawStr(64 - (string_width / 2), 40, charge_value_buffer); // draw the charge value string in the middle of the screen
u8g2.sendBuffer(); // send U8G2 buffer to the display
// calculate the FPS
millis_time_last = millis_time; // store last millisecond value
millis_time = millis(); // get millisecond value from the start of the program
fps = round(1000.0/ (millis_time*1.0-millis_time_last)); // calculate FPS (frames per second) value
}