This commit is contained in:
2026-06-16 13:02:06 +02:00
commit 9e45f1f199
29 changed files with 2531 additions and 0 deletions

View File

@@ -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)

10
Maker_Pico_Box/button.py Normal file
View File

@@ -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)

View File

@@ -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)

View File

@@ -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])

172
Maker_Pico_Box/main.py Normal file
View File

@@ -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 (0100)
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 (02)
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)

111
Maker_Pico_Box/main3.py Normal file
View File

@@ -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 (02)
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)

342
Maker_Pico_Box/main4.py Normal file
View File

@@ -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 0100%
last_state = [1, 1, 1]
press_time = [0, 0, 0]
# ============================================================
# FLASHING SINGLE PIXEL (GP28)
# ============================================================
FLASH_COLOR_INDEX = 1 # Choose color from palette (010)
FLASH_BRIGHTNESS = 5 # 0100%
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 02
# -----------------------------
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 02 (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 (010)
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)

174
Maker_Pico_Box/main5.py Normal file
View File

@@ -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 02 (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)

View File

@@ -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)

29
Maker_Pico_Box/rainbow.py Normal file
View File

@@ -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)

107
calibrate.py Normal file
View File

@@ -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")

1
calibration.cfg Normal file
View File

@@ -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}

21
config.py Normal file
View File

@@ -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

53
display.py Normal file
View File

@@ -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()

153
html/app.js Normal file
View File

@@ -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();

BIN
html/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

287
html/index.html Normal file
View File

@@ -0,0 +1,287 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Tank Monitor</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
/* ======================= */
/* THEME VARIABLES */
/* ======================= */
:root {
--bg: #f4f6f8;
--text: #000;
--card-bg: #ffffff;
--accent: #1976d2;
--tank-bg: #e0e0e0;
--wave: rgba(255,255,255,0.35);
--footer: #555;
}
body.dark {
--bg: #121212;
--text: #e0e0e0;
--card-bg: #1f1f1f;
--accent: #1e88e5;
--tank-bg: #333;
--wave: rgba(255,255,255,0.25);
--footer: #888;
}
body {
font-family: Arial, sans-serif;
background: var(--bg);
color: var(--text);
margin: 0;
padding: 0;
transition: background 0.3s, color 0.3s;
}
/* ======================= */
/* HEADER + THEME SWITCH */
/* ======================= */
.header {
background: var(--accent);
color: white;
padding: 15px;
text-align: center;
font-size: 22px;
position: relative;
}
.theme-toggle {
position: absolute;
right: 20px;
top: 15px;
}
.theme-toggle input {
appearance: none;
width: 50px;
height: 25px;
background: #555;
border-radius: 50px;
cursor: pointer;
position: relative;
transition: background 0.3s;
}
.theme-toggle input:checked {
background: #90caf9;
}
.theme-toggle input::before {
content: "";
position: absolute;
width: 21px;
height: 21px;
background: white;
border-radius: 50%;
top: 2px;
left: 2px;
transition: transform 0.3s;
}
.theme-toggle input:checked::before {
transform: translateX(25px);
}
/* ======================= */
/* CONTAINERS + CARDS */
/* ======================= */
.container {
width: 95%;
max-width: 900px;
margin: auto;
padding-top: 20px;
}
.card {
background: var(--card-bg);
border-radius: 12px;
padding: 20px;
margin-bottom: 20px;
box-shadow: 0 2px 10px rgba(0,0,0,0.25);
transition: background 0.3s;
}
.card h3 {
margin-top: 0;
color: var(--accent);
}
.stats-block p {
margin: 6px 0;
font-size: 16px;
}
.tank-flex-wrapper {
display: flex;
justify-content: space-between;
align-items: center;
gap: 30px;
flex-wrap: wrap;
}
/* ======================= */
/* VERTICAL TANK WAVE */
/* ======================= */
.tank-container {
width: min(100vw, 150px);
height: min(75vh, 250px);
background: var(--tank-bg);
border-radius: 10px;
overflow: hidden;
display: flex;
align-items: flex-end;
box-shadow: inset 0 0 10px rgba(0,0,0,0.6);
transition: width 0.3s, height 0.3s;
}
#tank-fill {
width: 100%;
height: 0%;
background: #4caf50;
position: relative;
transition: height 1s linear, background-color 1s linear;
}
#tank-fill::before,
#tank-fill::after {
content: "";
position: absolute;
left: 0;
width: 200%;
height: 20px;
background: var(--wave);
border-radius: 50%;
animation: wave 3s infinite linear;
}
#tank-fill::before { top: -5px; }
#tank-fill::after {
top: -12px;
opacity: 0.5;
animation-duration: 5s;
}
@keyframes wave {
from { transform: translateX(-50%); }
to { transform: translateX(0%); }
}
/* ======================= */
/* BUTTONS */
/* ======================= */
button {
padding: 12px 20px;
background: #e53935;
color: white;
border: none;
border-radius: 8px;
font-size: 16px;
cursor: pointer;
width: 100%;
}
button:hover {
background: #ff5252;
}
.button-row {
display: flex;
gap: 15px;
}
.button-row button {
width: 100%;
}
@media (max-width: 480px) {
.button-row {
flex-direction: column;
}
}
/* ======================= */
/* FOOTER */
/* ======================= */
.footer {
text-align: center;
margin-top: 30px;
color: var(--footer);
font-size: 12px;
padding-bottom: 30px;
}
/* Mobile tank adjustment */
@media (max-width: 600px) {
.tank-container {
width: 65px;
height: 25vh;
}
}
</style>
<script src="/app.js" defer></script>
</head>
<body>
<div class="header">
Tank Monitor
<label class="theme-toggle">
<input id="modeSwitch" type="checkbox">
</label>
</div>
<div class="container">
<!-- System Stats + Tank -->
<div class="card">
<h3>System Stats</h3>
<div class="tank-flex-wrapper">
<div class="stats-block">
<p><b>Date:</b> <span id="date">--</span></p>
<p><b>Time:</b> <span id="time">--</span></p>
<p><b>Uptime:</b> <span id="uptime">--</span></p>
<p><b>Level:</b> <span id="level">--</span></p>
<p><b>Pressure:</b> <span id="pressure">--</span></p>
<p><b>Tank Level:</b> <span id="percent">--</span></p>
<p><b>CPU Temp:</b> <span id="cpu">--</span></p>
<p><b>WiFi RSSI:</b> <span id="rssi">--</span></p>
<p><b>Free Memory:</b> <span id="mem">--</span></p>
</div>
<div class="tank-container">
<div id="tank-fill"></div>
</div>
</div>
</div>
<!-- Actions -->
<div class="card">
<h3>Actions</h3>
<div class="button-row">
<!-- <button onclick="fetch('/tare')">Tank Full</button> -->
<button onclick="tankFullProtected()">Tank Full</button>
<button onclick="fetch('/calibrate')">Calibrate</button>
</div>
</div>
</div>
<div class="footer" id="footer-text">
Powered by Raspberry Pi Pico W & HX710B
</div>
</body>
</html>

116
hx710b.py Normal file
View File

@@ -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

116
lib/hx710b.py Normal file
View File

@@ -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

89
lib/ssd1306.py Normal file
View File

@@ -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)

238
main.py Normal file
View File

@@ -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 (0100)
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())

2
reboot.py Normal file
View File

@@ -0,0 +1,2 @@
import machine
machine.reset()

30
scan-i2c.py Normal file
View File

@@ -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)

57
sensors.py Normal file
View File

@@ -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

89
ssd1306.py Normal file
View File

@@ -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)

9
test-time.py Normal file
View File

@@ -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)

34
timeutil.py Normal file
View File

@@ -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))

106
webserver.py Normal file
View File

@@ -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

15
wifi.py Normal file
View File

@@ -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")