Hardware components | ||||||
![]() |
| × | 1 | |||
Software apps and online services | ||||||
![]() |
|
-Therefore, I created a cat-proof enclosure and designed a mounting system for the M5stack Core 2 inside the box, constructing a mechanism to grow cat grass using LEDs from the M5stack.
-Furthermore, considering that plants should ideally be grown using sunlight, I installed a solar panel to charge a mobile battery, which then supplies power to the LED and M5stack, balancing the energy budget to preserve the plant's natural independence.
Process
solar panel, battery, USB-C, INA219, BMP280, DCDC converter, PCB, Pogo-pin, LED, M5stack Core2 for AWS
-Modeling by Fusion360
-Frame and brackets made by Prusa MK3S using Kexcelled mad PLA fillament
-Machining Gcode for MDF panels and PCB
-MDF and PCB milled by CNC3018
Unique points
-Pogo pin I2C connect base like official product
-PCB fixing points designed to receive the force from pogo pin and it's support for stable I2C connection
Solar power standalone Cat Grass Garden
C/C++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();
}
Comments