Thermologger is a compact, sensor-powered climate monitor built around the Raspberry Pi Pico W. It measures temperature and humidity, logs data in real time, and displays animated feedback on an LCD with LED alerts.
Why did you make it?I wanted a fun, interactive project that blends education and automation—something perfect for STEM learning, maker showcases, or smart home setups. Thermologger is designed to be beginner-friendly but still packed with personality and customization potential.
What makes it special?It’s more than just numbers on a screen—Thermologger greets you with boot messages, tracks environmental trends, and features animated LCD effects like scrolling banners and bouncing droplets. The LEDs flash on errors, pulse on startup, and even shift color based on readings, giving it a dynamic and responsive vibe.
ThermoLogger code
MicroPythonfrom machine import Pin, I2C, ADC
from ws2812 import WS2812
from lcd1602 import LCD
from ir_rx.nec import NEC_8
from ir_rx.print_error import print_error
import utime, math, dht
# --- Hardware Setup ---
thermistor = ADC(28)
i2c = I2C(1, sda=Pin(6), scl=Pin(7), freq=400000)
lcd = LCD(i2c)
leds = WS2812(Pin(0), 8)
pin_ir = Pin(17, Pin.IN)
button = Pin(14, Pin.IN)
warning_led = Pin(15, Pin.OUT)
dht_sensor = dht.DHT11(Pin(16, Pin.IN))
utime.sleep(2)
# --- State ---
state = {
"single_shot_active": False, "single_shot_start": 0, "persistent_on": False,
"fade_mode": False, "rainbow_mode": False, "user_brightness": 1.0, "led_brightness": 1.0, "rainbow_j": 0,
"night_mode": False, "ONE_SHOT_MS": 2000,
"party_mode": False, "party_anim_index": 0, "water_anim": False, "water_anim_index": 0,
"uptime_display": False, "selftest_display": False, "selftest_result": "", "selftest_time": 0,
"easter_egg_active": False, "easter_egg_seq": [],
"idle_mode": False, "idle_start_time": 0,
"log_file_path": "/log.csv", "min_temp": None, "max_temp": None, "min_hum": None, "max_hum": None,
"show_minmax": False, "scrolling": False, "scroll_index": 0,
"last_activity": utime.ticks_ms(), "boot_time": utime.ticks_ms(),
"AUTO_OFF_MS": 120000, "auto_off": False,
"last_temp_f": None, "last_humidity": None, "last_lcd_update": 0
}
# --- Colors ---
RED, GREEN, LIGHT_BLUE, WHITE = 0xFF0000, 0x00FF00, 0x00FFFF, 0xFFFFFF
def set_strip(color):
r = int(((color >> 16) & 0xFF) * state["led_brightness"])
g = int(((color >> 8) & 0xFF) * state["led_brightness"])
b = int((color & 0xFF) * state["led_brightness"])
adj_color = (r << 16) | (g << 8) | b
for i in range(8): leds[i] = adj_color
leds.write()
def clear_strip():
for i in range(8): leds[i] = 0
leds.write()
def read_temp_f():
raw = thermistor.read_u16()
Vr = 3.3 * raw / 65535.0
Rt = 10000 * Vr / (3.3 - Vr)
Tk = 1.0 / ((math.log(Rt / 10000.0) / 3950.0) + (1.0 / (273.15 + 25.0)))
Tc = Tk - 273.15
return Tc * 9.0 / 5.0 + 32.0
def fade_strip(base_color):
period = 3000
t = utime.ticks_ms() % period
half = period / 2
fade = t / half if t < half else (period - t) / half
brightness = fade * state["led_brightness"]
r = int(((base_color >> 16) & 0xFF) * brightness)
g = int(((base_color >> 8) & 0xFF) * brightness)
b = int((base_color & 0xFF) * brightness)
set_strip((r << 16) | (g << 8) | b)
def wheel(pos):
if pos < 85: return ((pos * 3) << 16) | ((255 - pos * 3) << 8)
elif pos < 170: pos -= 85; return ((255 - pos * 3) << 16) | (pos * 3)
else: pos -= 170; return (pos * 3 << 8) | (255 - pos * 3)
# --- IR Key Map ---
KEY_CODES = {
0x16: "0", 0x0C: "1", 0x18: "2", 0x5E: "3", 0x08: "4", 0x1C: "5", 0x5A: "6", 0x42: "7",
0x52: "8", 0x4A: "9", 0x09: "+", 0x15: "-", 0x07: "EQ", 0x0D: "U/SD", 0x19: "CYCLE",
0x44: "PLAY/PAUSE", 0x43: "FORWARD", 0x40: "BACKWARD", 0x45: "POWER", 0x47: "MUTE", 0x46: "MODE"
}
def decode_key(data): return KEY_CODES.get(data)
def ir_callback(data, addr, ctrl):
if data < 0: return
key = decode_key(data)
s = state
# Easter egg unlock: 8-8-9
if not s["easter_egg_active"]:
if key in ["8", "9"]:
s["easter_egg_seq"].append(key)
if len(s["easter_egg_seq"]) > 3: s["easter_egg_seq"] = s["easter_egg_seq"][-3:]
if s["easter_egg_seq"] == ["8", "8", "9"]: s["easter_egg_active"] = True
else: s["easter_egg_seq"] = []
else:
s["easter_egg_active"] = False
s["easter_egg_seq"] = []
s["last_activity"] = utime.ticks_ms()
s["auto_off"] = False
# IR actions
if key == "0":
s["party_mode"] = not s["party_mode"]; s["party_anim_index"] = 0
s["water_anim"] = s["uptime_display"] = s["selftest_display"] = False
elif key == "1":
s["single_shot_active"] = True; s["single_shot_start"] = utime.ticks_ms()
elif key == "2": s["fade_mode"] = not s["fade_mode"]
elif key == "POWER": s["persistent_on"] = not s["persistent_on"]
elif key == "3": s["rainbow_mode"] = not s["rainbow_mode"]
elif key == "4":
s["water_anim"] = not s["water_anim"]; s["water_anim_index"] = 0
s["party_mode"] = s["uptime_display"] = s["selftest_display"] = False
elif key == "5":
s["uptime_display"] = not s["uptime_display"]
s["party_mode"] = s["water_anim"] = s["selftest_display"] = False
elif key == "6":
s["selftest_display"] = not s["selftest_display"]
s["party_mode"] = s["water_anim"] = s["uptime_display"] = False
if s["selftest_display"]:
result = []
try: _ = read_temp_f(); result.append("Therm:OK")
except: result.append("Therm:ERR")
try: dht_sensor.measure(); _ = dht_sensor.humidity(); result.append("DHT:OK")
except: result.append("DHT:ERR")
try: lcd.backlight(True); result.append("LCD:OK")
except: result.append("LCD:ERR")
try: set_strip(RED); result.append("LED:OK")
except: result.append("LED:ERR")
s["selftest_result"] = ",".join(result)
s["selftest_time"] = utime.ticks_ms()
elif key == "+": s["user_brightness"] = min(1.0, s["user_brightness"] + 0.1)
elif key == "-": s["user_brightness"] = max(0.1, s["user_brightness"] - 0.1)
elif key == "U/SD": s["show_minmax"] = not s["show_minmax"]
elif key == "EQ": s["scrolling"] = not s["scrolling"]
ir = NEC_8(pin_ir, ir_callback)
ir.error_function(print_error)
# --- Main Loop ---
while True:
now = utime.ticks_ms()
s = state
# Auto-off/idle
if not s["idle_mode"] and not s["night_mode"] and not s["auto_off"] and not s["party_mode"] and not s["water_anim"] and not s["uptime_display"] and not s["selftest_display"] and not s["easter_egg_active"]:
if utime.ticks_diff(now, s["last_activity"]) > 300000:
s["idle_mode"] = True; lcd.clear()
try: lcd.backlight(False)
except: pass
s["led_brightness"] = 0.0
if s["idle_mode"]:
clear_strip(); utime.sleep_ms(200); continue
# Update sensors every 1s
sensor_update = False
if not s["night_mode"] and not s["auto_off"] and utime.ticks_diff(now, s.get("last_lcd_update", 0)) > 1000:
s["last_lcd_update"] = now
try: s["last_temp_f"] = read_temp_f()
except: s["last_temp_f"] = None
try:
dht_sensor.measure()
h = dht_sensor.humidity()
if h is not None and isinstance(h, int) and 0 <= h <= 100: s["last_humidity"] = h
except: pass
# Min/max
if isinstance(s["last_temp_f"], (int, float)):
if s["min_temp"] is None or s["last_temp_f"] < s["min_temp"]: s["min_temp"] = s["last_temp_f"]
if s["max_temp"] is None or s["last_temp_f"] > s["max_temp"]: s["max_temp"] = s["last_temp_f"]
if isinstance(s["last_humidity"], int):
if s["min_hum"] is None or s["last_humidity"] < s["min_hum"]: s["min_hum"] = s["last_humidity"]
if s["max_hum"] is None or s["last_humidity"] > s["max_hum"]: s["max_hum"] = s["last_humidity"]
# Log
try:
with open(s["log_file_path"], "a") as f:
f.write("{},{},{}\n".format(utime.time(), s["last_temp_f"], s["last_humidity"]))
except: pass
sensor_update = True
# --- Special Modes ---
temp_f = s["last_temp_f"] if s["last_temp_f"] is not None else 0
too_hot = (s["last_temp_f"] is not None and s["last_temp_f"] > 90)
flash_on = (now // 1000) % 2 == 0
sensor_error = (s["last_temp_f"] is None or (isinstance(s["last_temp_f"], float) and (s["last_temp_f"] < -50 or s["last_temp_f"] > 150)) or
s["last_humidity"] is None or (isinstance(s["last_humidity"], int) and (s["last_humidity"] < 0 or s["last_humidity"] > 100)))
# Sad face
if sensor_error and not s["night_mode"] and not s["auto_off"]:
lcd.clear()
lcd.message([" :( \n "," \n :( "," \n "][(now // 400) % 3])
set_strip(RED); utime.sleep_ms(400); continue
# Easter egg
if s["easter_egg_active"] and not s["night_mode"] and not s["auto_off"]:
lcd.clear()
lcd.message([" YOU FOUND \n THE EGG! "," ●●●●● \n ● "," ○○○○○ \n ○ "][(now // 500) % 3])
for i in range(8):
color = wheel((now // 10 + i * 32) & 255)
r = int(((color >> 16) & 0xFF) * s["led_brightness"])
g = int(((color >> 8) & 0xFF) * s["led_brightness"])
b = int((color & 0xFF) * s["led_brightness"])
leds[i] = (r << 16) | (g << 8) | b
leds.write(); utime.sleep_ms(200); continue
# Party
if s["party_mode"] and not s["night_mode"] and not s["auto_off"]:
lcd.clear()
lcd.message(["[ PARTY! ]\n[ ■ ]","[ PARTY! ]\n[ ■ ]","[ PARTY! ]\n[ ■ ]","[ PARTY! ]\n[ ■ ]"][s["party_anim_index"] % 4])
s["party_anim_index"] = (s["party_anim_index"] + 1) % 4
for i in range(8):
idx = (i * 256 // 8 + (s["party_anim_index"] * 16)) & 255
color = wheel(idx)
r = int(((color >> 16) & 0xFF) * s["led_brightness"])
g = int(((color >> 8) & 0xFF) * s["led_brightness"])
b = int((color & 0xFF) * s["led_brightness"])
leds[i] = (r << 16) | (g << 8) | b
leds.write(); utime.sleep_ms(200); continue
# Water
if s["water_anim"] and not s["night_mode"] and not s["auto_off"]:
lcd.clear()
lcd.message([" ○ \n "," \n ○ "," \n "][s["water_anim_index"] % 3])
s["water_anim_index"] = (s["water_anim_index"] + 1) % 3
set_strip(LIGHT_BLUE); utime.sleep_ms(300); continue
# Uptime
if s["uptime_display"] and not s["night_mode"] and not s["auto_off"]:
lcd.clear()
uptime_ms = utime.ticks_diff(now, s["boot_time"])
uptime_s = uptime_ms // 1000
h, m, sec = uptime_s // 3600, (uptime_s % 3600) // 60, uptime_s % 60
lcd.message("Uptime:\n{:02}:{:02}:{:02}".format(h, m, sec))
utime.sleep_ms(500); continue
# Self-test
if s["selftest_display"] and not s["night_mode"] and not s["auto_off"]:
lcd.clear()
lcd.message("Self-Test:\n" + s["selftest_result"][:16])
if utime.ticks_diff(now, s["selftest_time"]) > 3000: s["selftest_display"] = False
utime.sleep_ms(200); continue
# --- Too Hot Warning ---
if too_hot and not s["night_mode"] and not s["auto_off"]:
if flash_on:
lcd.clear(); lcd.message(" TOO HOT! \n TOO HOT! "); warning_led.value(1)
else:
lcd.clear(); warning_led.value(0)
else: warning_led.value(0)
# --- Normal Display ---
if sensor_update:
lcd.clear()
if s["show_minmax"]:
tmin = s["min_temp"] if s["min_temp"] is not None else 0
tmax = s["max_temp"] if s["max_temp"] is not None else 0
hmin = s["min_hum"] if s["min_hum"] is not None else 0
hmax = s["max_hum"] if s["max_hum"] is not None else 0
lcd.message("T:{:.1f}/{:.1f}\nH:{}%/{}%".format(tmin, tmax, hmin, hmax))
elif s["scrolling"]:
banner = " ThermoLogger "
plane = "✈"
pos = s["scroll_index"] % (len(banner) + 16)
if pos < 16:
line1 = " " * (15 - pos) + plane + banner[:pos+1]
elif pos < len(banner) + 1:
banner_part = banner[:pos - 15]
line1 = plane + banner_part
line1 = line1.ljust(16)
else:
end_space = pos - len(banner)
line1 = plane + banner + " " * end_space
line1 = line1[-16:]
if pos < 16:
line2 = " " * (15 - pos) + "~" + " " * (pos + 1)
elif pos < len(banner) + 1:
line2 = "~" + " " * 15
else:
line2 = " " * 16
lcd.message(line1[:16] + "\n" + line2[:16])
s["scroll_index"] = (s["scroll_index"] + 1) % (len(banner) + 16)
else:
if isinstance(s["last_humidity"], int) and 0 <= s["last_humidity"] <= 100:
lcd.message("Temp: {:.1f}°F\nHum: {}%".format(s["last_temp_f"], s["last_humidity"]))
else:
lcd.message("Temp: {:.1f}°F\nHum: --%".format(s["last_temp_f"]))
elif s["night_mode"] or s["auto_off"]:
lcd.clear()
# --- LED Color Logic ---
if temp_f > 90: base_color = RED
elif temp_f > 50: base_color = GREEN
elif temp_f > 20: base_color = LIGHT_BLUE
else: base_color = WHITE
# --- Brightness/Backlight ---
if s["night_mode"] or s["auto_off"]:
s["led_brightness"] = 0.05
try: lcd.backlight(False)
except: pass
else:
s["led_brightness"] = s["user_brightness"]
try: lcd.backlight(True)
except: pass
# --- One-shot timeout ---
if s["single_shot_active"] and utime.ticks_diff(now, s["single_shot_start"]) > s["ONE_SHOT_MS"]:
s["single_shot_active"] = False
# --- LED Control ---
if (not s["auto_off"]) and s["rainbow_mode"]:
for i in range(8):
idx = (i * 256 // 8 + s["rainbow_j"]) & 255
color = wheel(idx)
r = int(((color >> 16) & 0xFF) * s["led_brightness"])
g = int(((color >> 8) & 0xFF) * s["led_brightness"])
b = int((color & 0xFF) * s["led_brightness"])
leds[i] = (r << 16) | (g << 8) | b
leds.write(); s["rainbow_j"] = (s["rainbow_j"] + 1) % 256
elif (not s["auto_off"]) and s["fade_mode"]:
fade_strip(base_color)
elif (not s["auto_off"]) and (s["persistent_on"] or s["single_shot_active"]):
set_strip(base_color)
else:
clear_strip()
utime.sleep_ms(50)
Comments