Chiba Robotics
Published © CC BY

Solar power standalone Cat Grass Garden

Protect your cat's grass from early nibbling. This box runs for about a week on solar-powered standalone systems, ensuring perfect growth.

AdvancedFull instructions providedOver 3 days58
Solar power standalone Cat Grass Garden

Things used in this project

Hardware components

M5Stack Core2 ESP32 IoT Development Kit
M5Stack Core2 ESP32 IoT Development Kit
×1

Software apps and online services

Arduino IDE
Arduino IDE

Story

Read more

Custom parts and enclosures

cad image

I'll upload the STEP files later...

Schematics

schematics board

Mainboard with INA219x2 , BMP280x1, M5stack core2 GoBottom2 Pogo pin and USB-C connectors

Code

Solar power standalone Cat Grass Garden

C/C++
Power monitoring from the solar panel to the battery by INA219
Power monitoring from the battery to the loads (LED, sensors etc...)
PID LED brightness control by the 24h charging and consuming energy
High and low temperature alert by BMP280
Energy loss alert for -36Wh
#include <Arduino.h>
#include <Wire.h>
#include <Adafruit_INA219.h>
#include <WiFi.h>
#include <time.h>
#include <M5Unified.h>
#include <Adafruit_BMP280.h>
#include <Adafruit_NeoPixel.h>
#include <Preferences.h>

// ---------- hardware config ----------
#define LED_PIN 33
#define NUM_LEDS 60
#define TFT_BRIGHT_BLUE strip.Color(0, 180, 255) //word color

const char* ssid = "SSID";
const char* password = "password";
const char* ntpServer = "ntp.nict.jp";
const long gmtOffset_sec = 9 * 3600;
const int daylightOffset_sec = 0;

Adafruit_BMP280 bmp;
Adafruit_INA219 ina1(0x40); // charging 
Adafruit_INA219 ina2(0x45); // consuming
Adafruit_NeoPixel strip(NUM_LEDS, LED_PIN, NEO_GRB + NEO_KHZ800);

Preferences prefs;

// ---------- globals ----------
float dailyEnergy1 = 0.0f, dailyEnergy2 = 0.0f; // today ttl Wh
float previousEnergy1 = 0.0f, previousEnergy2 = 0.0f; // yesterday ttl Wh
float ledDuty = 10.0f;
float integral_term = 0.0f, lastError = 0.0f;

unsigned long lastSensorLog = 0;
unsigned long lastLogTime = 0;
unsigned long lastTimeSync = 0;
unsigned long lastDisplayUpdate = 0;
unsigned long lastSensorUpdate = 0;

const unsigned long SENSOR_INTERVAL = 1000; // 1sec update
int lastDay = -1;

float W1=0, W2=0, temp=25.0f, pres=1013.0f;
float energyBalance = 0.0f;
bool alertsActive = false;
String alertMsg = "";

// modes
enum Mode { NORMAL, DEMO };
Mode currentMode = NORMAL;

enum DisplayMode { DISP_TEXT, DISP_GRAPH, DISP_METER, DISP_NONE };
DisplayMode dispMode = DISP_TEXT;

// ---------- utilities ----------
float sanitizeValue(float newVal, float lastVal, float minVal, float maxVal, const char* label) {
  if (!isfinite(newVal) || newVal < minVal || newVal > maxVal) {
    Serial.printf("Invalid %s reading: %f (keeping last=%f)\n", label, newVal, lastVal);
    return lastVal;
  }
  return newVal;
}

inline float totalGenWh()  { return previousEnergy1 + dailyEnergy1; }
inline float totalLoadWh() { return previousEnergy2 + dailyEnergy2; }
inline float netWh()       { return totalGenWh() - totalLoadWh(); }

// ---------- sensors ----------
void readSensors(float& W1, float& W2, float& temperature, float& pressure) {
  float v1 = ina1.getBusVoltage_V();
  float i1_A = ina1.getCurrent_mA() / 1000.0f;
  float w1 = v1 * i1_A;

  float v2 = ina2.getBusVoltage_V();
  float i2_A = ina2.getCurrent_mA() / 1000.0f;
  float w2 = v2 * i2_A;

  float t = bmp.readTemperature();
  float p = bmp.readPressure() / 100.0f;

  W1 = sanitizeValue(w1, W1, -2000.0f, 2000.0f, "W1");
  W2 = sanitizeValue(w2, W2, -2000.0f, 2000.0f, "W2");
  temperature = sanitizeValue(t, temp, -40.0f, 85.0f, "Temp");
  pressure = sanitizeValue(p, pres, 300.0f, 1100.0f, "Press");
}

// ---------- LED ----------
void setupLED() {
  strip.begin();
  strip.setBrightness(60);
  strip.show();
}

void setRGBAll(uint8_t duty) {
  struct tm tmnow;
  getLocalTime(&tmnow);
  duty = constrain(duty, 0, 50);
  uint8_t level = map(duty, 10, 30, 128, 255);
  uint32_t color;
  if (tmnow.tm_hour >= 8 && tmnow.tm_hour < 24) {
    uint8_t r = level;
    uint8_t g = 0;
    uint8_t b = level/4;
    color = strip.Color(r, g, b);
  } else {
    color = strip.Color(0, 0, 0);
  }

  for (int i = 0; i < NUM_LEDS; ++i) strip.setPixelColor(i, color);
  strip.show();
}

void ledStartupSequence() {
  uint32_t colors[4] = {
    strip.Color(100,0,0), strip.Color(0,100,0), strip.Color(0,0,100), strip.Color(100,100,100)
  };

  for (int c = 0; c < 4; ++c) {
    for (int i=0;i<NUM_LEDS;i++) strip.setPixelColor(i,0);
    strip.show();
    for (int i=0;i<NUM_LEDS;i++) {
      strip.setPixelColor(i, colors[c]);
      strip.show();
      delay(25);
    }
    delay(200);
  }
  setRGBAll((uint8_t)ledDuty);
}

// ---------- Energy update ----------
void updateEnergy(unsigned long elapsed_ms) {
  if (elapsed_ms == 0) return;
  float hours = ((float)elapsed_ms) / 3600000.0f;
  float gen_Wh = W1 * hours;
  float cons_Wh = W2 * hours;
  dailyEnergy1 += gen_Wh;
  dailyEnergy2 += cons_Wh;

  // 24時間相当でバランス
  const float window = 24.0f; // 24時間
  energyBalance = dailyEnergy1 - dailyEnergy2;
  if (energyBalance > 0) energyBalance = min(energyBalance, window * W1);
  else energyBalance = max(energyBalance, -window * W2);
}

// ---------- PID ----------
void updatePID(float W1, float W2) {
  float error_power = W1 - W2;
  float error_debt  = energyBalance;
  const float Kp = 0.02f;
  const float Ki = 0.005f;
  float delta = Kp * error_power + Ki * error_debt;
  ledDuty += delta;
  ledDuty = constrain(ledDuty, 10.0f, 30.0f);
  setRGBAll((uint8_t)ledDuty);
}

// ---------- WiFi / NTP ----------
bool connectWiFiWithTimeout(const char* ssid, const char* password, uint32_t timeoutMs = 10000) {
  M5.Lcd.println("Connecting WiFi...");
  WiFi.begin(ssid, password);
  uint32_t start = millis();
  while (WiFi.status() != WL_CONNECTED && (millis() - start) < timeoutMs) {
    delay(500);
    M5.Lcd.print(".");
  }
  if (WiFi.status() == WL_CONNECTED) {
    M5.Lcd.println("\nWiFi OK");
    return true;
  } else {
    M5.Lcd.println("\nWiFi FAIL (timeout)");
    WiFi.disconnect(true);
    return false;
  }
}

bool syncTimeWithTimeout(uint32_t timeoutMs = 5000, int retries = 3) {
  for (int attempt = 0; attempt < retries; attempt++) {
    M5.Lcd.printf("NTP sync... try %d\n", attempt + 1);
    configTime(9 * 3600, 0, "ntp.nict.jp", "pool.ntp.org");
    uint32_t start = millis();
    struct tm tinfo;
    while ((millis() - start) < timeoutMs) {
      if (getLocalTime(&tinfo)) {
        M5.Lcd.printf("NTP OK: %04d/%02d/%02d %02d:%02d:%02d\n",
                      tinfo.tm_year + 1900, tinfo.tm_mon + 1, tinfo.tm_mday,
                      tinfo.tm_hour, tinfo.tm_min, tinfo.tm_sec);
        return true;
      }
      delay(500);
    }
    M5.Lcd.println("NTP timeout");
  }
  M5.Lcd.println("NTP FAIL (all retries)");
  return false;
}

// ---------- alerts ----------
void checkAlerts(){
  bool tempAlert = (temp < 0 || temp > 35);
  bool energyAlert = (energyBalance < -5.0f);
  alertsActive = tempAlert || energyAlert;

  if(alertsActive){
    if(tempAlert) alertMsg = "Temp out of range!";
    else if(energyAlert) alertMsg = "Energy low!";
  } else {
    alertMsg = "";
  }
}

// ---------- midnight reset ----------
void checkMidnightReset() {
  struct tm t;
  if (!getLocalTime(&t)) return;

  if (t.tm_mday != lastDay) {
    previousEnergy1 += dailyEnergy1;
    previousEnergy2 += dailyEnergy2;
    dailyEnergy1 = 0.0f;
    dailyEnergy2 = 0.0f;
    lastDay = t.tm_mday;
    prefs.putFloat("daily1", dailyEnergy1);
    prefs.putFloat("daily2", dailyEnergy2);
    prefs.putInt("lastDay", lastDay);
  }
}

// ---------- display ----------
void updateDisplayPartial() {
    struct tm t;
    if(!getLocalTime(&t)) return;

    // --- 左上データ ---
    static int lastSec = -1;
    if(t.tm_sec != lastSec){
        lastSec = t.tm_sec;
        M5.Lcd.fillRect(0, 0, 160, 24, BLACK);
        M5.Lcd.setTextColor(TFT_WHITE, BLACK);
        M5.Lcd.setTextSize(2);
        M5.Lcd.setCursor(0, 0);
        char buf[16];
        sprintf(buf, "%02d:%02d:%02d", t.tm_hour, t.tm_min, t.tm_sec);
        M5.Lcd.print(buf);
    }

    // --- W1 / W2 ---
    static float lastW1=-1e6, lastW2=-1e6;
    if(abs(W1-lastW1)>0.01){
        lastW1 = W1;
        M5.Lcd.fillRect(0, 24, 160, 20, BLACK);
        M5.Lcd.setTextColor(TFT_YELLOW, BLACK);
        M5.Lcd.setCursor(0, 24);
        M5.Lcd.printf("W1: %.1f W", -W1);
    }
    if(abs(W2-lastW2)>0.01){
        lastW2 = W2;
        M5.Lcd.fillRect(0, 44, 160, 20, BLACK);
        M5.Lcd.setTextColor(TFT_YELLOW, BLACK);
        M5.Lcd.setCursor(0, 44);
        M5.Lcd.printf("W2: %.1f W", W2);
    }

    // --- 今日の積算 ---
    static float lastDaily1=-1e6, lastDaily2=-1e6;
    if(abs(dailyEnergy1-lastDaily1)>0.01){
        lastDaily1 = dailyEnergy1;
        M5.Lcd.fillRect(0, 64, 160, 20, BLACK);
        M5.Lcd.setTextColor(TFT_ORANGE, BLACK);
        M5.Lcd.setCursor(0, 64);
        M5.Lcd.printf("Wh1: %.1f Wh", -dailyEnergy1);
    }
    if(abs(dailyEnergy2-lastDaily2)>0.01){
        lastDaily2 = dailyEnergy2;
        M5.Lcd.fillRect(0, 84, 160, 20, BLACK);
        M5.Lcd.setTextColor(TFT_ORANGE, BLACK);
        M5.Lcd.setCursor(0, 84);
        M5.Lcd.printf("Wh2: %.1f Wh", dailyEnergy2);
    }

    // --- エネルギーバランス ---
    static float lastBalance=-1e6;
    if(abs(energyBalance-lastBalance)>0.01){
        lastBalance = energyBalance;
        M5.Lcd.fillRect(0, 104, 160, 20, BLACK);
        M5.Lcd.setTextColor(TFT_GREEN, BLACK);
        M5.Lcd.setCursor(0, 104);
        M5.Lcd.printf("Balance: %.1f Wh", energyBalance);
    }

    // --- 温度 / 気圧 ---
    static float lastTemp=-1000, lastPres=-1e6;
    if(abs(temp-lastTemp)>0.1){
        lastTemp = temp;
        M5.Lcd.fillRect(0, 124, 160, 20, BLACK);
        M5.Lcd.setTextColor(TFT_BRIGHT_BLUE, BLACK);
        M5.Lcd.setCursor(0, 124);
        M5.Lcd.printf("Temp: %.1f C", temp);
    }
    if(abs(pres-lastPres)>0.1){
        lastPres = pres;
        M5.Lcd.fillRect(0, 144, 160, 20, BLACK);
        M5.Lcd.setTextColor(TFT_BRIGHT_BLUE, BLACK);
        M5.Lcd.setCursor(0, 144);
        M5.Lcd.printf("Pres: %.1f hPa", pres);
    }

    // --- Alerts (右下) ---
    const int ax = 120, ay = 210;
    const int aw = 200, ah = 24;
    M5.Lcd.fillRect(ax, ay, aw, ah, BLACK);

    if(alertsActive){
        if(temp < 0 || temp > 35){ // 温度Alert
            M5.Lcd.setTextColor(TFT_RED, BLACK);
            M5.Lcd.setCursor(ax, ay);
            M5.Lcd.printf("Temp ALERT!");
        }
        else if(energyBalance < -5.0f){ // エネルギーAlert
            M5.Lcd.setTextColor(TFT_RED, BLACK);
            M5.Lcd.setCursor(ax, ay);
            M5.Lcd.printf("Energy ALERT!");
        }
    }
}


// ---------- setup ----------
void setup() {
  Serial.begin(115200);
  delay(50);

  auto cfg = M5.config();
  M5.begin(cfg);

  prefs.begin("energy", false);
  dailyEnergy1 = prefs.getFloat("daily1", 0.0f);
  dailyEnergy2 = prefs.getFloat("daily2", 0.0f);
  lastDay = prefs.getInt("lastDay", -1);

  setupLED();
  ledStartupSequence();

  Wire.begin(21, 22);

  ina1.begin(&Wire);
  ina2.begin(&Wire);
  bmp.begin(0x76);

  connectWiFiWithTimeout(ssid, password, 10000);
  syncTimeWithTimeout(5000);

  M5.Lcd.fillScreen(BLACK);

  lastSensorLog = millis();
  lastLogTime = millis();
  lastDisplayUpdate = millis();
  lastSensorUpdate = millis();
}

// ---------- loop ----------
void loop() {
  M5.update();
  unsigned long now = millis();
  unsigned long elapsed = now - lastSensorLog;

  if(now - lastSensorUpdate >= SENSOR_INTERVAL){
    readSensors(W1, W2, temp, pres);
    updatePID(W1, W2);
    updateEnergy(elapsed);
    updateDisplayPartial();
    checkAlerts();
    lastSensorUpdate = now;
    lastSensorLog = now;
  }

  if(now - lastLogTime >= 60000){
    prefs.putFloat("daily1", dailyEnergy1);
    prefs.putFloat("daily2", dailyEnergy2);
    lastLogTime = now;
  }

  checkMidnightReset();
}

Credits

Chiba Robotics
1 project • 0 followers

Comments