commit 9e45f1f199ff61cf90d5a80a1a242792cb2a1c49 Author: Raymond Bailie Date: Tue Jun 16 13:02:06 2026 +0200 commit 1 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 0000000..69bcc7f Binary files /dev/null and b/html/favicon.ico differ diff --git a/html/index.html b/html/index.html new file mode 100644 index 0000000..0de86f3 --- /dev/null +++ b/html/index.html @@ -0,0 +1,287 @@ + + + + + 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")