project
Ghost Hand
Public Unreviewedby John Lauer
A small battery gadget that keeps an automatic sink faucet running while you use the sink: it learns the faucet's timeout, re-arms with the shortest blip, and stops after 10 minutes or when picked up.
main
John Lauer
Rev 2: aim, status-dot lights, Adafruit 1317 battery, rev 2 cost, revisions, firmware
363a22f
1d ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250
/*
* leds.c : WS2812B-2020-V6 driver and light patterns
*
* Rev 2 chain: LED 0 = D4 STATUS dot, LEDs 1..4 = D5..D8 charge gauge
* (25 / 50 / 75 / 100 %). Every light is a 1.5 mm round clear-resin pipe, so
* the status dot shows everything rev 1's logo showed, at about half the drive.
*
* Why a bit-bang at 10 MHz (and not SPI / USART-SPI / CCL):
* LED_DIN is PB5. On the ATtiny1616 PB5 carries CLKOUT, AIN8, AC0 AINP1 and
* TCA0 WO2 (alternate) only: no SPI MOSI, no USART TxD, no CCL LUT output.
* So no serial peripheral can drive it; a cycle-counted loop it is. At the
* normal 5 MHz a 1.25 us bit is 6.25 cycles, too few to place both edges,
* and 20 MHz needs 4.5 V. 10 MHz (OSC20M / 2) is inside the 0-10 MHz
* @ 2.7 V speed grade, giving 13 cycles per bit:
*
* WS2812B-2020-V6 spec this loop @ 10 MHz
* T0H 220..380 ns 3 cycles = 300 ns
* T1H 580 ns..1 us 7 cycles = 700 ns
* T0L 580 ns..1 us 10 cycles = 1000 ns
* T1L 580 ns..1 us 6 cycles = 600 ns
* bit >= 1.25 us 13 cycles = 1.3 us
* RES >= 280 us frames are >= 15 ms apart
*
* T0L sits at the top of its range; WS2812-class parts only latch on a low
* longer than RES, so a longer low between bits (the byte fetch adds about
* 0.6 us after each 8th bit) is harmless. The CPU clock is raised only for
* the ~200 us write with interrupts off, and only when the echo is idle;
* the echo event users are disconnected during the write so the TCBs never
* run on the doubled clock (a faucet pulse in that window just misses one
* echo, which no faucet treats as the hand leaving).
*
* Current cap: 12 mA per channel at 255, sum of all channel values limited to
* LED_SUM_MAX (640 -> ~30 mA), sized for small dots and the 150 mAh cell.
*/
#include <avr/io.h>
#include <avr/interrupt.h>
#include "board.h"
#include "leds.h"
#include "echo.h"
#include "power.h"
static uint8_t px[LED_COUNT * 3]; /* G, R, B per LED (WS2812 order) */
static uint8_t last[LED_COUNT * 3];
static bool rail_on;
static uint8_t dark_frames;
static led_pattern_t pat;
static uint8_t pct;
static uint32_t t0;
static bool f_echo, f_blip;
static void ws_send(const uint8_t *p, uint8_t n)
{
uint8_t b, bits;
__asm__ __volatile__(
"1: ld %[b], %a[p]+ \n\t"
" ldi %[bits], 8 \n\t"
"2: sbi %[port], %[pin] \n\t" /* c1 rising edge */
" nop \n\t" /* c2 */
" sbrs %[b], 7 \n\t" /* c3 (c3-c4 if bit is 1) */
" cbi %[port], %[pin] \n\t" /* c4 '0': low after 3 cyc */
" lsl %[b] \n\t" /* c5 */
" nop \n\t" /* c6 */
" nop \n\t" /* c7 */
" cbi %[port], %[pin] \n\t" /* c8 '1': low after 7 cyc */
" nop \n\t" /* c9 */
" nop \n\t" /* c10 */
" dec %[bits] \n\t" /* c11 */
" brne 2b \n\t" /* c12-c13 -> next bit at c14 */
" dec %[n] \n\t"
" brne 1b \n\t"
: [b] "=&d" (b), [bits] "=&d" (bits), [p] "+e" (p), [n] "+r" (n)
: [port] "I" (_SFR_IO_ADDR(VPORTB_OUT)), [pin] "I" (PIN_LED_DIN)
: "memory");
}
void leds_init(void)
{
PORTB.OUTSET = (1 << PIN_LED_EN_N); /* rail off */
PORTB.OUTCLR = (1 << PIN_LED_DIN);
PORTB.DIRSET = (1 << PIN_LED_EN_N) | (1 << PIN_LED_DIN);
rail_on = false;
for (uint8_t i = 0; i < sizeof last; i++) last[i] = 0;
}
void leds_off(void)
{
PORTB.OUTCLR = (1 << PIN_LED_DIN); /* never drive data into an unpowered LED */
PORTB.OUTSET = (1 << PIN_LED_EN_N);
rail_on = false;
pat = PAT_OFF;
for (uint8_t i = 0; i < sizeof last; i++) last[i] = 0;
}
void leds_pattern(led_pattern_t p, uint8_t percent, uint32_t now)
{
pat = p; pct = percent; t0 = now;
}
void leds_flags(bool echoing, bool blip) { f_echo = echoing; f_blip = blip; }
led_pattern_t leds_current(void) { return pat; }
static void set(uint8_t i, uint8_t r, uint8_t g, uint8_t b)
{
px[3 * i] = g; px[3 * i + 1] = r; px[3 * i + 2] = b;
}
/* triangle 0..255 over 'period' ms, squared for a perceptual fade */
static uint8_t breath(uint32_t t, uint16_t period)
{
uint32_t ph = t % period, half = period / 2;
uint32_t tri = ph < half ? ph : period - ph;
uint16_t lin = (uint16_t)(tri * 255u / half);
return (uint8_t)((lin * lin) / 255u);
}
static uint8_t level_segments(uint8_t p) { return (uint8_t)((p + 12u) / 25u); } /* 0..4 */
static void gauge(uint8_t lit, uint8_t fill_frac_255)
{
for (uint8_t s = 0; s < 4; s++) {
uint8_t k = s < lit ? 255 : 0;
if (s == lit && fill_frac_255) k = fill_frac_255;
if (pct <= 25) set(LED_GAUGE0 + s, (uint8_t)(28u * k / 255u), (uint8_t)(11u * k / 255u), 0); /* amber */
else set(LED_GAUGE0 + s, 0, (uint8_t)(28u * k / 255u), (uint8_t)(4u * k / 255u)); /* green */
}
}
static void render(uint32_t now)
{
uint32_t t = now - t0;
for (uint8_t i = 0; i < sizeof px; i++) px[i] = 0;
switch (pat) {
case PAT_OFF:
break;
case PAT_GAUGE: { /* fill one segment per 375 ms, hold */
uint8_t lit = level_segments(pct);
if (!lit && pct) lit = 1;
uint32_t step = t / 375u;
if (step >= lit) gauge(lit, 0);
else gauge((uint8_t)step, (uint8_t)((t % 375u) * 255u / 375u));
uint8_t l = (uint8_t)(t < 3000 ? t * 5u / 3000u : 5); /* status dot wakes up */
set(LED_STATUS, (uint8_t)(l * 6 / 10), l, l);
break;
}
case PAT_ARMED: {
uint8_t l = (uint8_t)(2u + (breath(t, 4000) * 10u) / 255u); /* 2..12 (rev 1 logo: 3..24) */
if (f_echo) l = (uint8_t)(l + l / 2); /* echoing: a bit brighter */
if (f_blip) l = ((t / 40u) & 1u) ? (uint8_t)(l * 2u) : 0; /* re-arm: quick flicker */
set(LED_STATUS, (uint8_t)(l * 6 / 10), l, l); /* soft cyan-white */
break;
}
case PAT_DONE: { /* slow warm fade, 2.5 s */
uint8_t k = t < 2500 ? (uint8_t)(255u - t * 255u / 2500u) : 0;
k = (uint8_t)((k * k) / 255u);
set(LED_STATUS, (uint8_t)(20u * k / 255u), (uint8_t)(15u * k / 255u), (uint8_t)(8u * k / 255u));
break;
}
case PAT_LOW: /* red blink 4 Hz on the status dot and the first segment */
if (((t / 250u) & 1u) == 0) {
set(LED_STATUS, 24, 0, 0);
set(LED_GAUGE0, 20, 0, 0);
}
break;
case PAT_CHARGING: {
uint8_t lit = (uint8_t)(pct / 25u);
if (lit >= 4) gauge(4, 0);
else {
gauge(lit, 0);
uint8_t k = breath(t, 1500);
set(LED_GAUGE0 + lit, 0, (uint8_t)(3u + 25u * k / 255u), (uint8_t)(4u * k / 255u));
}
break;
}
case PAT_FULL:
if (t < 5000) {
for (uint8_t s = 0; s < 4; s++) set(LED_GAUGE0 + s, 0, 28, 0);
} else {
uint32_t w = (t - 5000) % 10000u;
if (w < 600) {
uint8_t k = breath(w, 600);
for (uint8_t s = 0; s < 4; s++) set(LED_GAUGE0 + s, 0, (uint8_t)(8u * k / 255u), 0);
}
}
break;
case PAT_SHOW_CHARGE: {
uint8_t lit = level_segments(pct);
if (!lit && pct) lit = 1;
gauge(lit, 0);
break;
}
}
/* current cap */
uint16_t sum = 0;
for (uint8_t i = 0; i < sizeof px; i++) sum += px[i];
if (sum > LED_SUM_MAX)
for (uint8_t i = 0; i < sizeof px; i++) px[i] = (uint8_t)(((uint32_t)px[i] * LED_SUM_MAX) / sum);
}
bool leds_frame(uint32_t now)
{
render(now);
bool any = false, changed = false;
for (uint8_t i = 0; i < sizeof px; i++) {
if (px[i]) any = true;
if (px[i] != last[i]) changed = true;
}
if (!any) {
if (rail_on && ++dark_frames >= 4) { /* dark for a while: cut the rail */
PORTB.OUTCLR = (1 << PIN_LED_DIN);
PORTB.OUTSET = (1 << PIN_LED_EN_N);
rail_on = false;
for (uint8_t i = 0; i < sizeof last; i++) last[i] = 0;
}
if (!rail_on) return true;
} else dark_frames = 0;
if (!changed && rail_on) return true;
if (!rail_on) {
PORTB.OUTCLR = (1 << PIN_LED_EN_N) | (1 << PIN_LED_DIN);
rail_on = true;
delay_ms_busy(1); /* LED power-on reset */
}
if (!echo_quiet()) return false; /* try again next frame */
echo_hold(true);
while (!echo_quiet() && !echo_blanking()) ; /* a shot that started just now: <= 40 us */
if (echo_blanking()) { echo_hold(false); return false; }
cli();
clock_fast();
ws_send(px, sizeof px);
clock_normal();
sei();
echo_hold(false);
for (uint8_t i = 0; i < sizeof px; i++) last[i] = px[i];
return true;
}