Skip to content

Commit dcfaec9

Browse files
committed
add neopixel hw_test, relax time requirements on 800ms integration time.
1 parent f486d7d commit dcfaec9

3 files changed

Lines changed: 236 additions & 3 deletions

File tree

hw_tests/01_mode.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ def measure_update_rate(sensor, i2c_dev, timeout_s):
8080
raw2 = read_raw_lux(sensor)
8181
print(f"Raw after change: 0x{raw2:04X}")
8282
print(f"Update latency: {manual_time * 1000:.0f} ms")
83-
test("Manual ~800ms cycle", 0.4 <= manual_time <= 1.5)
83+
test("Manual ~800ms cycle", 0.2 <= manual_time <= 1.5)
8484

8585
# --- MANUAL_CONTINUOUS (CONT=1) + 100ms: fast ---
8686
print("\n--- Manual continuous + 100ms (expect ~100ms) ---")

hw_tests/03_integration_time.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,9 +101,9 @@ def measure_update_rate(sensor, i2c_dev, int_time, settle_s, timeout_s):
101101

102102
# --- 800ms (3 runs, median) ---
103103
print("\n--- 800ms integration ---")
104-
t_slow = measure_update_rate(sensor, sensor, IntegrationTime.MS_400, 1.0, 2.0)
104+
t_slow = measure_update_rate(sensor, sensor, IntegrationTime.MS_800, 1.0, 2.0)
105105
print(f"Median update time: {t_slow * 1000:.0f} ms")
106-
test("800ms: 400-1500ms", 0.4 <= t_slow <= 1.5)
106+
test("800ms: 200-1500ms", 0.2 <= t_slow <= 1.5)
107107

108108
# --- Ordering is the key test ---
109109
print("\n--- Timing order ---")

hw_tests/06_neopixel.py

Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
# SPDX-FileCopyrightText: 2026 Tim Cocks for Adafruit Industries
2+
# SPDX-License-Identifier: MIT
3+
4+
"""
5+
Hardware test: Drive an 8-pixel NeoPixel strip and verify the MAX44009
6+
responds with corresponding lux changes, including threshold interrupts.
7+
8+
Wiring:
9+
NeoPixel strip (8 pixels) data -> board.D9
10+
MAX44009 INT pin -> board.D10 (open-drain, external pull-up)
11+
12+
The sensor should face the NeoPixels so changing pixel brightness
13+
directly changes the measured lux.
14+
"""
15+
16+
import time
17+
18+
import board
19+
import busio
20+
import digitalio
21+
import neopixel
22+
23+
import adafruit_max44009
24+
from adafruit_max44009 import Mode
25+
26+
NEOPIXEL_PIN = board.D9
27+
NUM_PIXELS = 8
28+
INT_PIN = board.D10
29+
30+
# Time to allow the sensor to integrate a new light level before reading.
31+
# Default auto mode runs an 800ms cycle; allow a little margin.
32+
SETTLE = 1.0
33+
34+
passed = 0
35+
failed = 0
36+
37+
38+
def test(name, condition):
39+
global passed, failed
40+
if condition:
41+
print(f"PASS: {name}")
42+
passed += 1
43+
else:
44+
print(f"FAIL: {name}")
45+
failed += 1
46+
47+
48+
def set_pixels(pixels, level):
49+
"""Fill all pixels with a white level (0-255) and show."""
50+
pixels.fill((level, level, level))
51+
pixels.show()
52+
53+
54+
def read_stable_lux(sensor, samples=2):
55+
"""Read lux a few times, return the last sample."""
56+
value = 0.0
57+
for _ in range(samples):
58+
time.sleep(SETTLE)
59+
value = sensor.lux
60+
return value
61+
62+
63+
pixels = None
64+
int_pin = None
65+
66+
try:
67+
i2c = busio.I2C(board.SCL, board.SDA)
68+
sensor = adafruit_max44009.MAX44009(i2c)
69+
test("Sensor found", True)
70+
71+
pixels = neopixel.NeoPixel(NEOPIXEL_PIN, NUM_PIXELS, brightness=1.0, auto_write=False)
72+
set_pixels(pixels, 0)
73+
74+
int_pin = digitalio.DigitalInOut(INT_PIN)
75+
int_pin.direction = digitalio.Direction.INPUT
76+
int_pin.pull = digitalio.Pull.UP
77+
78+
# Use continuous auto mode for faster, clean readings.
79+
sensor.mode = Mode.CONTINUOUS
80+
81+
print("\n=== MAX44009 NeoPixel Light Level Test ===\n")
82+
83+
# ------------------------------------------------------------------
84+
# Part 1: brighter pixels -> higher lux
85+
# ------------------------------------------------------------------
86+
print("--- Brightness sweep ---")
87+
88+
set_pixels(pixels, 0)
89+
lux_off = read_stable_lux(sensor)
90+
print(f"Pixels OFF (0) -> {lux_off} lux")
91+
92+
set_pixels(pixels, 20)
93+
lux_dim = read_stable_lux(sensor)
94+
print(f"Pixels DIM (20) -> {lux_dim} lux")
95+
96+
set_pixels(pixels, 100)
97+
lux_mid = read_stable_lux(sensor)
98+
print(f"Pixels MID (100)-> {lux_mid} lux")
99+
100+
set_pixels(pixels, 255)
101+
lux_bright = read_stable_lux(sensor)
102+
print(f"Pixels FULL (255)-> {lux_bright} lux")
103+
104+
# Valid readings
105+
test("lux_off valid", lux_off == lux_off)
106+
test("lux_dim valid", lux_dim == lux_dim)
107+
test("lux_mid valid", lux_mid == lux_mid)
108+
test("lux_bright valid", lux_bright == lux_bright)
109+
110+
# Monotonic increase with brightness. Allow a small absolute slack
111+
# so sensor quantization at very low levels doesn't cause flaps.
112+
slack = 0.5
113+
test("dim > off", lux_dim > lux_off + slack)
114+
test("mid > dim", lux_mid > lux_dim + slack)
115+
test("bright > mid", lux_bright > lux_mid + slack)
116+
test("bright >> off", lux_bright > lux_off + 5.0)
117+
118+
# ------------------------------------------------------------------
119+
# Part 2: upper-threshold interrupt fires when light rises
120+
# ------------------------------------------------------------------
121+
print("\n--- Upper threshold interrupt ---")
122+
123+
# Park below threshold, pick an upper threshold between dim and bright.
124+
set_pixels(pixels, 20)
125+
time.sleep(SETTLE)
126+
lux_low = sensor.lux
127+
set_pixels(pixels, 255)
128+
time.sleep(SETTLE)
129+
lux_high = sensor.lux
130+
print(f"Low level: {lux_low} High level: {lux_high}")
131+
132+
upper = (lux_low + lux_high) / 2.0
133+
if upper <= lux_low:
134+
upper = lux_low * 2.0 + 1.0
135+
136+
# Return to low, prepare interrupt.
137+
set_pixels(pixels, 20)
138+
time.sleep(SETTLE)
139+
140+
sensor.interrupt_enabled = False
141+
sensor.lower_threshold = 0.0
142+
sensor.upper_threshold = upper
143+
sensor.threshold_timer = 0
144+
_ = sensor.interrupt_status # clear pending
145+
sensor.interrupt_enabled = True
146+
time.sleep(SETTLE)
147+
148+
print(f"Upper threshold set to ~{sensor.upper_threshold} lux")
149+
pin_val = int_pin.value
150+
print(f"INT pin (below upper): {'HIGH' if pin_val else 'LOW'}")
151+
test("INT HIGH when lux below upper threshold", pin_val == True)
152+
test("Status clear below threshold", sensor.interrupt_status == False)
153+
154+
# Drive light above the upper threshold.
155+
set_pixels(pixels, 255)
156+
# Allow one full measurement cycle after change.
157+
time.sleep(SETTLE + 0.5)
158+
159+
pin_val = int_pin.value
160+
print(f"INT pin (above upper): {'HIGH' if pin_val else 'LOW'}")
161+
test("INT LOW when lux above upper threshold", pin_val == False)
162+
test("Status set above upper threshold", sensor.interrupt_status == True)
163+
164+
# After reading status the pin should release back to HIGH.
165+
time.sleep(0.05)
166+
test("INT returns HIGH after status read", int_pin.value == True)
167+
168+
# ------------------------------------------------------------------
169+
# Part 3: lower-threshold interrupt fires when light drops
170+
# ------------------------------------------------------------------
171+
print("\n--- Lower threshold interrupt ---")
172+
173+
# Start bright, upper out of the way, lower between dim and bright.
174+
set_pixels(pixels, 255)
175+
time.sleep(SETTLE)
176+
177+
lower = (lux_low + lux_high) / 2.0
178+
if lower <= lux_low:
179+
lower = lux_low + 1.0
180+
181+
sensor.interrupt_enabled = False
182+
sensor.upper_threshold = 188000.0 # effectively disabled
183+
sensor.lower_threshold = lower
184+
sensor.threshold_timer = 0
185+
_ = sensor.interrupt_status # clear pending
186+
sensor.interrupt_enabled = True
187+
time.sleep(SETTLE)
188+
189+
print(f"Lower threshold set to ~{sensor.lower_threshold} lux")
190+
pin_val = int_pin.value
191+
print(f"INT pin (above lower): {'HIGH' if pin_val else 'LOW'}")
192+
test("INT HIGH when lux above lower threshold", pin_val == True)
193+
test("Status clear above lower threshold", sensor.interrupt_status == False)
194+
195+
# Drop light below the lower threshold.
196+
set_pixels(pixels, 0)
197+
time.sleep(SETTLE + 0.5)
198+
199+
pin_val = int_pin.value
200+
print(f"INT pin (below lower): {'HIGH' if pin_val else 'LOW'}")
201+
test("INT LOW when lux below lower threshold", pin_val == False)
202+
test("Status set below lower threshold", sensor.interrupt_status == True)
203+
204+
# ------------------------------------------------------------------
205+
# Cleanup
206+
# ------------------------------------------------------------------
207+
sensor.interrupt_enabled = False
208+
sensor.upper_threshold = 188000.0
209+
sensor.lower_threshold = 0.0
210+
sensor.threshold_timer = 255
211+
212+
print()
213+
print(f"=== Summary: {passed} passed, {failed} failed ===")
214+
print("ALL TESTS PASSED" if passed > 0 and failed == 0 else "SOME TESTS FAILED")
215+
216+
except Exception as e:
217+
print(f"FAIL: Unhandled exception: {e}")
218+
219+
finally:
220+
try:
221+
if pixels is not None:
222+
pixels.fill((0, 0, 0))
223+
pixels.show()
224+
pixels.deinit()
225+
except Exception:
226+
pass
227+
try:
228+
if int_pin is not None:
229+
int_pin.deinit()
230+
except Exception:
231+
pass
232+
233+
print("~~END~~")

0 commit comments

Comments
 (0)