Connnor
Published © GPL3+

ThermoLoggger

Thermologger is a fun, sensor-powered climate monitor with animated LCD, data logging, and LED alerts—perfect for STEM & smart spaces!

BeginnerFull instructions provided3 hours48
ThermoLoggger

Story

Read more

Schematics

ThermoLogger Wiring Schematic

Code

ThermoLogger code

MicroPython
Code Requires Certain codes from the Kepler kit main zip in the libs folder look in the shell in thonny to see what codes need to be uploaded
from 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)

Credits

Connnor
1 project • 0 followers

Comments