Open Green Energy
Published © CC BY-NC-ND

DIY Solar Power Meter

DIY Solar Power Meter is a low cost ESP32 based handheld device to measure Solar Irradiance, Tilt, and Azimuth

IntermediateFull instructions provided20 hours15
DIY Solar Power Meter

Things used in this project

Hardware components

Seeed Studio XIAO ESP32C3
×1
Current Sensor - INA226
×1
OLED Display
×1
Solar Cell
×1
GY-511 LSM303
×1

Software apps and online services

Arduino IDE
Arduino IDE

Hand tools and fabrication machines

Soldering iron (generic)
Soldering iron (generic)
Hot Plate, 800 W
Hot Plate, 800 W

Story

Read more

Schematics

Schematic Diagram

Solar Cell Datasheet

Code

Calibration Code

Arduino
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <INA226.h>

// ---------------- OLED ----------------
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_RESET    -1
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);

// ---------------- INA226 ----------------
INA226 ina;
static const uint8_t INA_ADDR = 0x44;
static const float SHUNT_OHMS = 0.50;   // R500 shunt

void setup() {
  Wire.begin();

  // OLED init
  if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
    while (1); // OLED failed
  }
  display.clearDisplay();
  display.setTextColor(SSD1306_WHITE);

  // INA226 init
  ina.begin(INA_ADDR);
  ina.configure(INA226_AVERAGES_16,
                INA226_BUS_CONV_TIME_1100US,
                INA226_SHUNT_CONV_TIME_1100US,
                INA226_MODE_SHUNT_CONT);

  ina.calibrate(SHUNT_OHMS, 2.0);  // Max expected current ~2A
}

void loop() {
  float current_A = ina.readShuntCurrent();   // in Amps
  float isc_mA = current_A * 1000.0;           // convert to mA

  display.clearDisplay();

  display.setTextSize(1);
  display.setCursor(0, 0);
  display.println("ISC CALIBRATION");

  display.drawLine(0, 10, 127, 10, SSD1306_WHITE);

  display.setTextSize(2);
  display.setCursor(10, 22);
  display.print(isc_mA, 1);
  display.println(" mA");

  display.setTextSize(1);
  display.setCursor(0, 52);
  display.println("Note value manually");

  display.display();

  delay(1000);  // update every second
}

Full Code

Arduino
/*
 DIY SOLAR POWER METER - Rev0 - 31.01.2026

  Schematic pin mapping:
    Bat_Sense  -> D0
    MOSFET     -> D1
    BUTTON     -> D2
    Temp_Sense -> D3
    SDA        -> D4
    SCL        -> D5  
  
  DISPLAY PAGE :
  
  Page-1 : HOME      (compass needle + battery) + Irradiance + Tilt
  Page-2 : MIN / MAX (heading only) + Irradiance + Min/Max
  Page-3 : TILT/AZ   (heading only) + Tilt + Azimuth

  Sensors Used:
    1) INA226  -> Isc (mA) -> Irradiance (W/m²) using calibration constant
    2) GY-511 (LSM303) -> Tilt (accel) + Azimuth (mag, tilt-compensated)
    3) Battery voltage ADC -> Battery %
  
  

    - Set CAL_K_WM2_PER_mA based on your calibration.
      Example: Isc_STC = 84 mA at 1000 W/m² -> K = 1000/84 = 11.9048
      
*/

#include <Wire.h>
#include <math.h>

#include <Adafruit_Sensor.h>
#include <Adafruit_LSM303_Accel.h>
#include <Adafruit_LSM303DLH_Mag.h>

#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <INA226.h>

// ================= DISPLAY =================
#define OLED_W 128
#define OLED_H 64
#define OLED_RST -1
#define OLED_ADDR 0x3C
static const int TOP_UI_SHIFT_Y = 3;

Adafruit_SSD1306 display(OLED_W, OLED_H, &Wire, OLED_RST);

// ================= PIN DEFINITIONS =================
#define PIN_BAT_SENSE   D0
#define PIN_MOSFET     D1
#define PIN_BUTTON     D2

// ================= INA226 =================
static const uint8_t INA_ADDR = 0x44;
static const float SHUNT_OHMS = 0.50;     // R500
static const float INA_MAX_A  = 0.20;
INA226 ina;

// ================= GY-511 =================
Adafruit_LSM303_Accel accel;
Adafruit_LSM303DLH_Mag mag;

// ================= CALIBRATION =================
// Example: 84 mA → 1000 W/m²
static const float CAL_K_WM2_PER_mA = 1000.0f / 84.0f;
static const float CAL_C_WM2 = 0.0f;

// ================= BATTERY =================
static const float ADC_VREF = 3.30f;
static const float BAT_DIVIDER_RATIO = 2.0f;
static const float BAT_V_MIN = 3.30f;
static const float BAT_V_MAX = 4.15f;

// ================= PAGE CONTROL =================
enum PageId : uint8_t { PAGE_HOME, PAGE_MINMAX, PAGE_TILT_AZ };
static const PageId PAGE_SEQ[3] = { PAGE_HOME, PAGE_MINMAX, PAGE_TILT_AZ };
uint8_t seqIndex = 0;

unsigned long lastBtnMs = 0;
static const unsigned long DEBOUNCE_MS = 120;

// ================= LIVE VALUES =================
float g_isc_mA   = 0;
float g_irr_wm2  = 0;
float g_tilt_deg = 0;
float g_azim_deg = 0;

int g_batt_pct = 0;
int g_irr_min = 9999;
int g_irr_max = 0;

unsigned long lastUpdateMs = 0;
static const unsigned long UPDATE_MS = 250;

// ================= UTILS =================
static float rad2deg(float r) { return r * 57.2958f; }

static int clamp(int v, int lo, int hi) {
  if (v < lo) return lo;
  if (v > hi) return hi;
  return v;
}

// ================= UI HELPERS =================
static void printCentered(int y, const char* txt, uint8_t size) {
  int16_t x1, y1;
  uint16_t w, h;
  display.setTextSize(size);
  display.getTextBounds(txt, 0, y, &x1, &y1, &w, &h);
  display.setCursor((OLED_W - w) / 2, y);
  display.print(txt);
}

static void drawBattery(int pct) {
  display.setTextSize(1);
  const int bw = 22, bh = 10;
  const int bx = OLED_W - bw - 2;
  const int by = 1 + TOP_UI_SHIFT_Y;

  display.drawRect(bx, by, bw, bh, SSD1306_WHITE);
  display.fillRect(bx + bw, by + 2, 3, bh - 4, SSD1306_WHITE);

  int fillW = (bw - 4) * pct / 100;
  display.fillRect(bx + 2, by + 2, fillW, bh - 4, SSD1306_WHITE);

  display.setCursor(bx - 26, TOP_UI_SHIFT_Y);
  display.print(pct);
  display.print("%");
}

static void drawCompassIcon() {
  const int cx = 11;
  const int cy = 6 + TOP_UI_SHIFT_Y;
  display.fillTriangle(cx, cy - 7, cx - 3, cy + 1, cx + 3, cy + 1, SSD1306_WHITE);
  display.drawLine(cx, cy - 7, cx, cy + 5, SSD1306_WHITE);
}

// ================= SENSOR READ =================
static int readBatteryPercent() {
  uint16_t raw = analogRead(PIN_BAT_SENSE);
  float v_adc = (raw / 4095.0f) * ADC_VREF;
  float v_bat = v_adc * BAT_DIVIDER_RATIO;
  int pct = (v_bat - BAT_V_MIN) * 100.0f / (BAT_V_MAX - BAT_V_MIN);
  return clamp(pct, 0, 100);
}

static void readINA() {
  float iA = ina.readShuntCurrent();
  g_isc_mA = iA * 1000.0f;

  g_irr_wm2 = CAL_K_WM2_PER_mA * g_isc_mA + CAL_C_WM2;
  if (g_irr_wm2 < 0) g_irr_wm2 = 0;

  int irr = (int)g_irr_wm2;
  if (irr < g_irr_min) g_irr_min = irr;
  if (irr > g_irr_max) g_irr_max = irr;
}

static void readTiltAz() {
  lsm303AccelData a;
  lsm303MagData m;
  accel.read(&a);
  mag.read(&m);

  float ax = a.x, ay = a.y, az = a.z;

  float magA = sqrt(ax * ax + ay * ay + az * az);
  if (magA < 0.01f) magA = 0.01f;

  g_tilt_deg = rad2deg(acos(az / magA));

  float heading = atan2(m.y, m.x);
  g_azim_deg = rad2deg(heading);
  if (g_azim_deg < 0) g_azim_deg += 360.0f;
}

// ================= PAGES =================
static void drawHome() {
  display.clearDisplay();
  drawCompassIcon();
  drawBattery(g_batt_pct);

  char buf[20];
  sprintf(buf, "%d W/m2", (int)g_irr_wm2);
  printCentered(20, buf, 2);

  display.drawLine(8, 38, OLED_W - 8, 38, SSD1306_WHITE);

  sprintf(buf, "%.1f%c", g_tilt_deg, 247);
  printCentered(48, buf, 1);

  display.display();
}

static void drawMinMax() {
  display.clearDisplay();
  printCentered(2 + TOP_UI_SHIFT_Y, "MIN / MAX", 1);

  char buf[20];
  sprintf(buf, "%d W/m2", (int)g_irr_wm2);
  printCentered(18, buf, 2);

  display.drawLine(8, 34, OLED_W - 8, 34, SSD1306_WHITE);

  display.setTextSize(1);
  display.setCursor(2, 42);
  display.print("Min: ");
  display.print(g_irr_min);

  display.setCursor(64, 42);
  display.print("Max: ");
  display.print(g_irr_max);

  display.display();
}

static void drawTiltAz() {
  display.clearDisplay();
  printCentered(2 + TOP_UI_SHIFT_Y, "TILT / AZIMUTH", 1);

  char buf[20];
  sprintf(buf, "%.1f%c", g_tilt_deg, 247);
  printCentered(20, buf, 2);

  display.drawLine(8, 38, OLED_W - 8, 38, SSD1306_WHITE);

  sprintf(buf, "%.1f%c", g_azim_deg, 247);
  printCentered(44, buf, 2);

  display.display();
}

static void renderPage() {
  if (PAGE_SEQ[seqIndex] == PAGE_HOME) drawHome();
  else if (PAGE_SEQ[seqIndex] == PAGE_MINMAX) drawMinMax();
  else drawTiltAz();
}

// ================= BUTTON =================
static void handleButton() {
  if (digitalRead(PIN_BUTTON) == LOW) {
    if (millis() - lastBtnMs > DEBOUNCE_MS) {
      lastBtnMs = millis();
      while (digitalRead(PIN_BUTTON) == LOW) delay(5);
      seqIndex = (seqIndex + 1) % 3;
      renderPage();
    }
  }
}

// ================= SETUP =================
void setup() {
  pinMode(PIN_BUTTON, INPUT_PULLUP);
  pinMode(PIN_MOSFET, OUTPUT);
  digitalWrite(PIN_MOSFET, LOW);

  Wire.begin();

  display.begin(SSD1306_SWITCHCAPVCC, OLED_ADDR);
  analogReadResolution(12);

  ina.begin(INA_ADDR);
  ina.configure(INA226_AVERAGES_16,
                INA226_BUS_CONV_TIME_1100US,
                INA226_SHUNT_CONV_TIME_1100US,
                INA226_MODE_SHUNT_CONT);
  ina.calibrate(SHUNT_OHMS, INA_MAX_A);

  accel.begin();
  mag.begin();

  renderPage();
}

// ================= LOOP =================
void loop() {
  handleButton();

  if (millis() - lastUpdateMs > UPDATE_MS) {
    lastUpdateMs = millis();
    g_batt_pct = readBatteryPercent();
    readINA();
    readTiltAz();
    renderPage();
  }
}

Credits

Open Green Energy
3 projects • 10 followers
The Green Energy Harvester, loves to make things related to Arduino, Solar Energy, and Crafts from used stuff.

Comments