From 9e45f1f199ff61cf90d5a80a1a242792cb2a1c49 Mon Sep 17 00:00:00 2001 From: Raymond Bailie Date: Tue, 16 Jun 2026 13:02:06 +0200 Subject: [PATCH] commit 1 --- Maker_Pico_Box/3buttons.py | 48 +++++ Maker_Pico_Box/button.py | 10 + Maker_Pico_Box/color_button.py | 44 +++++ Maker_Pico_Box/color_off.py | 32 +++ Maker_Pico_Box/main.py | 172 +++++++++++++++++ Maker_Pico_Box/main3.py | 111 +++++++++++ Maker_Pico_Box/main4.py | 342 +++++++++++++++++++++++++++++++++ Maker_Pico_Box/main5.py | 174 +++++++++++++++++ Maker_Pico_Box/main_old.py | 46 +++++ Maker_Pico_Box/rainbow.py | 29 +++ calibrate.py | 107 +++++++++++ calibration.cfg | 1 + config.py | 21 ++ display.py | 53 +++++ html/app.js | 153 +++++++++++++++ html/favicon.ico | Bin 0 -> 15406 bytes html/index.html | 287 +++++++++++++++++++++++++++ hx710b.py | 116 +++++++++++ lib/hx710b.py | 116 +++++++++++ lib/ssd1306.py | 89 +++++++++ main.py | 238 +++++++++++++++++++++++ reboot.py | 2 + scan-i2c.py | 30 +++ sensors.py | 57 ++++++ ssd1306.py | 89 +++++++++ test-time.py | 9 + timeutil.py | 34 ++++ webserver.py | 106 ++++++++++ wifi.py | 15 ++ 29 files changed, 2531 insertions(+) create mode 100644 Maker_Pico_Box/3buttons.py create mode 100644 Maker_Pico_Box/button.py create mode 100644 Maker_Pico_Box/color_button.py create mode 100644 Maker_Pico_Box/color_off.py create mode 100644 Maker_Pico_Box/main.py create mode 100644 Maker_Pico_Box/main3.py create mode 100644 Maker_Pico_Box/main4.py create mode 100644 Maker_Pico_Box/main5.py create mode 100644 Maker_Pico_Box/main_old.py create mode 100644 Maker_Pico_Box/rainbow.py create mode 100644 calibrate.py create mode 100644 calibration.cfg create mode 100644 config.py create mode 100644 display.py create mode 100644 html/app.js create mode 100644 html/favicon.ico create mode 100644 html/index.html create mode 100644 hx710b.py create mode 100644 lib/hx710b.py create mode 100644 lib/ssd1306.py create mode 100644 main.py create mode 100644 reboot.py create mode 100644 scan-i2c.py create mode 100644 sensors.py create mode 100644 ssd1306.py create mode 100644 test-time.py create mode 100644 timeutil.py create mode 100644 webserver.py create mode 100644 wifi.py diff --git a/Maker_Pico_Box/3buttons.py b/Maker_Pico_Box/3buttons.py new file mode 100644 index 0000000..f60e3cf --- /dev/null +++ b/Maker_Pico_Box/3buttons.py @@ -0,0 +1,48 @@ +import machine +import time +import neopixel + +# NeoPixel setup +NUM_LEDS = 3 +np = neopixel.NeoPixel(machine.Pin(17), NUM_LEDS) + +# Buttons +button_pins = [20, 21, 22] +buttons = [machine.Pin(pin, machine.Pin.IN, machine.Pin.PULL_UP) for pin in button_pins] + +# Colors to cycle through +colors = [ + (255, 0, 0), # Red + (0, 255, 0), # Green + (0, 0, 255), # Blue + (255, 255, 0), # Yellow + (255, 0, 255), # Magenta + (0, 255, 255), # Cyan + (255, 255, 255) # White +] + +# Individual color indexes for each pixel +indexes = [0, 0, 0] +last_state = [1, 1, 1] # buttons start unpressed + +def show_pixels(): + for i in range(NUM_LEDS): + np[i] = colors[indexes[i]] + np.write() + +# Initial display +show_pixels() + +while True: + for i in range(3): + state = buttons[i].value() + + # Detect press (1 → 0) + if last_state[i] == 1 and state == 0: + indexes[i] = (indexes[i] + 1) % len(colors) + show_pixels() + time.sleep_ms(200) # debounce + + last_state[i] = state + + time.sleep_ms(10) diff --git a/Maker_Pico_Box/button.py b/Maker_Pico_Box/button.py new file mode 100644 index 0000000..b97ad5c --- /dev/null +++ b/Maker_Pico_Box/button.py @@ -0,0 +1,10 @@ +import machine +import time + +while True: + if machine.bootsel_button(): + print("BOOTSEL button pressed!") + else: + print("Not pressed.") + time.sleep(0.2) + diff --git a/Maker_Pico_Box/color_button.py b/Maker_Pico_Box/color_button.py new file mode 100644 index 0000000..bbb9ab1 --- /dev/null +++ b/Maker_Pico_Box/color_button.py @@ -0,0 +1,44 @@ +import machine +import neopixel +import time + +# NeoPixel setup +NUM_LEDS = 8 +np = neopixel.NeoPixel(machine.Pin(28), NUM_LEDS) + +# Button on GP22 (active LOW) +button = machine.Pin(22, machine.Pin.IN, machine.Pin.PULL_UP) + +# Color list (R, G, B) +colors = [ + (255, 0, 0), # Red + (0, 255, 0), # Green + (0, 0, 255), # Blue + (255, 255, 0), # Yellow + (255, 0, 255), # Magenta + (0, 255, 255), # Cyan + (255, 255, 255) # White +] + +current_color = 0 +last_state = 1 # button not pressed + +def show_color(color): + for i in range(NUM_LEDS): + np[i] = color + np.write() + +# Initial color +show_color(colors[current_color]) + +while True: + state = button.value() + + # Detect button press (HIGH → LOW) + if last_state == 1 and state == 0: + current_color = (current_color + 1) % len(colors) + show_color(colors[current_color]) + time.sleep_ms(200) # Debounce delay + + last_state = state + time.sleep_ms(10) diff --git a/Maker_Pico_Box/color_off.py b/Maker_Pico_Box/color_off.py new file mode 100644 index 0000000..7c447af --- /dev/null +++ b/Maker_Pico_Box/color_off.py @@ -0,0 +1,32 @@ +import machine +import neopixel +import time + +# NeoPixel setup +NUM_LEDS = 8 +np = neopixel.NeoPixel(machine.Pin(28), NUM_LEDS) + +# Button on GP22 (active LOW) +button = machine.Pin(22, machine.Pin.IN, machine.Pin.PULL_UP) + +# Color list (R, G, B) +colors = [ + (0, 0, 0), + (255, 0, 0), # Red + (0, 255, 0), # Green + (0, 0, 255), # Blue + (255, 255, 0), # Yellow + (255, 0, 255), # Magenta + (0, 255, 255), # Cyan + (255, 255, 255) # White +] + +current_color = 0 +last_state = 1 # button not pressed + +def show_color(color): + for i in range(NUM_LEDS): + np[i] = color + np.write() + +show_color(colors[current_color]) diff --git a/Maker_Pico_Box/main.py b/Maker_Pico_Box/main.py new file mode 100644 index 0000000..40a9518 --- /dev/null +++ b/Maker_Pico_Box/main.py @@ -0,0 +1,172 @@ +import machine +import time +import neopixel + +# ============================================================ +# HEARTBEAT CLASS — smooth fade with MIN + MAX brightness +# ============================================================ + +class HeartbeatFadeNeoPixel: + def __init__(self, neopixel_obj, pixel_index=0, color=(255, 0, 0), + min_brightness=5, max_brightness=100, + step=2, interval_ms=20): + + self.np = neopixel_obj + self.index = pixel_index + self.color = color + + self.min_brightness = min_brightness + self.max_brightness = max_brightness + self.step = step + self.interval = interval_ms + + self.brightness = min_brightness + self.direction = 1 # 1 = fade up, -1 = fade down + self.last_time = time.ticks_ms() + + def apply_brightness(self, c, level): + r, g, b = c + factor = level / 100 + return (int(r * factor), int(g * factor), int(b * factor)) + + def update(self): + now = time.ticks_ms() + if time.ticks_diff(now, self.last_time) >= self.interval: + self.last_time = now + + # Update brightness + self.brightness += self.direction * self.step + + # Bounce at limits + if self.brightness >= self.max_brightness: + self.brightness = self.max_brightness + self.direction = -1 + + if self.brightness <= self.min_brightness: + self.brightness = self.min_brightness + self.direction = 1 + + # Apply brightness + self.np[self.index] = self.apply_brightness(self.color, self.brightness) + self.np.write() + + +# ============================================================ +# NEOPIXEL SETUP +# ============================================================ + +STRIP_PIN = 17 +STRIP_LEDS = 3 +strip = neopixel.NeoPixel(machine.Pin(STRIP_PIN), STRIP_LEDS) + +SINGLE_PIN = 28 +single = neopixel.NeoPixel(machine.Pin(SINGLE_PIN), 1) + +# ============================================================ +# BUTTONS +# ============================================================ + +button_pins = [20, 21, 22] +buttons = [machine.Pin(pin, machine.Pin.IN, machine.Pin.PULL_UP) for pin in button_pins] + +# ============================================================ +# COLOR PALETTE +# ============================================================ + +colors = [ + (0, 0, 0), # Off + (255, 0, 0), # Red + (0, 255, 0), # Green + (0, 0, 255), # Blue +# (255, 255, 0), # Yellow +# (0, 255, 255), # Cyan +# (255, 0, 255), # Magenta +# (255, 128, 0), # Orange +# (128, 0, 255), # Purple +# (255, 20, 147), # Pink +# (128, 255, 0), # Lime + (255, 255, 255), # White +] + +# ============================================================ +# STRIP STATE +# ============================================================ + +color_index = [0, 0, 0] +brightness = [50, 50, 50] +last_state = [1, 1, 1] +press_time = [0, 0, 0] + +# ============================================================ +# HEARTBEAT CONFIG (NOW HAS MIN + MAX) +# ============================================================ + +FLASH_COLOR_INDEX = 1 # Palette color +FLASH_MIN = 1 # << lowest brightness (0–100) +FLASH_MAX = 10 # << highest brightness +FLASH_STEP = 0.2 # fade speed +FLASH_INTERVAL_MS = 20 # smoothness + +heartbeat = HeartbeatFadeNeoPixel( + neopixel_obj=single, + pixel_index=0, + color=colors[FLASH_COLOR_INDEX], + min_brightness=FLASH_MIN, + max_brightness=FLASH_MAX, + step=FLASH_STEP, + interval_ms=FLASH_INTERVAL_MS +) + +# ============================================================ +# HELPERS +# ============================================================ + +def apply_brightness(color, level): + r, g, b = color + factor = level / 100 + return (int(r * factor), int(g * factor), int(b * factor)) + +def update_strip(): + for i in range(STRIP_LEDS): + strip[i] = apply_brightness(colors[color_index[i]], brightness[i]) + strip.write() + +# ============================================================ +# INITIAL +# ============================================================ + +update_strip() + +# ============================================================ +# MAIN LOOP +# ============================================================ + +while True: + now = time.ticks_ms() + + # BUTTON CONTROLLED PIXELS (0–2) + for i in range(3): + state = buttons[i].value() + + if last_state[i] == 1 and state == 0: + press_time[i] = now + + if last_state[i] == 0 and state == 1: + duration = time.ticks_diff(now, press_time[i]) + + if duration > 500: + brightness[i] += 10 + if brightness[i] > 100: + brightness[i] = 10 + else: + color_index[i] = (color_index[i] + 1) % len(colors) + + update_strip() + time.sleep_ms(200) + + last_state[i] = state + + # HEARTBEAT ON GP28 + heartbeat.update() + + time.sleep_ms(10) diff --git a/Maker_Pico_Box/main3.py b/Maker_Pico_Box/main3.py new file mode 100644 index 0000000..d754c73 --- /dev/null +++ b/Maker_Pico_Box/main3.py @@ -0,0 +1,111 @@ +import machine +import time +import neopixel + +# ---------------------------- +# NeoPixel SETUP +# ---------------------------- + +# Strip on GP17 (3 LEDs) +STRIP_PIN = 17 +STRIP_LEDS = 3 +strip = neopixel.NeoPixel(machine.Pin(STRIP_PIN), STRIP_LEDS) + +# Single Pixel on GP28 (1 LED) +SINGLE_PIN = 28 +single = neopixel.NeoPixel(machine.Pin(SINGLE_PIN), 1) + +# ---------------------------- +# Buttons on GP20, GP21, GP22 +# ---------------------------- +button_pins = [20, 21, 22] +buttons = [machine.Pin(pin, machine.Pin.IN, machine.Pin.PULL_UP) for pin in button_pins] + +# ---------------------------- +# 12-color palette +# ---------------------------- +colors = [ + (0, 0, 0), # Off + (255, 0, 0), # Red + (0, 255, 0), # Green + (0, 0, 255), # Blue + (255, 255, 0), # Yellow + (0, 255, 255), # Cyan + (255, 0, 255), # Magenta + (255, 128, 0), # Orange + (128, 0, 255), # Purple + (255, 20, 147), # Pink + (128, 255, 0), # Lime + (255, 255, 255), # White +] + +# State for strip pixels (0–2) +color_index = [0, 0, 0] +brightness = [5, 5, 5] +last_state = [1, 1, 1] +press_time = [0, 0, 0] + +# State for single pixel (pixel #3) +single_color_index = 1 +single_brightness = 5 + +# ---------------------------- +# Helpers +# ---------------------------- +def apply_brightness(color, level): + r, g, b = color + factor = level / 100 + return (int(r * factor), int(g * factor), int(b * factor)) + +def update_strip(): + for i in range(STRIP_LEDS): + strip[i] = apply_brightness(colors[color_index[i]], brightness[i]) + strip.write() + +def update_single(): + single[0] = apply_brightness(colors[single_color_index], single_brightness) + single.write() + +# Initial setup +update_strip() +update_single() + +# ---------------------------- +# Main loop +# ---------------------------- +while True: + now = time.ticks_ms() + + # Handle buttons for pixels 0,1,2 + for i in range(3): + state = buttons[i].value() + + # Button pressed + if last_state[i] == 1 and state == 0: + press_time[i] = now + + # Button released + if last_state[i] == 0 and state == 1: + duration = time.ticks_diff(now, press_time[i]) + + if duration > 500: + # LONG PRESS → brightness + brightness[i] += 10 + if brightness[i] > 100: + brightness[i] = 10 + else: + # SHORT PRESS → color change + color_index[i] = (color_index[i] + 1) % len(colors) + + update_strip() + time.sleep_ms(200) # debounce + + last_state[i] = state + + # (Optional) Example automatic color change for pixel 3 + # single_color_index = (single_color_index + 1) % len(colors) + # update_single() + # time.sleep(0.5) + + time.sleep_ms(10) + diff --git a/Maker_Pico_Box/main4.py b/Maker_Pico_Box/main4.py new file mode 100644 index 0000000..28cd46b --- /dev/null +++ b/Maker_Pico_Box/main4.py @@ -0,0 +1,342 @@ +import machine +import time +import neopixel + +# ============================================================ +# NEOPIXELS +# ============================================================ + +# 3-LED strip on GP17 +STRIP_PIN = 17 +STRIP_LEDS = 3 +strip = neopixel.NeoPixel(machine.Pin(STRIP_PIN), STRIP_LEDS) + +# Single flashing LED on GP28 +SINGLE_PIN = 28 +single = neopixel.NeoPixel(machine.Pin(SINGLE_PIN), 1) + +# ============================================================ +# BUTTONS (GP20, GP21, GP22) +# ============================================================ + +button_pins = [20, 21, 22] +buttons = [machine.Pin(pin, machine.Pin.IN, machine.Pin.PULL_UP) for pin in button_pins] + +# ============================================================ +# COLOR PALETTE (index-based) +# ============================================================ + +colors = [ + (0, 0, 0), # 0 Off + (255, 0, 0), # 1 Red + (0, 255, 0), # 2 Green + (0, 0, 255), # 3 Blue + (255, 255, 0), # 4 Yellow + (0, 255, 255), # 5 Cyan + (255, 0, 255), # 6 Magenta + (255, 128, 0), # 7 Orange + (128, 0, 255), # 8 Purple + (255, 20, 147), # 9 Pink + (128, 255, 0), # 10 Lime + (255, 255, 255) # 11 White +] + +# ============================================================ +# STATE FOR PIXELS 0-2 (buttons) +# ============================================================ + +color_index = [0, 0, 0] # Color for each strip LED +brightness = [5, 5, 5] # Brightness 0–100% +last_state = [1, 1, 1] +press_time = [0, 0, 0] + +# ============================================================ +# FLASHING SINGLE PIXEL (GP28) +# ============================================================ + +FLASH_COLOR_INDEX = 1 # Choose color from palette (0–10) +FLASH_BRIGHTNESS = 5 # 0–100% +FLASH_INTERVAL_MS = 500 # Flash speed + +flash_on = False +last_flash_time = time.ticks_ms() + +# ============================================================ +# HELPER FUNCTIONS +# ============================================================ + +def apply_brightness(color, level): + r, g, b = color + factor = level / 100 + return (int(r * factor), int(g * factor), int(b * factor)) + +def update_strip(): + for i in range(STRIP_LEDS): + strip[i] = apply_brightness(colors[color_index[i]], brightness[i]) + strip.write() + +def single_on(): + c = apply_brightness(colors[FLASH_COLOR_INDEX], FLASH_BRIGHTNESS) + single[0] = c + single.write() + +def single_off(): + single[0] = (0, 0, 0) + single.write() + +# ============================================================ +# INITIAL STATE +# ============================================================ + +update_strip() +single_off() + +class HeartbeatNeoPixel: + def __init__(self, neopixel_obj, pixel_index=0, color=(255, 0, 0), brightness=50, interval_ms=500): + self.np = neopixel_obj + self.index = pixel_index + self.color = color + self.brightness = brightness + self.interval = interval_ms + + self.state = False + self.last_time = time.ticks_ms() + + def apply_brightness(self, c): + r, g, b = c + factor = self.brightness / 100 + return (int(r * factor), int(g * factor), int(b * factor)) + + def update(self): + now = time.ticks_ms() + if time.ticks_diff(now, self.last_time) >= self.interval: + self.state = not self.state + self.last_time = now + + if self.state: + self.np[self.index] = self.apply_brightness(self.color) + else: + self.np[self.index] = (0, 0, 0) + + self.np.write() + +# heartbeat = HeartbeatNeoPixel( +# neopixel_obj=single, +# pixel_index=0, +# color=colors[FLASH_COLOR_INDEX], +# brightness=FLASH_BRIGHTNESS, +# interval_ms=FLASH_INTERVAL_MS +# ) +# ============================================================ +# MAIN LOOP +# ============================================================ + +while True: + now = time.ticks_ms() + + # ----------------------------- + # BUTTONS → control pixels 0–2 + # ----------------------------- + heartbeat.update() + + for i in range(3): + state = buttons[i].value() + + if last_state[i] == 1 and state == 0: + press_time[i] = now + + if last_state[i] == 0 and state == 1: + duration = time.ticks_diff(now, press_time[i]) + + if duration > 500: + # LONG PRESS → adjust brightness + brightness[i] += 10 + if brightness[i] > 100: + brightness[i] = 10 + else: + # SHORT PRESS → next color + color_indimport machine +import time +import neopixel + +# ============================================================ +# HEARTBEAT CLASS (NeoPixel version) +# ============================================================ + +class HeartbeatNeoPixel: + def __init__(self, neopixel_obj, pixel_index=0, color=(255, 0, 0), + brightness=50, interval_ms=500): + self.np = neopixel_obj + self.index = pixel_index + self.color = color + self.brightness = brightness + self.interval = interval_ms + + self.state = False + self.last_time = time.ticks_ms() + + def apply_brightness(self, c): + r, g, b = c + factor = self.brightness / 100 + return (int(r * factor), int(g * factor), int(b * factor)) + + def update(self): + now = time.ticks_ms() + if time.ticks_diff(now, self.last_time) >= self.interval: + self.state = not self.state + self.last_time = now + + if self.state: + self.np[self.index] = self.apply_brightness(self.color) + else: + self.np[self.index] = (0, 0, 0) + + self.np.write() + + +# ============================================================ +# NEOPIXEL SETUP +# ============================================================ + +# 3-LED strip on GP17 +STRIP_PIN = 17 +STRIP_LEDS = 3 +strip = neopixel.NeoPixel(machine.Pin(STRIP_PIN), STRIP_LEDS) + +# Single heartbeat pixel on GP28 +SINGLE_PIN = 28 +single = neopixel.NeoPixel(machine.Pin(SINGLE_PIN), 1) + +# ============================================================ +# BUTTONS (GP20, GP21, GP22) +# ============================================================ + +button_pins = [20, 21, 22] +buttons = [machine.Pin(pin, machine.Pin.IN, machine.Pin.PULL_UP) + for pin in button_pins] + +# ============================================================ +# COLOR PALETTE +# ============================================================ + +colors = [ + (255, 0, 0), # 0 Red + (0, 255, 0), # 1 Green + (0, 0, 255), # 2 Blue + (255, 255, 0), # 3 Yellow + (0, 255, 255), # 4 Cyan + (255, 0, 255), # 5 Magenta + (255, 128, 0), # 6 Orange + (128, 0, 255), # 7 Purple + (255, 20, 147), # 8 Pink + (128, 255, 0), # 9 Lime + (255, 255, 255) # 10 White +] + +# ============================================================ +# STATE FOR PIXELS 0–2 (BUTTON CONTROLLED) +# ============================================================ + +color_index = [0, 0, 0] +brightness = [50, 50, 50] +last_state = [1, 1, 1] +press_time = [0, 0, 0] + +# ============================================================ +# HEARTBEAT PIXEL CONFIG +# ============================================================ + +FLASH_COLOR_INDEX = 7 # choose from palette (0–10) +FLASH_BRIGHTNESS = 100 +FLASH_INTERVAL_MS = 500 # speed of heartbeat blink + +heartbeat = HeartbeatNeoPixel( + neopixel_obj=single, + pixel_index=0, + color=colors[FLASH_COLOR_INDEX], + brightness=FLASH_BRIGHTNESS, + interval_ms=FLASH_INTERVAL_MS +) + +# ============================================================ +# HELPER FUNCTIONS +# ============================================================ + +def apply_brightness(color, level): + r, g, b = color + factor = level / 100 + return (int(r * factor), int(g * factor), int(b * factor)) + +def update_strip(): + for i in range(STRIP_LEDS): + strip[i] = apply_brightness(colors[color_index[i]], brightness[i]) + strip.write() + +# ============================================================ +# INITIAL STATE +# ============================================================ + +update_strip() + +# ============================================================ +# MAIN LOOP +# ============================================================ + +while True: + now = time.ticks_ms() + + # -------------------------------------------- + # BUTTON HANDLING FOR PIXELS 0, 1, 2 + # -------------------------------------------- + for i in range(3): + state = buttons[i].value() + + if last_state[i] == 1 and state == 0: + press_time[i] = now + + if last_state[i] == 0 and state == 1: + duration = time.ticks_diff(now, press_time[i]) + + if duration > 500: + # LONG PRESS → brightness + brightness[i] += 10 + if brightness[i] > 100: + brightness[i] = 10 + else: + # SHORT PRESS → next color + color_index[i] = (color_index[i] + 1) % len(colors) + + update_strip() + time.sleep_ms(200) # debounce + + last_state[i] = state + + # -------------------------------------------- + # HEARTBEAT PIXEL UPDATE (GP28) + # -------------------------------------------- + heartbeat.update() + + time.sleep_ms(10) + +ex[i] = (color_index[i] + 1) % len(colors) + + update_strip() + time.sleep_ms(200) # debounce + + last_state[i] = state + + # ----------------------------- + # FLASHING SINGLE PIXEL (GP28) + # ----------------------------- +# if time.ticks_diff(now, last_flash_time) >= FLASH_INTERVAL_MS: +# flash_on = not flash_on +# last_flash_time = now +# +# if flash_on: +# single_on() +# else: +# single_off() + + + time.sleep_ms(10) + diff --git a/Maker_Pico_Box/main5.py b/Maker_Pico_Box/main5.py new file mode 100644 index 0000000..f6d02e0 --- /dev/null +++ b/Maker_Pico_Box/main5.py @@ -0,0 +1,174 @@ +import machine +import time +import neopixel + +# ============================================================ +# HEARTBEAT CLASS (smooth fading version) +# ============================================================ + +class HeartbeatFadeNeoPixel: + def __init__(self, neopixel_obj, pixel_index=0, color=(255, 0, 0), + max_brightness=100, step=2, interval_ms=20): + self.np = neopixel_obj + self.index = pixel_index + self.color = color + self.max_brightness = max_brightness + self.step = step # how fast brightness changes + self.interval = interval_ms + + self.brightness = 0 + self.direction = 1 # 1 = going up, -1 = going down + self.last_time = time.ticks_ms() + + def apply_brightness(self, c, level): + r, g, b = c + factor = level / 100 + return (int(r * factor), int(g * factor), int(b * factor)) + + def update(self): + now = time.ticks_ms() + if time.ticks_diff(now, self.last_time) >= self.interval: + self.last_time = now + + # Update brightness + self.brightness += self.direction * self.step + + # Bounce at limits + if self.brightness >= self.max_brightness: + self.brightness = self.max_brightness + self.direction = -1 + + if self.brightness <= 0: + self.brightness = 0 + self.direction = 1 + + # Apply brightness + self.np[self.index] = self.apply_brightness(self.color, self.brightness) + self.np.write() + + +# ============================================================ +# NEOPIXEL SETUP +# ============================================================ + +# 3-LED strip on GP17 +STRIP_PIN = 17 +STRIP_LEDS = 3 +strip = neopixel.NeoPixel(machine.Pin(STRIP_PIN), STRIP_LEDS) + +# Single heartbeat pixel on GP28 +SINGLE_PIN = 28 +single = neopixel.NeoPixel(machine.Pin(SINGLE_PIN), 1) + +# ============================================================ +# BUTTONS (GP20, GP21, GP22) +# ============================================================ + +button_pins = [20, 21, 22] +buttons = [machine.Pin(pin, machine.Pin.IN, machine.Pin.PULL_UP) + for pin in button_pins] + +# ============================================================ +# COLOR PALETTE +# ============================================================ + +colors = [ + (0, 0, 0), # Off + (255, 0, 0), # Red + (0, 255, 0), # Green + (0, 0, 255), # Blue + (255, 255, 0), # Yellow + (0, 255, 255), # Cyan + (255, 0, 255), # Magenta + (255, 128, 0), # Orange + (128, 0, 255), # Purple + (255, 20, 147), # Pink + (128, 255, 0), # Lime + (255, 255, 255), # White +] + +# ============================================================ +# STATE FOR PIXELS 0–2 (BUTTON-CONTROLLED) +# ============================================================ + +color_index = [0, 0, 0] +brightness = [50, 50, 50] +last_state = [1, 1, 1] +press_time = [0, 0, 0] + +# ============================================================ +# HEARTBEAT PIXEL CONFIG (smooth fade) +# ============================================================ + +FLASH_COLOR_INDEX = 1 # Choose from palette +FLASH_MAX_BRIGHTNESS = 20 # Peak brightness for fade +FLASH_STEP = 0.5 # Speed of fade +FLASH_INTERVAL_MS = 30 # Lower = smoother animation + +heartbeat = HeartbeatFadeNeoPixel( + neopixel_obj=single, + pixel_index=0, + color=colors[FLASH_COLOR_INDEX], + max_brightness=FLASH_MAX_BRIGHTNESS, + step=FLASH_STEP, + interval_ms=FLASH_INTERVAL_MS +) + +# ============================================================ +# HELPERS +# ============================================================ + +def apply_brightness(color, level): + r, g, b = color + factor = level / 100 + return (int(r * factor), int(g * factor), int(b * factor)) + +def update_strip(): + for i in range(STRIP_LEDS): + strip[i] = apply_brightness(colors[color_index[i]], brightness[i]) + strip.write() + +# ============================================================ +# INITIAL STATE +# ============================================================ + +update_strip() + +# ============================================================ +# MAIN LOOP +# ============================================================ + +while True: + now = time.ticks_ms() + + # -------------------------------- + # BUTTON HANDLING FOR 3-LED STRIP + # -------------------------------- + for i in range(3): + state = buttons[i].value() + + if last_state[i] == 1 and state == 0: + press_time[i] = now + + if last_state[i] == 0 and state == 1: + duration = time.ticks_diff(now, press_time[i]) + + if duration > 500: + brightness[i] += 10 + if brightness[i] > 100: + brightness[i] = 10 + else: + color_index[i] = (color_index[i] + 1) % len(colors) + + update_strip() + time.sleep_ms(200) + + last_state[i] = state + + # ---------------------------- + # SMOOTH HEARTBEAT UPDATE + # ---------------------------- + heartbeat.update() + + time.sleep_ms(10) + diff --git a/Maker_Pico_Box/main_old.py b/Maker_Pico_Box/main_old.py new file mode 100644 index 0000000..8bda377 --- /dev/null +++ b/Maker_Pico_Box/main_old.py @@ -0,0 +1,46 @@ +import machine +import neopixel +import time + +# NeoPixel setup +NUM_LEDS = 8 +np = neopixel.NeoPixel(machine.Pin(17), NUM_LEDS) # 28 + +# Button on GP22 (active LOW) +button = machine.Pin(22, machine.Pin.IN, machine.Pin.PULL_UP) + +# Color list (R, G, B) +colors = [ + (255, 0, 0), # Red + (0, 255, 0), # Green + (0, 0, 255), # Blue + (255, 255, 0), # Yellow + (255, 0, 255), # Magenta + (0, 255, 255), # Cyan + (255, 255, 255) # White +] + +current_color = 0 +last_state = 1 # button not pressed + +def show_color(color): + for i in range(NUM_LEDS): + np[i] = color + np.write() + +# Initial color +show_color(colors[current_color]) + +while True: + state = button.value() + + # Detect button press (HIGH → LOW) + if last_state == 1 and state == 0: + current_color = (current_color + 1) % len(colors) + show_color(colors[current_color]) + time.sleep_ms(200) # Debounce delay + + last_state = state + time.sleep_ms(10) + + diff --git a/Maker_Pico_Box/rainbow.py b/Maker_Pico_Box/rainbow.py new file mode 100644 index 0000000..ced3cee --- /dev/null +++ b/Maker_Pico_Box/rainbow.py @@ -0,0 +1,29 @@ +import machine +import neopixel +import time + +# GP28 pin and number of LEDs +pin = 28 +num_leds = 8 # change to how many LEDs you have + +np = neopixel.NeoPixel(machine.Pin(pin), num_leds) + +# Simple color helper +def set_color(r, g, b): + for i in range(num_leds): + np[i] = (r, g, b) + np.write() + +# Demo loop +while True: + set_color(255, 0, 0) # Red + time.sleep(1) + + set_color(0, 255, 0) # Green + time.sleep(1) + + set_color(0, 0, 255) # Blue + time.sleep(1) + + set_color(0, 0, 0) # Off + time.sleep(1) diff --git a/calibrate.py b/calibrate.py new file mode 100644 index 0000000..6543e8f --- /dev/null +++ b/calibrate.py @@ -0,0 +1,107 @@ +import time +import ntptime +import json +import os +from hx710b import HX710B # Correct driver +from config import * + +CALIB_FILE = "calibration.cfg" + +tz_offset = 0 +utc = time.localtime() +local = time.localtime(time.mktime(utc) + tz_offset * 3600) +ctime = f"{local[0]:02}-{local[1]:02}-{local[2]:02} {local[3]:02}:{local[4]:02}:{local[5]:02}" +#print("Time =", local) +print("Time =", ctime) + +# --------------------------------------------------------- +# USER INPUT +# --------------------------------------------------------- +# Water depth (meters) +# KNOWN_DEPTH_M = 1.680 + +# Pressure created by water column (kPa) +KNOWN_PRESSURE_KPA = KNOWN_DEPTH_M * 9.81 + +print( + "\nUsing depth {:.3f} m → expected pressure {:.3f} kPa".format( + KNOWN_DEPTH_M, KNOWN_PRESSURE_KPA + ) +) + +# --------------------------------------------------------- +# SAVE CALIBRATION +# --------------------------------------------------------- +def save_calibration(ctime, zero_offset, raw_at_pressure, scale_factor): + data = { + "zero_offset": zero_offset, + "raw_at_pressure": raw_at_pressure, + "scale_factor": scale_factor, + "Date_Time": ctime + } + + with open(CALIB_FILE, "w") as f: + json.dump(data, f) + + print("\n✔ Saved calibration:") + print(" Date_Time =", ctime) + print(" zero_offset =", zero_offset) + print(" raw_at_pressure =", raw_at_pressure) + print(" scale_factor =", scale_factor) + + +# --------------------------------------------------------- +# RAW AVERAGING +# --------------------------------------------------------- +def read_average(hx, samples=30): + total = 0 + for _ in range(samples): + total += hx.read_raw() + time.sleep_ms(5) + return total / samples + + +# --------------------------------------------------------- +# CALIBRATION PROCESS +# --------------------------------------------------------- +print("\n=== HX710B Depth-Based Calibration ===") + +hx = HX710B(dout_pin=0, sck_pin=1) + +# Step 1 — ZERO PRESSURE +print("\n➡ Ensure tank is OPEN TO AIR (0 kPa)...") +time.sleep(3) + +zero_offset = read_average(hx) +print("Zero offset captured:", zero_offset) + +# Step 2 — APPLY KNOWN PRESSURE +print( + "\n➡ Now apply EXACTLY {:.3f} kPa (depth {:.3f} m)...".format( + KNOWN_PRESSURE_KPA, KNOWN_DEPTH_M + ) +) +time.sleep(10) + +raw_at_pressure = read_average(hx) +print("Raw at known depth:", raw_at_pressure) + +# Compute scale factor +scale_factor = KNOWN_PRESSURE_KPA / (raw_at_pressure - zero_offset) + +# Save calibration (INCLUDING raw value) +save_calibration(ctime, zero_offset, raw_at_pressure, scale_factor) + +print("\n=== Calibration Complete ===") + +# --------------------------------------------------------- +# VERIFY +# --------------------------------------------------------- +hx.zero_offset = zero_offset +hx.scale_factor = scale_factor + +test = hx.read_kpa() + +print("\nVerification reading:", test, "kPa") +print("Expected:", KNOWN_PRESSURE_KPA, "kPa") +print("\nDone.\n") diff --git a/calibration.cfg b/calibration.cfg new file mode 100644 index 0000000..4c099f7 --- /dev/null +++ b/calibration.cfg @@ -0,0 +1 @@ +{"Date_Time": "2026-01-18 15:17:40", "zero_offset": -2477692.0, "raw_at_pressure": 7415596.0, "scale_factor": 1.427877e-06} \ No newline at end of file diff --git a/config.py b/config.py new file mode 100644 index 0000000..8a124c5 --- /dev/null +++ b/config.py @@ -0,0 +1,21 @@ +SSID = "WORKSHOP" +PASSWORD = "hellothere" +HOSTNAME = "TANK-MONITOR" +DISPLAY_ON = False +HEARTBEAT = True +AVERAGE = True +VERSION = "20260123B" +CPU_FREQ = 250_000_000 +TZ_OFFSET = 2 + +MAX_CONNECTIONS = 5 + +MIN_PRESSURE_KPA = 0.0 +#MAX_PRESSURE_KPA = 13.72 +#MAX_PRESSURE_KPA = 50.13 +MAX_DEPTH_M = 1.44 + +#KNOWN_DEPTH_M = 1.680 +KNOWN_DEPTH_M = MAX_DEPTH_M +MAX_PRESSURE_KPA = KNOWN_DEPTH_M * 9.81 +TANK_CAPACITY_L = 1800.00 \ No newline at end of file diff --git a/display.py b/display.py new file mode 100644 index 0000000..54bda0d --- /dev/null +++ b/display.py @@ -0,0 +1,53 @@ +from machine import I2C, Pin +import ssd1306 +import framebuf + +class OLED: + def __init__(self): + i2c = I2C(1, scl=Pin(3), sda=Pin(2), freq=400_000) + + self.width = 128 + self.height = 64 + + # Real OLED + self.oled = ssd1306.SSD1306_I2C(self.width, self.height, i2c) + + # Off-screen buffer (normal orientation) + self.buf = bytearray(self.width * self.height // 8) + self.fb = framebuf.FrameBuffer(self.buf, self.width, self.height, framebuf.MONO_VLSB) + + # -------------------------------------------------- + # Rotate framebuffer 180° + # -------------------------------------------------- + def rotate_180(self): + self.oled.fill(0) + + for y in range(self.height): + for x in range(self.width): + if self.fb.pixel(x, y): + self.oled.pixel( + self.width - 1 - x, + self.height - 1 - y, + 1 + ) + + # -------------------------------------------------- + # Display content + # -------------------------------------------------- + def show(self, rtc, pressure, percent, cpu_temp, rssi_percent, depth_mm): + dt = rtc.datetime() + ctime = f"{dt[4]:02}:{dt[5]:02}:{dt[6]:02}" + + # Draw NORMAL orientation into framebuffer + self.fb.fill(0) + + self.fb.text(ctime, 30, 0) + self.fb.text(f"P: {pressure:.2f} kPa", 0, 10) + self.fb.text(f"D: {depth_mm:.0f} mm", 0, 20) + self.fb.text(f"L: {percent:.2f} %", 0, 30) + self.fb.text(f"CPU: {cpu_temp:.1f} C", 0, 40) + self.fb.text(f"WiFi: {rssi_percent}%", 0, 50) + + # Rotate and display + self.rotate_180() + self.oled.show() diff --git a/html/app.js b/html/app.js new file mode 100644 index 0000000..4dc680b --- /dev/null +++ b/html/app.js @@ -0,0 +1,153 @@ +// ========================================================= +// Tank Monitor - app.js +// ========================================================= + +const TANK_FULL_PASSWORD = "1234"; // 🔐 CHANGE THIS + +// =============================== +// Fetch sensor data and update UI +// =============================== +async function fetchSensorData() { + try { + const response = await fetch("/sensor"); + const data = await response.json(); + + // Level + const level_mm = data.pressure / 9.810; + document.getElementById("level").innerText = + level_mm.toFixed(3) + " m"; + + + // Pressure + document.getElementById("pressure").innerText = + data.pressure.toFixed(3) + " Kpa"; + + // Tank % + Liters (robust) + const pct = data.tank_percent; + const capacity = Number(TANK_CAPACITY_L); + + const percentEl = document.getElementById("percent"); + + if (!isNaN(capacity) && capacity > 0) { + const liters = (pct / 100) * capacity; + percentEl.innerText = + `${pct.toFixed(1)} % [${liters.toFixed(0)} L]`; + } else { + percentEl.innerText = + `${pct.toFixed(1)} %`; + } + + // CPU temp + document.getElementById("cpu").innerText = + data.cpu_temp.toFixed(1) + " °C"; + + // RSSI: dBm → % + let rssiPercent = (data.rssi + 100) * 2; + rssiPercent = Math.max(0, Math.min(100, rssiPercent)); + document.getElementById("rssi").innerText = + rssiPercent.toFixed(0) + " %"; + + // Free memory → KB + document.getElementById("mem").innerText = + (data.mem / 1024).toFixed(1) + " KB"; + + // Uptime + document.getElementById("uptime").innerText = + data.uptime; + + updateTankBar(pct); + + // Change footer on html page + // data.footer = "Powered by Raspberry Pi Pico W & HX710B - Test" + document.getElementById("footer-text").innerText = data.footer; + + + } catch (err) { + console.log("Sensor fetch error:", err); + } +} + +// =============================== +// Tank animation + color scaling +// =============================== +function updateTankBar(percent) { + const fill = document.getElementById("tank-fill"); + + percent = Math.max(0, Math.min(100, percent)); + fill.style.height = percent + "%"; + fill.style.backgroundColor = tankColor(percent); +} + +function tankColor(p) { + let r, g, b; + + if (p <= 50) { + const t = p / 50; + r = 211 + (251 - 211) * t; + g = 50 + (192 - 50) * t; + b = 47 + (45 - 47) * t; + } else { + const t = (p - 50) / 50; + r = 251 + (76 - 251) * t; + g = 192 + (175 - 192) * t; + b = 45 + (80 - 45) * t; + } + + return `rgb(${Math.round(r)}, ${Math.round(g)}, ${Math.round(b)})`; +} + +// =============================== +// Date & Time +// =============================== +function updateClock() { + const now = new Date(); + + document.getElementById("date").innerText = + `${now.getFullYear()}-${String(now.getMonth()+1).padStart(2,"0")}-${String(now.getDate()).padStart(2,"0")}`; + + document.getElementById("time").innerText = + `${String(now.getHours()).padStart(2,"0")}:${String(now.getMinutes()).padStart(2,"0")}:${String(now.getSeconds()).padStart(2,"0")}`; +} + +// =============================== +// Theme toggle +// =============================== +const modeSwitch = document.getElementById("modeSwitch"); + +if (localStorage.getItem("theme") === "dark") { + document.body.classList.add("dark"); + if (modeSwitch) modeSwitch.checked = true; +} + +if (modeSwitch) { + modeSwitch.addEventListener("change", () => { + document.body.classList.toggle("dark", modeSwitch.checked); + localStorage.setItem("theme", modeSwitch.checked ? "dark" : "light"); + }); +} + +// =============================== +// Password-protected Tank Full +// =============================== +function tankFullProtected() { + const entered = prompt("Enter password to set Tank FULL:"); + if (entered !== TANK_FULL_PASSWORD) { + alert("❌ Incorrect password"); + return; + } + + if (!confirm("Confirm: Set tank to FULL?")) return; + + fetch("/tare"); +} + +window.tankFullProtected = tankFullProtected; + +// =============================== +// Start +// =============================== +setInterval(fetchSensorData, 1000); +setInterval(updateClock, 1000); + +fetchSensorData(); +updateClock(); diff --git a/html/favicon.ico b/html/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..69bcc7f139507899eb866009dbc1ff9193c4171f GIT binary patch literal 15406 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 m%DJXL$+?oB&=&m!a|~PgEN(i|P3%6B + + + + Tank Monitor + + + + + + + + + +
+ Tank Monitor + +
+ +
+ + +
+

System Stats

+ +
+ +
+

Date: --

+

Time: --

+

Uptime: --

+

Level: --

+

Pressure: --

+

Tank Level: --

+

CPU Temp: --

+

WiFi RSSI: --

+

Free Memory: --

+
+ +
+
+
+ +
+
+ + +
+

Actions

+ +
+ + + +
+
+ +
+ + + + + + diff --git a/hx710b.py b/hx710b.py new file mode 100644 index 0000000..3272ebc --- /dev/null +++ b/hx710b.py @@ -0,0 +1,116 @@ +from machine import Pin +import time +import json +import os + +CALIB_FILE = "calibration.cfg" + + +# --------------------------------------------------------- +# Calibration File Helpers +# --------------------------------------------------------- + +def save_calibration(zero_offset, scale_factor): + """Save calibration values to internal flash.""" + data = { + "zero_offset": zero_offset, + "scale_factor": scale_factor + } + with open(CALIB_FILE, "w") as f: + json.dump(data, f) + print("✔ Calibration saved.") + + +def load_calibration(): + """Load existing calibration or return defaults.""" + if CALIB_FILE not in os.listdir(): + print("⚠ No calibration file found, using defaults.") + return 0, 1 + + try: + with open(CALIB_FILE, "r") as f: + data = json.load(f) + print("✔ Calibration loaded.") + return data.get("zero_offset", 0), data.get("scale_factor", 1) + except Exception as e: + print("⚠ Calibration load error:", e) + return 0, 1 + + +# --------------------------------------------------------- +# HX710B Class +# --------------------------------------------------------- + +class HX710B: + def __init__(self, dout_pin, sck_pin): + self.dout = Pin(dout_pin, Pin.IN) + self.sck = Pin(sck_pin, Pin.OUT) + self.sck.value(0) + + # Load previous calibration (or defaults) + self.zero_offset, self.scale_factor = load_calibration() + + # ----------------------------------------------------- + # Low-level raw ADC read + # ----------------------------------------------------- + def read_raw(self): + """Read the 24-bit raw ADC value.""" + while self.dout.value(): + pass # wait for data ready + + count = 0 + for _ in range(24): + self.sck.value(1) + count = (count << 1) | self.dout.value() + self.sck.value(0) + + # 25th pulse to set gain for next reading + self.sck.value(1) + self.sck.value(0) + + # Signed 24-bit conversion + if count & 0x800000: + count -= 0x1000000 + + return count + + # ----------------------------------------------------- + # Zero Calibration + # ----------------------------------------------------- + def calibrate_zero(self, samples=20): + """Measure 0 kPa and determine zero offset.""" + total = 0 + for _ in range(samples): + total += self.read_raw() + time.sleep_ms(5) + + self.zero_offset = total / samples + save_calibration(self.zero_offset, self.scale_factor) + + print("✔ Zero calibrated:", self.zero_offset) + return self.zero_offset + + # ----------------------------------------------------- + # Full-scale / Known Pressure Calibration + # ----------------------------------------------------- + def calibrate_full(self, known_kpa, samples=20): + """Measure raw ADC at known pressure and compute scaling.""" + total = 0 + for _ in range(samples): + total += self.read_raw() + time.sleep_ms(5) + + raw_avg = total / samples + self.scale_factor = known_kpa / (raw_avg - self.zero_offset) + + save_calibration(self.zero_offset, self.scale_factor) + + print("✔ Scale calibrated:", self.scale_factor) + return self.scale_factor + + # ----------------------------------------------------- + # Convert raw → kPa using saved calibration + # ----------------------------------------------------- + def read_kpa(self): + raw = self.read_raw() + return (raw - self.zero_offset) * self.scale_factor diff --git a/lib/hx710b.py b/lib/hx710b.py new file mode 100644 index 0000000..3272ebc --- /dev/null +++ b/lib/hx710b.py @@ -0,0 +1,116 @@ +from machine import Pin +import time +import json +import os + +CALIB_FILE = "calibration.cfg" + + +# --------------------------------------------------------- +# Calibration File Helpers +# --------------------------------------------------------- + +def save_calibration(zero_offset, scale_factor): + """Save calibration values to internal flash.""" + data = { + "zero_offset": zero_offset, + "scale_factor": scale_factor + } + with open(CALIB_FILE, "w") as f: + json.dump(data, f) + print("✔ Calibration saved.") + + +def load_calibration(): + """Load existing calibration or return defaults.""" + if CALIB_FILE not in os.listdir(): + print("⚠ No calibration file found, using defaults.") + return 0, 1 + + try: + with open(CALIB_FILE, "r") as f: + data = json.load(f) + print("✔ Calibration loaded.") + return data.get("zero_offset", 0), data.get("scale_factor", 1) + except Exception as e: + print("⚠ Calibration load error:", e) + return 0, 1 + + +# --------------------------------------------------------- +# HX710B Class +# --------------------------------------------------------- + +class HX710B: + def __init__(self, dout_pin, sck_pin): + self.dout = Pin(dout_pin, Pin.IN) + self.sck = Pin(sck_pin, Pin.OUT) + self.sck.value(0) + + # Load previous calibration (or defaults) + self.zero_offset, self.scale_factor = load_calibration() + + # ----------------------------------------------------- + # Low-level raw ADC read + # ----------------------------------------------------- + def read_raw(self): + """Read the 24-bit raw ADC value.""" + while self.dout.value(): + pass # wait for data ready + + count = 0 + for _ in range(24): + self.sck.value(1) + count = (count << 1) | self.dout.value() + self.sck.value(0) + + # 25th pulse to set gain for next reading + self.sck.value(1) + self.sck.value(0) + + # Signed 24-bit conversion + if count & 0x800000: + count -= 0x1000000 + + return count + + # ----------------------------------------------------- + # Zero Calibration + # ----------------------------------------------------- + def calibrate_zero(self, samples=20): + """Measure 0 kPa and determine zero offset.""" + total = 0 + for _ in range(samples): + total += self.read_raw() + time.sleep_ms(5) + + self.zero_offset = total / samples + save_calibration(self.zero_offset, self.scale_factor) + + print("✔ Zero calibrated:", self.zero_offset) + return self.zero_offset + + # ----------------------------------------------------- + # Full-scale / Known Pressure Calibration + # ----------------------------------------------------- + def calibrate_full(self, known_kpa, samples=20): + """Measure raw ADC at known pressure and compute scaling.""" + total = 0 + for _ in range(samples): + total += self.read_raw() + time.sleep_ms(5) + + raw_avg = total / samples + self.scale_factor = known_kpa / (raw_avg - self.zero_offset) + + save_calibration(self.zero_offset, self.scale_factor) + + print("✔ Scale calibrated:", self.scale_factor) + return self.scale_factor + + # ----------------------------------------------------- + # Convert raw → kPa using saved calibration + # ----------------------------------------------------- + def read_kpa(self): + raw = self.read_raw() + return (raw - self.zero_offset) * self.scale_factor diff --git a/lib/ssd1306.py b/lib/ssd1306.py new file mode 100644 index 0000000..976f823 --- /dev/null +++ b/lib/ssd1306.py @@ -0,0 +1,89 @@ +# ssd1306.py +# MicroPython SSD1306 OLED driver (I2C) +# Compatible with 128x64 and 128x32 displays + +from machine import I2C, Pin +import framebuf +import time + +# SSD1306 commands +SET_CONTRAST = 0x81 +DISPLAY_ALL_ON_RESUME = 0xA4 +DISPLAY_ALL_ON = 0xA5 +NORMAL_DISPLAY = 0xA6 +INVERT_DISPLAY = 0xA7 +DISPLAY_OFF = 0xAE +DISPLAY_ON = 0xAF +SET_DISPLAY_OFFSET = 0xD3 +SET_COM_PINS = 0xDA +SET_VCOM_DETECT = 0xDB +SET_DISPLAY_CLOCK_DIV = 0xD5 +SET_PRECHARGE = 0xD9 +SET_MULTIPLEX = 0xA8 +SET_LOW_COLUMN = 0x00 +SET_HIGH_COLUMN = 0x10 +SET_START_LINE = 0x40 +MEMORY_MODE = 0x20 +COLUMN_ADDR = 0x21 +PAGE_ADDR = 0x22 +COM_SCAN_INC = 0xC0 +COM_SCAN_DEC = 0xC8 +SEG_REMAP = 0xA0 +CHARGE_PUMP = 0x8D +EXTERNAL_VCC = 0x1 +SWITCH_CAP_VCC = 0x2 + +class SSD1306_I2C(framebuf.FrameBuffer): + def __init__(self, width, height, i2c, addr=0x3c): + self.width = width + self.height = height + self.i2c = i2c + self.addr = addr + self.pages = self.height // 8 + self.buffer = bytearray(self.pages * self.width) + super().__init__(self.buffer, self.width, self.height, framebuf.MONO_VLSB) + self.init_display() + + def write_cmd(self, cmd): + self.i2c.writeto(self.addr, bytearray([0x00, cmd])) + + def init_display(self): + for cmd in ( + DISPLAY_OFF, + SET_DISPLAY_CLOCK_DIV, 0x80, + SET_MULTIPLEX, self.height - 1, + SET_DISPLAY_OFFSET, 0x00, + SET_START_LINE | 0x00, + CHARGE_PUMP, 0x14, + MEMORY_MODE, 0x00, + SEG_REMAP | 0x1, + COM_SCAN_DEC, + SET_COM_PINS, 0x12 if self.height == 64 else 0x02, + SET_CONTRAST, 0xCF, + SET_PRECHARGE, 0xF1, + SET_VCOM_DETECT, 0x40, + DISPLAY_ALL_ON_RESUME, + NORMAL_DISPLAY, + DISPLAY_ON + ): + self.write_cmd(cmd) + self.fill(0) + self.show() + + def show(self): + for page in range(self.pages): + self.write_cmd(0xB0 + page) + self.write_cmd(SET_LOW_COLUMN) + self.write_cmd(SET_HIGH_COLUMN) + start = self.width * page + end = start + self.width + self.i2c.writeto(self.addr, b'\x40' + self.buffer[start:end]) + + def poweroff(self): + self.write_cmd(DISPLAY_OFF) + + def poweron(self): + self.write_cmd(DISPLAY_ON) + + def invert(self, invert): + self.write_cmd(INVERT_DISPLAY if invert else NORMAL_DISPLAY) diff --git a/main.py b/main.py new file mode 100644 index 0000000..8d876fe --- /dev/null +++ b/main.py @@ -0,0 +1,238 @@ +import time +import uasyncio as asyncio +import machine +import gc +import neopixel + +from config import * +from wifi import connect_wifi +from timeutil import setup_rtc +from sensors import Sensors +from display import OLED +from webserver import WebServer + + +# Set CPU speed +machine.freq(CPU_FREQ) + +#HOSTNAME = "Testname" + +# Connect WiFi +print("Version = ", VERSION) +print("Hostname = ", HOSTNAME) +wlan = connect_wifi(SSID, PASSWORD, HOSTNAME) +rtc = setup_rtc(TZ_OFFSET) +print("Server running on http://" + wlan.ifconfig()[0]) + +#TANK_CAPACITY_L = 3000 +print("Heart Beat =", HEARTBEAT) +print("Tank Volume =", TANK_CAPACITY_L) +# Init sensors and display +sensors = Sensors() +if DISPLAY_ON: + oled = OLED() + +sensors.test_pin1.on() +sensors.test_pin2.on() +sensors.test_pin0.on() +# Capture boot time +start_time = time.time() + +# print("Boot timestamp:", start_time) + +# Set tank full reference from current reading at boot + +#MAX_PRESSURE_KPA = sensors.read_pressure() +time.sleep_ms(250) +sensors.test_pin1.off() +sensors.test_pin2.off() +sensors.test_pin0.off() + +print("✔ Config loaded.") +print("Initial MAX_PRESSURE_KPA =", MAX_PRESSURE_KPA, "KPA") +print("Initial Known Depth =", KNOWN_DEPTH_M, "m") +#print("Initial Known Pressure =", MAX_PRESSURE_KPA) + +def rssi_percent(): + rssi = wlan.status("rssi") + rssi = max(-100, min(-50, rssi)) + return int((rssi + 100) * 2) + +class HeartbeatFadeNeoPixel: + def __init__(self, neopixel_obj, pixel_index=0, color=(255, 0, 0), + min_brightness=5, max_brightness=100, + step=2, interval_ms=20): + + self.np = neopixel_obj + self.index = pixel_index + self.color = color + + self.min_brightness = min_brightness + self.max_brightness = max_brightness + self.step = step + self.interval = interval_ms + + self.brightness = min_brightness + self.direction = 1 # 1 = fade up, -1 = fade down + self.last_time = time.ticks_ms() + + def apply_brightness(self, c, level): + r, g, b = c + factor = level / 100 + return (int(r * factor), int(g * factor), int(b * factor)) + + def update(self): + now = time.ticks_ms() + if time.ticks_diff(now, self.last_time) >= self.interval: + self.last_time = now + + # Update brightness + self.brightness += self.direction * self.step + + # Bounce at limits + if self.brightness >= self.max_brightness: + self.brightness = self.max_brightness + self.direction = -1 + + if self.brightness <= self.min_brightness: + self.brightness = self.min_brightness + self.direction = 1 + + # Apply brightness + self.np[self.index] = self.apply_brightness(self.color, self.brightness) + self.np.write() + + +# ============================================================ +# NEOPIXEL SETUP +# ============================================================ + +STRIP_PIN = 17 +STRIP_LEDS = 3 +strip = neopixel.NeoPixel(machine.Pin(STRIP_PIN), STRIP_LEDS) + +SINGLE_PIN = 28 +single = neopixel.NeoPixel(machine.Pin(SINGLE_PIN), 1) + +# ============================================================ +# BUTTONS +# ============================================================ + +button_pins = [20, 21, 22] +buttons = [machine.Pin(pin, machine.Pin.IN, machine.Pin.PULL_UP) for pin in button_pins] + +# ============================================================ +# COLOR PALETTE +# ============================================================ + +colors = [ + (0, 0, 0), # Off + (255, 0, 0), # Red + (0, 255, 0), # Green + (0, 0, 255), # Blue + (255, 255, 255), # White +] + +# ============================================================ +# STRIP STATE +# ============================================================ + +color_index = [0, 0, 0] +brightness = [50, 50, 50] +last_state = [1, 1, 1] +press_time = [0, 0, 0] + +# ============================================================ +# HEARTBEAT CONFIG (NOW HAS MIN + MAX) +# ============================================================ + +FLASH_COLOR_INDEX = 1 # Palette color +FLASH_MIN = 1 # << lowest brightness (0–100) +FLASH_MAX = 10 # << highest brightness +FLASH_STEP = 0.2 # fade speed +FLASH_INTERVAL_MS = 20 # smoothness + +heartbeat = HeartbeatFadeNeoPixel( + neopixel_obj=single, + pixel_index=0, + color=colors[FLASH_COLOR_INDEX], + min_brightness=FLASH_MIN, + max_brightness=FLASH_MAX, + step=FLASH_STEP, + interval_ms=FLASH_INTERVAL_MS +) + +# ============================================================ +# HELPERS +# ============================================================ + +def apply_brightness(color, level): + r, g, b = color + factor = level / 100 + return (int(r * factor), int(g * factor), int(b * factor)) + +def update_strip(): + for i in range(STRIP_LEDS): + strip[i] = apply_brightness(colors[color_index[i]], brightness[i]) + strip.write() + +# ============================================================ +# INITIAL +# ============================================================ + +update_strip() + +async def sensor_task(): + global MAX_PRESSURE_KPA + while True: + gc.collect() + sensors.check_buttons() +# heartbeat.update() + if AVERAGE: + pressure = sensors.read_pressure_avg() + else: + pressure = sensors.read_pressure() + #sensors.test_pin1.on() + if sensors.tank_full: + sensors.tank_full = False + MAX_PRESSURE_KPA = sensors.read_pressure() + + if HEARTBEAT: +# sensors.test_pin2.on() +# time.sleep_ms(100) +# sensors.test_pin2.off() +# color_index[i] = 0 + i=0 + color_index[i] = (color_index[i] + 1) % len(colors) + update_strip() +# time.sleep_ms(100) + + else: +# sensors.test_pin2.off() +# color_index[i] = 3 + update_strip() +# time.sleep_ms(100) + + percent = pressure / MAX_PRESSURE_KPA + depth_mm = percent * MAX_DEPTH_M * 1000 + sensors.tank_percentage = percent * 100 + + if DISPLAY_ON: + oled.show( + rtc, + pressure, + sensors.tank_percentage, + sensors.cpu_temp(), + rssi_percent(), + depth_mm + ) + await asyncio.sleep(1) + +async def main(): + asyncio.create_task(sensor_task()) + server = WebServer(wlan, sensors, start_time, MAX_CONNECTIONS) + srv = await asyncio.start_server(server.handle, "0.0.0.0", 80) + async with srv: + await asyncio.Event().wait() + +asyncio.run(main()) \ No newline at end of file diff --git a/reboot.py b/reboot.py new file mode 100644 index 0000000..b6cbf42 --- /dev/null +++ b/reboot.py @@ -0,0 +1,2 @@ +import machine +machine.reset() diff --git a/scan-i2c.py b/scan-i2c.py new file mode 100644 index 0000000..141d972 --- /dev/null +++ b/scan-i2c.py @@ -0,0 +1,30 @@ +from machine import Pin, I2C +import time + +# Scan I2C0 on GP0=SDA, GP1=SCL +i2c0 = I2C(0, scl=Pin(1), sda=Pin(0), freq=400000) + +# Scan I2C1 on GP2=SDA, GP3=SCL +i2c1 = I2C(1, scl=Pin(3), sda=Pin(2), freq=400000) + +while True: + print("Scanning I2C buses...") + devices0 = i2c0.scan() + devices1 = i2c1.scan() + + if devices0: + print("I2C0 Devices found:") + for d in devices0: + print(" - Address: 0x{:02X}".format(d)) + else: + print("No devices found on I2C0") + + if devices1: + print("I2C1 Devices found:") + for d in devices1: + print(" - Address: 0x{:02X}".format(d)) + else: + print("No devices found on I2C1") + + print("-----------------------------") + time.sleep(3) diff --git a/sensors.py b/sensors.py new file mode 100644 index 0000000..f5adc3d --- /dev/null +++ b/sensors.py @@ -0,0 +1,57 @@ +from machine import Pin, ADC +import time +from hx710b import HX710B + +class Sensors: + def __init__(self): + self.hx = HX710B(dout_pin=0, sck_pin=1) + + print("Using calibration:", self.hx.zero_offset, self.hx.scale_factor) + + self.pressure = 0.0 + self.tank_full_kpa = 13.72 + self.tank_full = False + self.tank_percent = 0.0 + + self.tare_btn = Pin(9, Pin.IN, Pin.PULL_UP) + self._tare_lock = False + + self.test_pin0 = Pin(20, Pin.OUT) # Green + self.test_pin1 = Pin(21, Pin.OUT) # Blue + self.test_pin2 = Pin(22, Pin.OUT) + + self.cpu_adc = ADC(4) + + def read_pressure(self): + #self.test_pin2.on() + self.pressure = self.hx.read_kpa() + #self.test_pin2.off() + return self.pressure + + def read_pressure_avg(self, alpha=0.2): + new = self.hx.read_kpa() + + if not hasattr(self, "pressure"): + self.pressure = new + else: + self.pressure = (alpha * new) + ((1 - alpha) * self.pressure) + + return self.pressure + + def set_tank_full(self): + self.tank_full_kpa = self.pressure + self.tank_full = True + self.tank_percent = 100.0 + print("Tank FULL set at", self.tank_full_kpa) + + def check_buttons(self): + if self.tare_btn.value() == 0 and not self._tare_lock: + self._tare_lock = True + time.sleep_ms(200) + self.set_tank_full() + elif self.tare_btn.value() == 1: + self._tare_lock = False + + def cpu_temp(self): + v = self.cpu_adc.read_u16() * 3.3 / 65535 + return 27 - (v - 0.706) / 0.001721 \ No newline at end of file diff --git a/ssd1306.py b/ssd1306.py new file mode 100644 index 0000000..976f823 --- /dev/null +++ b/ssd1306.py @@ -0,0 +1,89 @@ +# ssd1306.py +# MicroPython SSD1306 OLED driver (I2C) +# Compatible with 128x64 and 128x32 displays + +from machine import I2C, Pin +import framebuf +import time + +# SSD1306 commands +SET_CONTRAST = 0x81 +DISPLAY_ALL_ON_RESUME = 0xA4 +DISPLAY_ALL_ON = 0xA5 +NORMAL_DISPLAY = 0xA6 +INVERT_DISPLAY = 0xA7 +DISPLAY_OFF = 0xAE +DISPLAY_ON = 0xAF +SET_DISPLAY_OFFSET = 0xD3 +SET_COM_PINS = 0xDA +SET_VCOM_DETECT = 0xDB +SET_DISPLAY_CLOCK_DIV = 0xD5 +SET_PRECHARGE = 0xD9 +SET_MULTIPLEX = 0xA8 +SET_LOW_COLUMN = 0x00 +SET_HIGH_COLUMN = 0x10 +SET_START_LINE = 0x40 +MEMORY_MODE = 0x20 +COLUMN_ADDR = 0x21 +PAGE_ADDR = 0x22 +COM_SCAN_INC = 0xC0 +COM_SCAN_DEC = 0xC8 +SEG_REMAP = 0xA0 +CHARGE_PUMP = 0x8D +EXTERNAL_VCC = 0x1 +SWITCH_CAP_VCC = 0x2 + +class SSD1306_I2C(framebuf.FrameBuffer): + def __init__(self, width, height, i2c, addr=0x3c): + self.width = width + self.height = height + self.i2c = i2c + self.addr = addr + self.pages = self.height // 8 + self.buffer = bytearray(self.pages * self.width) + super().__init__(self.buffer, self.width, self.height, framebuf.MONO_VLSB) + self.init_display() + + def write_cmd(self, cmd): + self.i2c.writeto(self.addr, bytearray([0x00, cmd])) + + def init_display(self): + for cmd in ( + DISPLAY_OFF, + SET_DISPLAY_CLOCK_DIV, 0x80, + SET_MULTIPLEX, self.height - 1, + SET_DISPLAY_OFFSET, 0x00, + SET_START_LINE | 0x00, + CHARGE_PUMP, 0x14, + MEMORY_MODE, 0x00, + SEG_REMAP | 0x1, + COM_SCAN_DEC, + SET_COM_PINS, 0x12 if self.height == 64 else 0x02, + SET_CONTRAST, 0xCF, + SET_PRECHARGE, 0xF1, + SET_VCOM_DETECT, 0x40, + DISPLAY_ALL_ON_RESUME, + NORMAL_DISPLAY, + DISPLAY_ON + ): + self.write_cmd(cmd) + self.fill(0) + self.show() + + def show(self): + for page in range(self.pages): + self.write_cmd(0xB0 + page) + self.write_cmd(SET_LOW_COLUMN) + self.write_cmd(SET_HIGH_COLUMN) + start = self.width * page + end = start + self.width + self.i2c.writeto(self.addr, b'\x40' + self.buffer[start:end]) + + def poweroff(self): + self.write_cmd(DISPLAY_OFF) + + def poweron(self): + self.write_cmd(DISPLAY_ON) + + def invert(self, invert): + self.write_cmd(INVERT_DISPLAY if invert else NORMAL_DISPLAY) diff --git a/test-time.py b/test-time.py new file mode 100644 index 0000000..1dcc8c4 --- /dev/null +++ b/test-time.py @@ -0,0 +1,9 @@ +import time +import ntptime +tz_offset = 0 +utc = time.localtime() +local = time.localtime(time.mktime(utc) + tz_offset * 3600) +ctime = f"{local[0]:02}-{local[1]:02}-{local[2]:02} {local[3]:02}:{local[4]:02}:{local[5]:02}" +#print("Time =", local[0] + '-' + local[1] + '-' + local[2]) +#print("Time =", local) +print("Time =", ctime) \ No newline at end of file diff --git a/timeutil.py b/timeutil.py new file mode 100644 index 0000000..c9b273f --- /dev/null +++ b/timeutil.py @@ -0,0 +1,34 @@ +import time +import ntptime +import machine + +def setup_rtc(tz_offset): + ntptime.settime() + rtc = machine.RTC() + + utc = time.localtime() + local = time.localtime(time.mktime(utc) + tz_offset * 3600) + + rtc.datetime(( + local[0], local[1], local[2], 0, + local[3], local[4], local[5], 0 + )) + return rtc + +# Function to calculate the difference +def calculate_difference(dt1, dt2): + timestamp1 = rtc_to_timestamp(dt1) + timestamp2 = rtc_to_timestamp(dt2) + difference = abs(timestamp2 - timestamp1) + days = difference // (24 * 3600) + difference %= (24 * 3600) + hours = difference // 3600 + difference %= 3600 + minutes = difference // 60 + seconds = difference % 60 + return days, hours, minutes, seconds + +# Function to convert RTC datetime tuple to a timestamp +def rtc_to_timestamp(dt): + year, month, day, weekday, hour, minute, second, subseconds = dt + return time.mktime((year, month, day, hour, minute, second, 0, 0)) \ No newline at end of file diff --git a/webserver.py b/webserver.py new file mode 100644 index 0000000..1cd032e --- /dev/null +++ b/webserver.py @@ -0,0 +1,106 @@ +import json +import gc +import time +import uasyncio as asyncio + +# Import config value +from config import TANK_CAPACITY_L, VERSION + + +class WebServer: + def __init__(self, wlan, sensors, start_time, max_conn): + self.wlan = wlan + self.sensors = sensors + self.start_time = start_time + self.max_conn = max_conn + self.current = 0 + self.lock = asyncio.Lock() + + async def handle(self, reader, writer): + async with self.lock: + if self.current >= self.max_conn: + writer.close() + return + self.current += 1 + + try: + request = await reader.readline() + path = request.decode().split(" ")[1] + + # Discard headers + while await reader.readline() != b"\r\n": + pass + + # ---------- SENSOR JSON ---------- + if path == "/sensor": + uptime = int(time.time() - self.start_time) + + days = uptime // 86400 + hours = (uptime % 86400) // 3600 + minutes = (uptime % 3600) // 60 + seconds = uptime % 60 + + uptime_text = f"{days}d {hours:02}h {minutes:02}m {seconds:02}s" + + data = { + "pressure": self.sensors.pressure, + "tank_percent": self.sensors.tank_percentage, + "cpu_temp": self.sensors.cpu_temp(), + "rssi": self.wlan.status("rssi"), + "uptime": uptime_text, + "mem": gc.mem_free(), + "footer": "Powered by Raspberry Pi Pico W & HX710B (" + VERSION + ")" + } + + writer.write( + b"HTTP/1.1 200 OK\r\n" + b"Content-Type: application/json\r\n\r\n" + + json.dumps(data).encode() + ) + + # ---------- TARE ---------- + elif path == "/tare": + self.sensors.set_tank_full() + + writer.write( + b"HTTP/1.1 200 OK\r\n" + b"Content-Type: application/json\r\n\r\n" + + json.dumps({"ok": True}).encode() + ) + + # ---------- JAVASCRIPT (CONFIG INJECTED) ---------- + elif path == "/app.js": + with open("/html/app.js") as f: + js = f.read() + + injected = ( + "// injected by webserver.py\n" + f"const TANK_CAPACITY_L = {TANK_CAPACITY_L};\n\n" + ) + + writer.write( + b"HTTP/1.1 200 OK\r\n" + b"Content-Type: application/javascript\r\n" + b"Cache-Control: no-store\r\n\r\n" + + injected.encode() + + js.encode() + ) + + # ---------- HTML ---------- + else: + with open("/html/index.html") as f: + html = f.read() + + writer.write( + b"HTTP/1.1 200 OK\r\n" + b"Content-Type: text/html\r\n\r\n" + + html.encode() + ) + + await writer.drain() + + finally: + writer.close() + await writer.wait_closed() + async with self.lock: + self.current -= 1 diff --git a/wifi.py b/wifi.py new file mode 100644 index 0000000..d1f10fe --- /dev/null +++ b/wifi.py @@ -0,0 +1,15 @@ +import network +import time + +def connect_wifi(ssid, password, hostname): + wlan = network.WLAN(network.STA_IF) + wlan.active(True) + wlan.config(hostname=hostname) + wlan.connect(ssid, password) + + for _ in range(10): + if wlan.isconnected(): + return wlan + time.sleep(1) + + raise RuntimeError("WiFi connection failed")