Arduino

WiFi Cuckoo clock

This is a Wi-Fi-based “cuckoo” clock 🐦 running on ESP8266 and capable of the following functions:

  1. 🌐 Time synchronization About NTP.
  2. 🖥️ Display time on a TM1637 4-digit LED display.
  3. 🔊 Playing a soft "cuckoo" sound via relay or beeper (PIN1) every full hour.
  4. 🌤️/🌙 Hungarian summer/winter time (CET/CEST) automatic management.

💡 Functions

  1. ⏱️ Retrieve time from NTP (hu.pool.ntp.org) with automatic time zone management (CET-1CEST,M3.5.0/2,M10.5.0/3).
  2. 🕛 Display 12-hour format (1-12) for TM1637 on the display.
  3. 🔯 The colon (":") flashes every second.
  4. 🚀 At first start-up, the clock "catch up" with the current time starting from a standard hour (for example, 6 o'clock).
  5. 🐤 One cuckoo can be heard every entire hour (except for the first catch-up after switching on).
  6. ☀️ Daylight saving time at the beginning: on the last Sunday of March at 03:00, an extra cuckoo indicates that the clock has been set forward.
  7. 🌧️ At the start of winter time: on the last Sunday of October at 02:00, one cuckoo is omitted, adjusting to the time change.

🔧 Hardware

  1. 🧠 ESP8266 (e.g. NodeMCU v2) Software Dependencies
    1. ESP8266WiFi
    2. WiFiUdp
    3. TM1637Display 🧩
    4. time.h
    5. ArduinoOTA 📡

    Installing the TM1637 library from the Arduino IDE Library Manager:

    1. Search for to "TM1637" (usually the TM1637Display version is required).

    ⚙️ Settings

    Change these in the code:

    const char* ssid = "WIFI_SSID";
    const char* password = "WIFI_CREDENTIAL";

    OTA / hálózat:

    1. 🌐 OTA hosztnév: cuckoo
    2. 🔐 OTA jelszó: password

    📡 Over-The-Air (OTA) frissítés

    Firmware can be updated remotely:

    1. Arduino IDE → Devices → Ports → cuckoo (OTA device).
    2. 🗝️ Use the set OTA password.

    🚀 Usage

    1. 🔗 Assemble the hardware (ESP8266 + TM1637 + relay/beep).
    2. 📤 Upload the program using the Arduino IDE.
    3. 💬 Enter your Wi-Fi data (ssid, password).
    4. ⚡ Turn on the ESP8266; connects to Wi-Fi and synchronizes time from NTP.
    5. 🕛 The time is displayed in 12-hour format and the clock cuckoos every full hour.

    🛠️ Development / Modifications

    1. 🎵 The sound pattern or duration can be changed in the sendCuckoo() function.
    2. 🕐 The starting base hour can be modified with the baseHour variable of the handleCuckoo() function.
    3. 📅 The summer/winter time calculation logic (last Sunday of March/October) automatically calculates the appropriate day using temp.tm_mday and mktime().

    The code:

    #include <ESP8266WiFi.h>
    #include <WiFiUdp.h>
    #include <TM1637Display.h>
    #include <time.h>
    #include <ArduinoOTA.h>

    // ------------------------
    // WIFI
    // ------------------------
    const char* ssid = "WIFI_SSID";
    const char* password = "WIFI_CREDENTIAL";

    bool summerCuckooDone = false;
    bool winterCuckooDone = false;
    int lastHourChecked = -1;

    // ------------------------
    // PINOUT
    // ------------------------
    #define CLK D1
    #define DIO D2
    #define PIN1 D5 // cuckoo

    TM1637Display display(CLK, DIO);

    // ------------------------
    // NTP
    // ------------------------
    WiFiUDP udp;
    const char* ntpServer = "hu.pool.ntp.org";
    const int localNtpPort = 123;

    // ------------------------
    // TIME
    // ------------------------
    time_t now;
    tm timeinfo;

    bool showColon = true;
    unsigned long lastUpdate = 0;

    bool initialHandled = false;
    int lastCuckooHour = -1;
    int lastCuckooMinute = -1;
    bool skipNextCuckoo = false;


    // ------------------------
    // CUCKOO
    // ------------------------
    void sendCuckoo(int count) {
    for (int i = 0; i < count; i++) {
    digitalWrite(PIN1, HIGH);
    delay(50);
    digitalWrite(PIN1, LOW);
    delay(1000);
    }
    }

    void handleCuckoo(struct tm *t) {
    int hr = t->tm_hour;
    int min = t->tm_min;

    if (min != lastCuckooMinute) {

    if (!initialHandled) {
    int baseHour = 6;
    int toSend = (hr >= baseHour) ? hr - baseHour : (24 - baseHour + hr);
    for (int i = 0; i < toSend; i++) sendCuckoo(1);
    initialHandled = true;
    }
    else if (hr != lastCuckooHour) {
    if (skipNextCuckoo) {
    skipNextCuckoo = false;
    } else {
    sendCuckoo(1);
    }
    }

    lastCuckooHour = hr;
    lastCuckooMinute = min;
    }
    }

    // ------------------------
    // WIFI CONNECT
    // ------------------------
    void connectWiFi() {
    WiFi.mode(WIFI_STA);
    WiFi.begin(ssid, password);

    while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    }
    }

    // ------------------------
    // NTP TIME SYNC
    // ------------------------
    void syncTime() {
    configTzTime("CET-1CEST,M3.5.0/2,M10.5.0/3", ntpServer);
    delay(1000);
    time(&now);
    }

    // ------------------------
    // OTA SETUP
    // ------------------------
    void setupOTA() {
    ArduinoOTA.setHostname("cuckoo");
    ArduinoOTA.setPassword("password");

    ArduinoOTA.begin();
    }

    void handleDSTCuckoo(struct tm *t) {
    int hr = t->tm_hour;
    int month = t->tm_mon + 1;
    int day = t->tm_mday;

    if (hr == lastHourChecked) return;
    lastHourChecked = hr;

    // ===== SUMMER TIME FIX =====
    if (month == 3 && !summerCuckooDone) {
    tm temp = *t;
    temp.tm_mday = 31;
    mktime(&temp);
    int lastSunday = 31 - temp.tm_wday;

    if (day == lastSunday && hr == 3) {
    sendCuckoo(1);
    summerCuckooDone = true;
    }
    }

    // ===== WINTER TIME FIX =====
    if (month == 10 && !winterCuckooDone) {
    tm temp = *t;
    temp.tm_mday = 31;
    mktime(&temp);
    int lastSunday = 31 - temp.tm_wday;

    if (day == lastSunday && hr == 2) {
    skipNextCuckoo = true;
    winterCuckooDone = true;
    }
    }
    }

    // ------------------------
    // SETUP
    // ------------------------
    void setup() {
    pinMode(PIN1, OUTPUT);
    digitalWrite(PIN1, LOW);

    display.setBrightness(7);

    connectWiFi();
    udp.begin(localNtpPort);
    syncTime();
    initialHandled = false;

    setupOTA();
    }

    // ------------------------
    // LOOP
    // ------------------------
    void loop() {

    ArduinoOTA.handle();

    if (WiFi.status() != WL_CONNECTED) {
    connectWiFi();
    }

    unsigned long nowMs = millis();
    if (nowMs - lastUpdate >= 1000) {
    lastUpdate = nowMs;

    time(&now);
    localtime_r(&now, &timeinfo);

    static int lastDay = -1;
    if (timeinfo.tm_mday != lastDay) {
    lastDay = timeinfo.tm_mday;

    summerCuckooDone = false;
    winterCuckooDone = false;
    lastHourChecked = -1;

    }
    handleDSTCuckoo(&timeinfo);
    handleCuckoo(&timeinfo);

    int hr = timeinfo.tm_hour;
    int min = timeinfo.tm_min;

    int hr12 = hr % 12;
    if (hr12 == 0) hr12 = 12;

    display.showNumberDecEx(
    hr12 * 100 + min,
    showColon ? 0b11100000 : 0,
    true
    );

    showColon = !showColon;
    }
    }

    Alex
    2026-05-08 09:25:16.030804+02

Access card system unlocking

This project is an RFID-based automatic event triggering system that combines an Arduino RFID reader and a Python script.

If an authorized RFID card is detected, the system will automatically run commands on the computer.

⚙️ How does it work?

🧠 Arduino page

  1. Arduino reads RFID cards with an MFRC522 RFID Reader module
  2. Reads the card Your UID
  3. Compares it with a predefined UID
  4. If it matches:
  5. prints: “Jackpot FoR the Winnner”
  6. If it doesn’t match:
  7. Sends the message “Incorrect UID”

🐍 Python page

Python script:

  1. reads Arduino serial port (pyserial)
  2. monitors RFID messages
  3. if it receives the right text:
  4. simulates keystrokes (pyautogui)
  5. runs commands on the system
  6. stops the screensaver (XFCE Screensaver)

🚀 Triggered actions

If the RFID card is correct:

  1. Ctrl key event
  2. Enter text: password
  3. Press Enter
  4. Turn off screen saver

🔌 Hardware

  1. Arduino (any compatible)
  2. MFRC522 RFID Reader
  3. RFID card / tag
  4. USB connection to PC

🧰 Python dependencies


pip install pyserial pyautogui

📦 Main Logic

Arduino:

  1. RFID UID Read
  2. UID Check
  3. Send Serial Message

Python:

  1. Serial port monitoring
  2. String search: "Jackpot FoR the Winnner"
  3. Automatic action trigger

💡 What is it good for?

  1. Door opener / login system
  2. Physical key → digital access
  3. Automatic desktop action trigger
  4. Smart home integration basic

⚠️ Notes

  1. The serial port (/dev/ttyUSB1) per machine may change
  2. active desktop session may be required due to GUI automation
  3. DISPLAY setting is sometimes required on Linux systems

Arduino code:

#include <SPI.h>
#include <MFRC522.h>
constexpr uint8_t RST_PIN = 9; // Configurable, see typical pin layout above
constexpr uint8_t SS_PIN = 10; // Configurable, see typical pin layout above
MFRC522 mfrc522(SS_PIN, RST_PIN); // Create MFRC522 instance
int RXLED = 17;
void setup() {
pinMode(RXLED, OUTPUT);
pinMode(LED_BUILTIN, OUTPUT);
Serial.begin(9600);
//while (!Serial); // Do nothing if no serial port is opened (added for Arduinos based on ATMEGA32U4)
SPI.begin(); // Init SPI bus
mfrc522.PCD_Init(); // Init MFRC522
mfrc522.PCD_DumpVersionToSerial(); // Show details of PCD - MFRC522 Card Reader details
}
void readHex(byte *buffer, byte bufferSize) {
for (byte i = 0; i < bufferSize; i++) {
Serial.print(buffer[i] < 0x10 ? " 0" : " ");
Serial.print(buffer[i], HEX);
}
}
void loop() {
digitalWrite(LED_BUILTIN, HIGH); // turn the LED on
delay(15); // wait for half a second
digitalWrite(LED_BUILTIN, LOW); // turn the LED off
delay(15);
// Look for new cards
if ( ! mfrc522.PICC_IsNewCardPresent()) {
digitalWrite(RXLED, HIGH);
return;
}
// Select one of the cards
if ( ! mfrc522.PICC_ReadCardSerial()) {
return;
}

digitalWrite(RXLED, LOW);
// Serial.print("RFID UID: ");
readHex(mfrc522.uid.uidByte, mfrc522.uid.size);
// Serial.println();
// SET YOUR UID HERE (you should see it in your serial monitor)
if (mfrc522.uid.uidByte[0] == 0x43 &&
mfrc522.uid.uidByte[1] == 0x38 &&
mfrc522.uid.uidByte[2] == 0xDA &&
mfrc522.uid.uidByte[3] == 0x39) {
Serial.println("Jackpot FoR the Winnner");
}
else {
Serial.println("Incorrect UID");
delay(5000);
return;
}
digitalWrite(RXLED, HIGH);

delay(5000);
}

Alex
2026-05-07 11:08:27.361725+02

ESP32-CAM FTP image upload to idokep.hu

A lightweight, stable ESP32-CAM image capture and FTP upload system designed for 24/7 operation, with OTA (over-the-air update) support and optimized camera handling.

🚀 Features

  1. 📸 High-resolution image capture (UXGA / SVGA)
  2. 🌐 FTP upload (passive mode)
  3. 🔁 OTA firmware update support
  4. 🧠 Optimized camera exposure management (automatic warm-up + recording)
  5. ⚡ Stable WiFi operation
  6. 🌙 Optional night inactive mode
  7. 🔒 Memory-safe camera buffer management

🧠 Design goals

  1. Prevent ESP32 freezing and WiFi lag, which the continuous automatic camera exposure may cause recalculation
  2. Maintain OTA update availability during normal operation
  3. Provide reliable FTP data transfer
  4. Support long-term unattended operation

🧰 Hardware

  1. ESP32-CAM (AI Thinker)
  2. OV2640 camera module
  3. Stable 5V power supply (recommended: 1-2A, short cable)
  4. WiFi connection
  5. FTP server with PASV (passive) mode enabled

🔌 Pin assignment (AI Thinker)


#define PWDN_GPIO_NUM 32#define RESET_GPIO_NUM -1#define XCLK_GPIO_NUM 0#define SIOD_GPIO_NUM 26#define SIOC_GPIO_NUM 27#define Y9_GPIO_NUM 35#define Y8_GPIO_NUM 34#define Y7_GPIO_NUM 39#define Y6_GPIO_NUM 36#define Y5_GPIO_NUM 21#define Y4_GPIO_NUM 19#define Y3_GPIO_NUM 18#define Y2_GPIO_NUM 5#define VSYNC_GPIO_NUM 25#define HREF_GPIO_NUM 23#define PCLK_GPIO_NUM 22

⚙️ Configuration

WiFi Setting


const char* ssid = "YOUR_WIFI"; const char* password = "YOUR_PASSWORD"; />

FTP settings


const char* ftp_host = "ftp.example.com";const int ftp_port = 21;const char* ftp_user = "username";const char* ftp_pass = "password";

📸 Camera settings

Default high quality profile:


config.frame_size = FRAMESIZE_UXGA;config.jpeg_quality = 10;config.fb_count = 2;

⚠️ Note: UXGA is a heavy load. The project uses this, but the camera exposure is carefully managed to avoid system slowdown.

🧠 Camera stabilization (warming up → recording)

Continuous automatic exposure and amplification can cause CPU load and WiFi delay.

The solution:

  1. The camera first adjusts automatically over a few frames
  2. Then the settings are recorded for stable operation for

Warm up (auto mode)


s->set_gain_ctrl(s, 1);s->set_exposure_ctrl(s, 1);

Capture (manual mode)


s->set_exposure_ctrl(s, 0);s->set_gain_ctrl(s, 0);

🌙 Night mode

No image creation between 22:00 and 05:00:


bool isNightTime() {struct tm timeinfo;if (!getLocalTime(&timeinfo)) return false;int hour = timeinfo.tm_hour;return (hour >= 22 || hour < 5);}

✅ Tested

  1. ESP32-CAM (AI Thinker)
  2. OV2640 camera
  3. FTP server with PASV support

🧩 Optional enhancements

  1. Timestamped filenames
  2. FTP retry logic
  3. Day / night camera profiles
  4. Watchdog protection

Arduino code:


/*
ESP32-CAM FTP Uploader
---------------------
Author: Pucur
Created: 2026
License: MIT

Description:
ESP32-CAM based image capture and FTP upload system
with OTA update support and optimized camera handling.

This project is free to use, modify, and distribute
under the MIT License.
*/
#include "esp_camera.h"
#include <WiFi.h>
#include <ArduinoOTA.h>
#include "esp_system.h"
#include <time.h>
//#include <WebServer.h>

// ---- WiFi ----
const char* ssid = "WIFI_SSID";
const char* password = "WIFI_PASSWORD";
const char* otaPassword = "OTA_PASSWORD";
bool firstBoot = true;
unsigned long lastShot = 0;
const unsigned long interval = 30000;
volatile bool otaInProgress = false;
static unsigned long lastWiFiTry = 0;


// ---- FTP ----
const char* ftp_host = "ftp.viharvadasz.hu";
const int ftp_port = 21;
const char* ftp_user = "USERNAME";
const char* ftp_pass = "PASSWORD";

WiFiClient ftpCtrl;
WiFiClient ftpData;

// ---- WEB LOG ----
//WebServer server(80);
//String ftpLog = "";
//const size_t MAX_LOG = 6000;

// ---- Camera pinout ----
#define PWDN_GPIO_NUM 32
#define RESET_GPIO_NUM -1
#define XCLK_GPIO_NUM 0
#define SIOD_GPIO_NUM 26
#define SIOC_GPIO_NUM 27
#define Y9_GPIO_NUM 35
#define Y8_GPIO_NUM 34
#define Y7_GPIO_NUM 39
#define Y6_GPIO_NUM 36
#define Y5_GPIO_NUM 21
#define Y4_GPIO_NUM 19
#define Y3_GPIO_NUM 18
#define Y2_GPIO_NUM 5
#define VSYNC_GPIO_NUM 25
#define HREF_GPIO_NUM 23
#define PCLK_GPIO_NUM 22

// ================= LOG HELPER =================
/*
void addLog(const String &msg) {
ftpLog += msg + "\n";
if (ftpLog.length() > MAX_LOG) {
ftpLog.remove(0, ftpLog.length() - MAX_LOG);
}
}
*/
// ================= FTP HELPERS =================

String readFTP() {
String r = "";
unsigned long t = millis();

while (millis() - t < 2000) {
while (ftpCtrl.available()) {
char c = ftpCtrl.read();
r += c;
}
}

/*
if (r.length()) addLog("FTP << " + r);
return r;
*/
return r;
}

void sendFTP(String cmd) {
//addLog("FTP >> " + cmd);
ftpCtrl.print(cmd + "\r\n");
}

bool ftpConnect() {
//addLog("FTP CONNECT");
if (!ftpCtrl.connect(ftp_host, ftp_port)) {
// addLog("FTP CONNECT FAILED");
return false;
}

readFTP();

sendFTP("USER " + String(ftp_user));
readFTP();

sendFTP("PASS " + String(ftp_pass));
readFTP();

return true;
}

bool ftpUpload(uint8_t *data, size_t len) {

sendFTP("TYPE I");
readFTP();

sendFTP("CWD lol2");
readFTP();

sendFTP("PASV");
String resp = readFTP();

int ip[4], p1, p2;

int start = resp.indexOf('(');
int end = resp.indexOf(')');

if (start < 0 || end < 0 || end <= start) {
//addLog("PASV parse error: no brackets");
return false;
}

String pasvData = resp.substring(start + 1, end);

if (sscanf(pasvData.c_str(), "%d,%d,%d,%d,%d,%d",
&ip[0], &ip[1], &ip[2], &ip[3], &p1, &p2) != 6) {
//addLog("PASV parse error: bad numbers");
return false;
}

int dataPort = p1 * 256 + p2;
IPAddress dataIP(ip[0], ip[1], ip[2], ip[3]);

if (!ftpData.connect(dataIP, dataPort)) {
//addLog("DATA CONNECT FAILED");
return false;
}

sendFTP("STOR kep.jpg");

String storResp = readFTP();
if (!storResp.startsWith("150")) {
//addLog("STOR not accepted");
ftpData.stop();
return false;
}

size_t sent = 0;
while (sent < len) {
size_t chunk = ftpData.write(data + sent, len - sent);
if (chunk == 0) break;
sent += chunk;
delay(1);
yield();
}

ftpData.stop();
readFTP();

sendFTP("QUIT");
readFTP();

return true;
}

// ================= CAMERA =================

void warmUpCamera(int frames = 5) {
for (int i = 0; i < frames; i++) {
camera_fb_t * fb = esp_camera_fb_get();
if (fb) esp_camera_fb_return(fb);
delay(200);
}
firstBoot = false;
}

void initCamera() {
camera_config_t config;
config.ledc_channel = LEDC_CHANNEL_0;
config.ledc_timer = LEDC_TIMER_0;
config.pin_d0 = Y2_GPIO_NUM;
config.pin_d1 = Y3_GPIO_NUM;
config.pin_d2 = Y4_GPIO_NUM;
config.pin_d3 = Y5_GPIO_NUM;
config.pin_d4 = Y6_GPIO_NUM;
config.pin_d5 = Y7_GPIO_NUM;
config.pin_d6 = Y8_GPIO_NUM;
config.pin_d7 = Y9_GPIO_NUM;
config.pin_xclk = XCLK_GPIO_NUM;
config.pin_pclk = PCLK_GPIO_NUM;
config.pin_vsync = VSYNC_GPIO_NUM;
config.pin_href = HREF_GPIO_NUM;
config.pin_sscb_sda = SIOD_GPIO_NUM;
config.pin_sscb_scl = SIOC_GPIO_NUM;
config.pin_pwdn = PWDN_GPIO_NUM;
config.pin_reset = RESET_GPIO_NUM;
config.xclk_freq_hz = 20000000;
config.pixel_format = PIXFORMAT_JPEG;

if (psramFound()) {
config.frame_size = FRAMESIZE_UXGA;
config.jpeg_quality = 10;
config.fb_count = 1;
} else {
config.frame_size = FRAMESIZE_SVGA;
config.jpeg_quality = 12;
config.fb_count = 1;
}

if (esp_camera_init(&config) != ESP_OK) {
return;
}

sensor_t *s = esp_camera_sensor_get();
s->set_vflip(s, 1);

if (firstBoot) {
warmUpCamera();
firstBoot = false;
}
}

bool isNightTime() {
struct tm timeinfo;
if (!getLocalTime(&timeinfo)) return false;
int hour = timeinfo.tm_hour;
return (hour >= 22 || hour < 5);
}

// ================= SETUP =================

void setup() {
delay(2000);

WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) delay(500);

ArduinoOTA.setHostname("IDOKEP_CAM");
ArduinoOTA.setPassword(otaPassword);

ArduinoOTA
.onStart([]() {
otaInProgress = true;
Serial.println("OTA START");
})
.onEnd([]() {
otaInProgress = false;
Serial.println("OTA END");
})
.onError([](ota_error_t error) {
Serial.printf("OTA Error: %u\n", error);
otaInProgress = false;
});

ArduinoOTA.begin();

configTime(3600, 3600, "pool.ntp.org", "time.nist.gov");

// ---- WEB LOG ENDPOINT ----
/*
server.on("/log", []() {
server.send(200, "text/plain", ftpLog);
});
server.begin();
*/
unsigned long otaWindowStart = millis();
while (millis() - otaWindowStart < 30000) {
ArduinoOTA.handle();
delay(10);
}

if (!otaInProgress) {
initCamera();
}
}

// ================= LOOP =================

void loop() {
ArduinoOTA.handle();
//server.handleClient();

if (otaInProgress) {
delay(1);
return;
}

if (WiFi.status() != WL_CONNECTED) {
if (millis() - lastWiFiTry > 5000) {
lastWiFiTry = millis();
WiFi.disconnect();
WiFi.begin(ssid, password);
}
}


if (isNightTime()) {
for (int i = 0; i < 60; i++) {
ArduinoOTA.handle();
delay(1000);
}
return;
}

if (millis() - lastShot < interval) return;
lastShot = millis();

camera_fb_t *fb = esp_camera_fb_get();
if (!fb) return;
bool ok = ftpUpload(fb->buf, fb->len);

if (!otaInProgress && ftpConnect()) {
ftpUpload(fb->buf, fb->len);
ftpCtrl.stop();
}

esp_camera_fb_return(fb);
}


Alex
2026-05-07 10:45:49.844301+02

WeatherWizard weather station

A low-power BLE weather station using ESP32-C3 and Raspberry Pi 5.

This project measures temperature, humidity, air pressure, UV intensity and rain level using the following sensors:

  1. BME280 (I²C — temperature / humidity / air pressure)
  2. GUVA-S12SD (UV sensor)
  3. Rain sensor module

The ESP32-C3 transmits data via Bluetooth Low Energy (BLE) to a Raspberry Pi 5, which updates the Home Assistant sensors using the REST API.

🔧 Used hardware

KomponensLeírás
ESP32-C3 SuperMiniBLE + WiFi mikrokontroller
BME280 szenzorHőmérséklet / páratartalom / légnyomás
GUVA-S12SDUV intenzitás érzékelő
Esőérzékelő modulAnalóg esőérzékelő
TP4056 lítium akkumulátor töltőAkkumulátortöltő napelemes támogatással
NapelemAz eszköz tápellátásához
Samsung 18650 3250 mAh akkumulátorAz eszköz akkumulátora


Arduino code:


#include <Wire.h>
#include <Adafruit_Sensor.h>
#include <Adafruit_BME280.h>
#include <BLEDevice.h>
#include <BLEUtils.h>
#include <BLEServer.h>
#include "esp_sleep.h"

// ----- Sensor pins -----
int guvaPin = 0;
int rainPin = 1;
#define BME_SDA 8
#define BME_SCL 9
#define LED_PIN 2

Adafruit_BME280 bme;

// ----- BLE settings -----
#define SERVICE_UUID "9f5b0001-8b6d-4e2b-9a37-8a6f1e4d9a90"
#define CHARACTERISTIC_UUID "9f5b0002-8b6d-4e2b-9a37-8a6f1e4d9a90"

BLECharacteristic *pCharacteristic;
BLEServer* pServer;

// ---- Measure sensors and send data ----
void sendSensorData() {
int guvaVal = analogRead(guvaPin);
float guvaVoltage = guvaVal * (3.3 / 4095.0);

int rainVal = analogRead(rainPin);
float rainVoltage = rainVal * (3.3 / 4095.0);

bme.takeForcedMeasurement();
float temperature = bme.readTemperature();
float humidity = bme.readHumidity();
float pressure = bme.readPressure() / 100.0F;

char dataString[128];
snprintf(dataString, sizeof(dataString),
"T:%.2f,H:%.2f,P:%.2f,U:%.3f,R:%.3f",
temperature, humidity, pressure, guvaVoltage, rainVoltage);

pCharacteristic->setValue(dataString);
pCharacteristic->notify();
}

// ---- BLE server callback ----
class MyServerCallbacks : public BLEServerCallbacks {
void onConnect(BLEServer* pServer) {
sendSensorData(); // Send data when client connects
}

void onDisconnect(BLEServer* pServer) {
// Put the device into deep sleep after disconnect
Wire.beginTransmission(0x76);
Wire.write(0xF4);
Wire.write(0x00);
Wire.endTransmission();
digitalWrite(LED_PIN, LOW);
esp_sleep_enable_timer_wakeup(110 * 1000000ULL);
esp_deep_sleep_start();
}
};

void initBME280Loop() {
if (bme.begin(0x76, &Wire)) {
bme.setSampling(Adafruit_BME280::MODE_FORCED,
Adafruit_BME280::SAMPLING_X1,
Adafruit_BME280::SAMPLING_X1,
Adafruit_BME280::SAMPLING_X1,
Adafruit_BME280::FILTER_OFF);
}
}

void setup() {
Wire.begin(BME_SDA, BME_SCL);
initBME280Loop();

BLEDevice::init("WeatherWizard 32-C3");
pServer = BLEDevice::createServer();
pServer->setCallbacks(new MyServerCallbacks());

BLEService *pService = pServer->createService(SERVICE_UUID);
pCharacteristic = pService->createCharacteristic(
CHARACTERISTIC_UUID,
BLECharacteristic::PROPERTY_READ | BLECharacteristic::PROPERTY_NOTIFY
);
pService->start();

BLEAdvertising *pAdvertising = BLEDevice::getAdvertising();
pAdvertising->addServiceUUID(SERVICE_UUID);
pAdvertising->start();

digitalWrite(LED_PIN, HIGH);

// Enter deep sleep if no client connects within 10 seconds
delay(10000);
if (pServer->getConnectedCount() == 0) {
digitalWrite(LED_PIN, LOW);
esp_sleep_enable_timer_wakeup(110 * 1000000ULL);
esp_deep_sleep_start();
}
}

void loop() {
// Nothing to do here, runs once after wake-up
}

Python code:

import asyncio
from bleak import BleakScanner, BleakClient
from datetime import datetime
import requests

# --- Home Assistant configuration ---
HA_TOKEN = "YOUR_HOME_ASSISTANT_TOKEN"
HEADERS = {
"Authorization": f"Bearer {HA_TOKEN}",
"Content-Type": "application/json",
}
HA_SERVICE_URL = "http://homeassistant.local:8123/api/services/input_number/set_value" # Your HomeAssistant's address

# --- BLE constants ---
ESP_NAME = "WeatherWizard 32-C3"
CHAR_UUID = "9f5b0002-8b6d-4e2b-9a37-8a6f1e4d9a90"

# --- entity_id mapping ---
HA_ENTITIES = {
"temperature": "input_number.weatherwizard_temperature",
"humidity": "input_number.weatherwizard_humidity",
"pressure": "input_number.weatherwizard_pressure",
"uv": "input_number.weatherwizard_uv",
"rain": "input_number.weatherwizard_rain"
}

async def scan_and_send():
while True:
devices = await BleakScanner.discover(timeout=1)
found = next((d for d in devices if d.name == ESP_NAME), None)

if found:
try:
async with BleakClient(found.address) as client:
value = await client.read_gatt_char(CHAR_UUID)
data_str = value.decode()
print(f"📡 Received data: {data_str}")

parts = data_str.split(",")
temperature = float(parts[0].split(":")[1])
humidity = float(parts[1].split(":")[1])
pressure = float(parts[2].split(":")[1])
uv = float(parts[3].split(":")[1])
rain = float(parts[4].split(":")[1])

for key, value in [
("temperature", temperature),
("humidity", humidity),
("pressure", pressure),
("uv", uv),
("rain", rain)
]:
payload = {
"entity_id": HA_ENTITIES[key],
"value": value
}
response = requests.post(HA_SERVICE_URL, json=payload, headers=HEADERS)
if response.ok:
print(f"✅ Updated {key} = {value}")
else:
print(f"❌ Failed to update {key}: {response.status_code}")

await asyncio.sleep(110)
except Exception as e:
print(f"⚠ Error while reading BLE data: {e}")
pass
else:
print("🔍 No WeatherWizard device found.")
pass

if __name__ == "__main__":
asyncio.run(scan_and_send())


Alex
2026-05-07 10:38:44.936159+02

Kirby the WiFi scanner

The Kirby WiFi Dashboard is an ESP32-based interactive WiFi scanner and monitor system that features cute Kirby animation, NeoPixel LED effects, audio alerts, and an OLED display. It monitors available WiFi networks, logs them to the LittleFS file system, and can “feed” Kirby the networks it finds. The tool also includes timing features, notifications, and a web interface to manage settings and monitor Kirby's activity.



Functions

🌐 WiFi search and logging

  1. Continuously scans WiFi networks (in AP+STA mode)
  2. Saves the detected WiFi networks to the wifi.txt file (new networks) or to the eat.txt file (already "eaten" by Kirby networks)
  3. Uses RAM cache and Bloom filter to effectively track known networks
  4. Automatically removes "consumed" WiFis from the active list

🐱 Kirby pet

  1. Kirby "eats" found WiFi networks every 3 hours
  2. Tracks Kirby's age and number of WiFis consumed
  3. An animated Kirby sprite appears on the OLED display

🖥️ OLED display

Displays:

  1. Kirby's animation
  2. The current age
  3. The "cooler" (WiFis waiting to be fed)
  4. All WiFis number
  5. The number of active WiFi networks

Additional functions:

  1. Show notifications (new WiFi or Kirby eating events)
  2. Sleep mode support for power saving

🌈 NeoPixel LED effects

  1. Rainbow transition effect with adjustable hue and with brightness
  2. Timed on and off

🔔 Sound signals (Buzzer)

  1. Play Mario coin sound when Kirby “eats” a WiFi
  2. Support mute function
  3. Mute can be timed

⏱️ Timer / Scheduler

Enables:

  1. Setting the sleep and wake-up time of the display
  2. Scheduling the sound mute
  3. Turning WiFi scanning on and off
  4. Controlling the LEDs
  5. Deep sleep timing energy saving for

🌐 Web interface

Full dashboard available from a browser

(default IP: 192.168.4.1)

Functions:

  1. The known and consumed WiFis display
  2. Buttons:
  3. Sleep
  4. Mute
  5. LED
  6. Scan
  7. Change scheduler settings directly from the website
  8. Kirby logo and cute user interface

🛠️ Hardware requirements

  1. ESP32-C3 (tested)
  2. SSD1306 OLED display (128x64)
  3. NeoPixel / WS2812 LED strip (integrated in ESP)
  4. Buzzer
  5. Optional: DS3231 real-time clock (RTC) for for accurate timing

⚡ Installation

  1. Upload the firmware to the ESP32 using Arduino IDE
  2. Connect the OLED display, NeoPixel and buzzer according to the pins given in the code
  3. Place wifi.txt and eat.txt files in LittleFS filesystem or let the firmware create them automatically
  4. Power on ESP32 — Kirby animation starts on the OLED display and starts searching for WiFi networks
  5. Web dashboard is accessible via AP IP address to manage settings

🚀 Usage

  1. Kirby automatically searches and logs WiFi networks
  2. OLED display shows Kirby's animation, age and WiFi stats
  3. LEDs display continuous rainbow effect (can be turned off)
  4. Buzzer plays sound when Kirby “eats” a WiFi (can be muted)
  5. Scheduler handles display, sound, scanning, LEDs and deep sleep

📝 Notes

  1. The system uses LittleFS file system to store wifi.txt and eat.txt
  2. Bloom filter provides effective duplication control for WiFi networks
  3. Kirby is automatically fed in 3-hour cycles from "birth time" based
  4. Supports web switches and scheduler settings

If you want to use the maximum 4MB partition for ESP:

  1. Copy board.txt to:
  2. /packages/esp32/hardware/esp32/version/
  3. A no_ota_bigger.csv file here:
  4. /packages/esp32/hardware/esp32/version/tools/partitions/


Download

Github repo

Alex
2026-05-07 10:05:38.110648+02

Virtual Plant Mood Display with Soil Moisture and Light Sensors

This Arduino program reads data from a soil moisture and ambient light sensor, then displays a cute, animated face on a 16x2 I2C LCD display to show the plant's "mood".

The face changes based on moisture and light values and alternates between different states: happy, sad, sleeping, and "dead" facial expressions.

Functions

  1. Soil moisture (analog input A0)
  2. Read light sensor data (analog input A1)
  3. Show mood with 4-frame ASCII animations on an I2C 16x2 LCD display (using (LiquidCrystal_I2C library))
  4. Animated facial expressions:
  5. Happy 😊
  6. per ms

Requirements

Hardware

  1. Arduino development board (Uno, Nano, Mega, etc.)
  2. Soil moisture sensor (with analog output)
  3. Light sensor (with analog output, e.g. photoresistor or photodiode)
  4. 16x2 I2C LCD display (compatible with (LiquidCrystal_I2C library)
  5. Connecting wires and breadboard

Software

  1. Arduino IDE (version 1.8.x or later recommended)
  2. Arduino LiquidCrystal_I2C library

Install:

Sketch -> Include Library -> Manage Libraries -> find: "LiquidCrystal_I2C"


The code:

#include <Wire.h>
#include <LiquidCrystal_I2C.h>

LiquidCrystal_I2C lcd(0x27, 16, 2);

// Sensor pins
const int moisturePin = A0;
const int lightPin = A1;

// Face animations (4 frames)
const char* happyFrames[] = {
"(^_^)", // base
"(^.^)", // softer
"(^o^)", // open mouth
"(^-^)" // closed, but happy
};

const char* sadFrames[] = {
"(._.)", "(;_;)", "(T_T)", "(._.)"
};

const char* deadFrames[] = {
"(x_x)", "(X_X)", "(x_x)", "(-_-)"
};

// Sleeping Zz animations (4 frames)
const char* sleepFrames[] = {
"(-_-). ", "(-_-). z ", "(-_-). Zz", "(-_-). zZ"
};

int frame = 0;
unsigned long lastFrameTime = 0;
const int frameDelay = 600;

void setup() {
lcd.init();
lcd.backlight();
pinMode(moisturePin, INPUT);
pinMode(lightPin, INPUT);
}

void loop() {
int moisture = analogRead(moisturePin);
int light = analogRead(lightPin);

int moisturePct = map(moisture, 1023, 0, 0, 100);
int lightPct = map(light, 0, 1023, 0, 100);

String topLine = "";

// 🌙 Sleep mode if it's dark
if (lightPct < 10) {
topLine = sleepFrames[frame];
}
else if (moisturePct < 20 || lightPct < 15) {
topLine = String(deadFrames[frame]) + " Dying";
}
else if (moisturePct < 40 || lightPct < 30) {
topLine = String(sadFrames[frame]) + " Sad";
}
else if (moisturePct > 90 && lightPct > 90) {
topLine = String(sadFrames[frame]) + " Too much";
}
else {
topLine = String(happyFrames[frame]) + " Happy";
}

// Top line: face + status (max 16 characters)
lcd.setCursor(0, 0);
lcd.print(topLine);
for (int i = topLine.length(); i < 16; i++) lcd.print(" "); // clear the rest

// Bottom line: light and water percentages
String bottomLine = "Light:" + String(lightPct) + "% " + "Water:" + String(moisturePct) + "%";
lcd.setCursor(0, 1);
lcd.print(bottomLine);
for (int i = bottomLine.length(); i < 16; i++) lcd.print(" "); // clear here too

// Animate frames every frameDelay ms
if (millis() - lastFrameTime > frameDelay) {
frame = (frame + 1) % 4;
lastFrameTime = millis();
}

delay(100);
}

Alex
2026-05-07 09:56:56.038538+02