poi.ino
1 // SPDX-FileCopyrightText: 2019 Phillip Burgess for Adafruit Industries 2 // 3 // SPDX-License-Identifier: MIT 4 5 /*------------------------------------------------------------------------ 6 POV LED poi sketch. Uses the following Adafruit parts (X2 for two poi): 7 8 - Trinket 5V (adafruit.com/product/1501) (NOT Pro Trinket or 3V Trinket) 9 - 150 mAh LiPoly battery (#1317) 10 - LiPoly backpack (#2124) 11 - Tiny SPDT slide switch (#805) 12 - 144 LED/m DotStar strip (#2328 or #2329) ONE is enough for four poi! 13 - Small tactile button (#1489) (optional) ONE is enough for 20 poi! 14 15 Use 'soda bottle preform' for enclosure w/5.25" (133 mm) inside depth. 16 3D-printable cap and insert can be downloaded from Thingiverse: 17 http://www.thingiverse.com/thing:918847 18 Add leash - e.g. paracord, or fancy ones available from flowtoys.com. 19 20 Needs Adafruit_DotStar library: github.com/adafruit/Adafruit_DotStar 21 22 The poi project is designed around the Trinket board specifically for 23 its small size (Pro Trinket won't fit). Throughout the code you'll see 24 mentions of other boards and extra features -- future projects might 25 adapt this code for larger spinnythings like clubs or staves -- but 26 for the poi, no, there's just no space inside the soda bottle preform, 27 it's Trinket only and just with the most basic features. 28 29 Adafruit invests time and resources providing this open source code, 30 please support Adafruit and open-source hardware by purchasing 31 products from Adafruit! 32 33 Written by Phil Burgess / Paint Your Dragon for Adafruit Industries. 34 MIT license, all text above must be included in any redistribution. 35 See 'COPYING' file for additional notes. 36 ------------------------------------------------------------------------*/ 37 38 #include <Arduino.h> 39 #include <Adafruit_DotStar.h> 40 #include <avr/power.h> 41 #include <avr/sleep.h> 42 // #include <SPI.h> // Enable this line on Pro Trinket 43 44 #ifdef __AVR_ATtiny85__ 45 typedef uint8_t line_t; // Max 255 lines/image on Trinket 46 #else 47 typedef uint16_t line_t; // Bigger images OK on other boards 48 #endif 49 50 // CONFIGURABLE STUFF ------------------------------------------------------ 51 52 #include "graphics.h" // Graphics data is contained in this header file. 53 // It's generated using the 'convert.py' Python script. Various image 54 // formats are supported, trading off color fidelity for PROGMEM space 55 // (particularly limited on Trinket). Handles 1-, 4- and 8-bit-per-pixel 56 // palette-based images, plus 24-bit truecolor. 1- and 4-bit palettes can 57 // be altered in RAM while running to provide additional colors, but be 58 // mindful of peak & average current draw if you do that! Power limiting 59 // is normally done in convert.py (keeps this code relatively small & fast). 60 // 1/4/8/24 were chosen because the AVR can handle these operations fairly 61 // easily (have idea for handing arbitrary bit depth w/328P, but this margin 62 // is too narrow to contain). 63 64 // Ideally you use hardware SPI as it's much faster, though limited to 65 // specific pins. If you really need to bitbang DotStar data & clock on 66 // different pins, optionally define those here: 67 //#define LED_DATA_PIN 0 68 //#define LED_CLOCK_PIN 1 69 70 // Select from multiple images using tactile button (#1489) between pin and 71 // ground. Requires suitably-built graphics.h file w/more than one image. 72 #define SELECT_PIN 3 73 74 // Optional feature -- not enabled here, no space -- a vibration switch 75 // (aligned perpendicular to leash) is used as a poor man's accelerometer. 76 // Poi then lights only when moving, saving some power. The 'fast' 77 // vibration switch is VERY sensitive and will trigger at the slightest 78 // bump, while the 'medium' switch requires a certain spin rate which may 79 // not trigger if you're doing mellow spins. Neither is perfect. To leave 80 // that out and simply have the poi run always-on, comment out this line: 81 //#define MOTION_PIN 2 82 83 // Another optional feature not enable due to physical size -- powering down 84 // DotStars when idle conserves more battery. Use a PNP transistor (e.g. 85 // 2N2907) (w/220 Ohm resistor to base) as a 'high side' switch to DotStar 86 // +V. DON'T do this NPN/low-side, may damage strip. MOTION_PIN must also 87 // be defined to use this (pointless without). 88 //#define POWER_PIN 4 89 90 #define SLEEP_TIME 2000 // Not-spinning time before sleep, in milliseconds 91 92 // Empty and full thresholds (millivolts) used for battery level display: 93 #define BATT_MIN_MV 3350 // Some headroom over battery cutoff near 2.9V 94 #define BATT_MAX_MV 4000 // And little below fresh-charged battery near 4.1V 95 96 boolean autoCycle = false; // Set to true to cycle images by default 97 #define CYCLE_TIME 15 // Time, in seconds, between auto-cycle images 98 99 // ------------------------------------------------------------------------- 100 101 #if defined(LED_DATA_PIN) && defined(LED_CLOCK_PIN) 102 // Older DotStar LEDs use GBR order. If colors are wrong, edit here. 103 Adafruit_DotStar strip = Adafruit_DotStar(NUM_LEDS, 104 LED_DATA_PIN, LED_CLOCK_PIN, DOTSTAR_BRG); 105 #else 106 Adafruit_DotStar strip = Adafruit_DotStar(NUM_LEDS, DOTSTAR_BRG); 107 #endif 108 109 void imageInit(void); 110 uint16_t readVoltage(void); 111 #ifdef MOTION_PIN 112 void sleep(void); 113 #endif 114 115 void setup() { 116 #if defined(__AVR_ATtiny85__) && (F_CPU == 16000000L) 117 clock_prescale_set(clock_div_1); // Enable 16 MHz on Trinket 118 #endif 119 120 #ifdef POWER_PIN 121 pinMode(POWER_PIN, OUTPUT); 122 digitalWrite(POWER_PIN, LOW); // Power-on LED strip 123 #endif 124 strip.begin(); // Allocate DotStar buffer, init SPI 125 strip.clear(); // Make sure strip is clear 126 strip.show(); // before measuring battery 127 128 // Display battery level bargraph on startup. It's just a vague estimate 129 // based on cell voltage (drops with discharge) but doesn't handle curve. 130 uint16_t mV = readVoltage(); 131 uint8_t lvl = (mV >= BATT_MAX_MV) ? NUM_LEDS : // Full (or nearly) 132 (mV <= BATT_MIN_MV) ? 1 : // Drained 133 1 + ((mV - BATT_MIN_MV) * NUM_LEDS + (NUM_LEDS / 2)) / 134 (BATT_MAX_MV - BATT_MIN_MV + 1); // # LEDs lit (1-NUM_LEDS) 135 for(uint8_t i=0; i<lvl; i++) { // Each LED to batt level... 136 uint8_t g = (i * 5 + 2) / NUM_LEDS; // Red to green 137 strip.setPixelColor(i, 4-g, g, 0); 138 strip.show(); // Animate a bit 139 delay(250 / NUM_LEDS); 140 } 141 delay(1500); // Hold last state a moment 142 strip.clear(); // Then clear strip 143 strip.show(); 144 145 imageInit(); // Initialize pointers for default image 146 147 #ifdef SELECT_PIN 148 pinMode(SELECT_PIN, INPUT_PULLUP); 149 #endif 150 #ifdef MOTION_PIN 151 pinMode(MOTION_PIN, INPUT_PULLUP); 152 sleep(); // Sleep until motion detected 153 #endif 154 } 155 156 // GLOBAL STATE STUFF ------------------------------------------------------ 157 158 uint32_t lastImageTime = 0L; // Time of last image change 159 #ifdef MOTION_PIN 160 uint32_t prev = 0L; // Used for sleep timing 161 #endif 162 uint8_t imageNumber = 0, // Current image being displayed 163 imageType, // Image type: PALETTE[1,4,8] or TRUECOLOR 164 *imagePalette, // -> palette data in PROGMEM 165 *imagePixels, // -> pixel data in PROGMEM 166 palette[16][3]; // RAM-based color table for 1- or 4-bit images 167 line_t imageLines, // Number of lines in active image 168 imageLine; // Current line number in image 169 #ifdef SELECT_PIN 170 uint8_t debounce = 0; // Debounce counter for image select pin 171 #endif 172 173 void imageInit() { // Initialize global image state for current imageNumber 174 imageType = pgm_read_byte(&images[imageNumber].type); 175 #ifdef __AVR_ATtiny85__ 176 imageLines = pgm_read_byte(&images[imageNumber].lines); 177 #else 178 imageLines = pgm_read_word(&images[imageNumber].lines); 179 #endif 180 imageLine = 0; 181 imagePalette = (uint8_t *)pgm_read_word(&images[imageNumber].palette); 182 imagePixels = (uint8_t *)pgm_read_word(&images[imageNumber].pixels); 183 // 1- and 4-bit images have their color palette loaded into RAM both for 184 // faster access and to allow dynamic color changing. Not done w/8-bit 185 // because that would require inordinate RAM (328P could handle it, but 186 // I'd rather keep the RAM free for other features in the future). 187 if(imageType == PALETTE1) memcpy_P(palette, imagePalette, 2 * 3); 188 else if(imageType == PALETTE4) memcpy_P(palette, imagePalette, 16 * 3); 189 lastImageTime = millis(); // Save time of image init for next auto-cycle 190 } 191 192 void nextImage(void) { 193 if(++imageNumber >= NUM_IMAGES) imageNumber = 0; 194 imageInit(); 195 } 196 197 // MAIN LOOP --------------------------------------------------------------- 198 199 void loop() { 200 uint32_t t = millis(); // Current time, milliseconds 201 #ifdef MOTION_PIN 202 // Tried to do this with watchdog timer but encountered gas pains, so... 203 if(!digitalRead(MOTION_PIN)) { // Vibration switch pulled down? 204 prev = t; // Yes, reset timer 205 } else if((t - prev) > SLEEP_TIME) { // No, SLEEP_TIME elapsed w/no switch? 206 sleep(); // Power down 207 prev = t; // Reset timer on wake 208 } 209 #endif 210 211 if(autoCycle) { 212 if((t - lastImageTime) >= (CYCLE_TIME * 1000L)) nextImage(); 213 // CPU clocks vary slightly; multiple poi won't stay in perfect sync. 214 // Keep this in mind when using auto-cycle mode, you may want to cull 215 // the image selection to avoid unintentional regrettable combinations. 216 } 217 #ifdef SELECT_PIN 218 if(digitalRead(SELECT_PIN)) { // Image select? 219 debounce = 0; // Not pressed -- reset counter 220 } else { // Pressed... 221 if(++debounce >= 25) { // Debounce input 222 nextImage(); // Switch to next image 223 while(!digitalRead(SELECT_PIN)); // Wait for release 224 // If held 1+ sec, toggle auto-cycle mode on/off 225 if((millis() - t) >= 1000L) autoCycle = !autoCycle; 226 debounce = 0; 227 } 228 } 229 #endif 230 231 // Transfer one scanline from pixel data to LED strip: 232 233 // If you're really pressed for graphics space and need just a few extra 234 // scanlines, and know for a fact you won't be using certain image modes, 235 // you can comment out the corresponding blocks below. e.g. PALETTE8 and 236 // TRUECOLOR are somewhat impractical on Trinket, and commenting them out 237 // can free up nearly 200 bytes of extra image storage. 238 239 switch(imageType) { 240 241 case PALETTE1: { // 1-bit (2 color) palette-based image 242 uint8_t pixelNum = 0, byteNum, bitNum, pixels, idx, 243 *ptr = (uint8_t *)&imagePixels[imageLine * NUM_LEDS / 8]; 244 for(byteNum = NUM_LEDS/8; byteNum--; ) { // Always padded to next byte 245 pixels = pgm_read_byte(ptr++); // 8 pixels of data (pixel 0 = LSB) 246 for(bitNum = 8; bitNum--; pixels >>= 1) { 247 idx = pixels & 1; // Color table index for pixel (0 or 1) 248 strip.setPixelColor(pixelNum++, 249 palette[idx][0], palette[idx][1], palette[idx][2]); 250 } 251 } 252 break; 253 } 254 255 case PALETTE4: { // 4-bit (16 color) palette-based image 256 uint8_t pixelNum, p1, p2, 257 *ptr = (uint8_t *)&imagePixels[imageLine * NUM_LEDS / 2]; 258 for(pixelNum = 0; pixelNum < NUM_LEDS; ) { 259 p2 = pgm_read_byte(ptr++); // Data for two pixels... 260 p1 = p2 >> 4; // Shift down 4 bits for first pixel 261 p2 &= 0x0F; // Mask out low 4 bits for second pixel 262 strip.setPixelColor(pixelNum++, 263 palette[p1][0], palette[p1][1], palette[p1][2]); 264 strip.setPixelColor(pixelNum++, 265 palette[p2][0], palette[p2][1], palette[p2][2]); 266 } 267 break; 268 } 269 270 #if 0 // Yep, demo images need ALL THE SPACE (see comment above) 271 case PALETTE8: { // 8-bit (256 color) PROGMEM-palette-based image 272 uint16_t o; 273 uint8_t pixelNum, 274 *ptr = (uint8_t *)&imagePixels[imageLine * NUM_LEDS]; 275 for(pixelNum = 0; pixelNum < NUM_LEDS; pixelNum++) { 276 o = pgm_read_byte(ptr++) * 3; // Offset into imagePalette 277 strip.setPixelColor(pixelNum, 278 pgm_read_byte(&imagePalette[o]), 279 pgm_read_byte(&imagePalette[o + 1]), 280 pgm_read_byte(&imagePalette[o + 2])); 281 } 282 break; 283 } 284 285 case TRUECOLOR: { // 24-bit ('truecolor') image (no palette) 286 uint8_t pixelNum, r, g, b, 287 *ptr = (uint8_t *)&imagePixels[imageLine * NUM_LEDS * 3]; 288 for(pixelNum = 0; pixelNum < NUM_LEDS; pixelNum++) { 289 r = pgm_read_byte(ptr++); 290 g = pgm_read_byte(ptr++); 291 b = pgm_read_byte(ptr++); 292 strip.setPixelColor(pixelNum, r, g, b); 293 } 294 break; 295 } 296 #endif 297 } 298 299 strip.show(); // Refresh LEDs 300 #if !defined(LED_DATA_PIN) && !defined(LED_CLOCK_PIN) 301 delayMicroseconds(900); // Because hardware SPI is ludicrously fast 302 #endif 303 if(++imageLine >= imageLines) imageLine = 0; // Next scanline, wrap around 304 } 305 306 // POWER-SAVING STUFF -- Relentlessly non-portable ------------------------- 307 308 #ifdef MOTION_PIN 309 void sleep() { 310 311 // Turn off LEDs... 312 strip.clear(); // Issue '0' data 313 strip.show(); 314 #ifdef POWER_PIN 315 digitalWrite(POWER_PIN, HIGH); // Cut power 316 #if !defined(LED_DATA_PIN) && !defined(LED_CLOCK_PIN) 317 #ifdef __AVR_ATtiny85__ 318 pinMode(1, INPUT); // Set SPI data & clock to inputs else 319 pinMode(2, INPUT); // DotStars power parasitically, jerks. 320 #else 321 pinMode(11, INPUT); 322 pinMode(13, INPUT); 323 #endif // ATtiny 324 #endif // Data/clock/pins 325 #endif // POWER_PIN 326 327 power_all_disable(); // Peripherals ALL OFF, best sleep-state battery use 328 329 // Enable pin-change interrupt on motion pin 330 #ifdef __AVR_ATtiny85__ 331 PCMSK = _BV(MOTION_PIN); // Pin mask 332 GIMSK = _BV(PCIE); // Interrupt enable 333 #else 334 volatile uint8_t *p = portInputRegister(digitalPinToPort(MOTION_PIN)); 335 if(p == &PIND) { // Pins 0-7 = PCINT16-23 336 PCMSK2 = _BV(MOTION_PIN); 337 PCICR = _BV(PCIE2); 338 } else if(p == &PINB) { // Pins 8-13 = PCINT0-5 339 PCMSK0 = _BV(MOTION_PIN- 8); 340 PCICR = _BV(PCIE0); 341 } else if(p == &PINC) { // Pins 14-20 = PCINT8-14 342 PCMSK1 = _BV(MOTION_PIN-14); 343 PCICR = _BV(PCIE1); 344 } 345 #endif 346 347 // If select pin is enabled, that wakes too! 348 #ifdef SELECT_PIN 349 debounce = 0; 350 #ifdef __AVR_ATtiny85__ 351 PCMSK |= _BV(SELECT_PIN); // Add'l pin mask 352 #else 353 volatile uint8_t *p = portInputRegister(digitalPinToPort(SELECT_PIN)); 354 if(p == &PIND) { // Pins 0-7 = PCINT16-23 355 PCMSK2 = _BV(SELECT_PIN); 356 PCICR = _BV(PCIE2); 357 } else if(p == &PINB) { // Pins 8-13 = PCINT0-5 358 PCMSK0 = _BV(SELECT_PIN- 8); 359 PCICR = _BV(PCIE0); 360 } else if(p == &PINC) { // Pins 14-20 = PCINT8-14 361 PCMSK1 = _BV(SELECT_PIN-14); 362 PCICR = _BV(PCIE1); 363 } 364 #endif // ATtiny 365 #endif // SELECT_PIN 366 367 set_sleep_mode(SLEEP_MODE_PWR_DOWN); // Deepest sleep mode 368 sleep_enable(); 369 interrupts(); 370 sleep_mode(); // Power down 371 372 // Resumes here on wake 373 374 // Clear pin change settings so interrupt won't fire again 375 #ifdef __AVR_ATtiny85__ 376 GIMSK = PCMSK = 0; 377 #else 378 PCICR = PCMSK0 = PCMSK1 = PCMSK2 = 0; 379 #endif 380 power_timer0_enable(); // Used by millis() 381 #if !defined(LED_DATA_PIN) && !defined(LED_CLOCK_PIN) 382 #ifdef __AVR_ATtiny85__ 383 pinMode(1, OUTPUT); // Re-enable SPI pins 384 pinMode(2, OUTPUT); 385 power_usi_enable(); // Used by DotStar 386 #else 387 pinMode(11, OUTPUT); // Re-enable SPI pins 388 pinMode(13, OUTPUT); 389 power_spi_enable(); // Used by DotStar 390 #endif // ATtiny 391 #endif // Data/clock pins 392 #ifdef POWER_PIN 393 digitalWrite(POWER_PIN, LOW); // Power-up LEDs 394 #endif 395 prev = millis(); // Save wake time 396 } 397 398 EMPTY_INTERRUPT(PCINT0_vect); // Pin change (does nothing, but required) 399 #ifndef __AVR_ATtiny85__ 400 ISR(PCINT1_vect, ISR_ALIASOF(PCINT0_vect)); 401 ISR(PCINT2_vect, ISR_ALIASOF(PCINT0_vect)); 402 #endif 403 404 #endif // MOTION_PIN 405 406 // Battery monitoring idea adapted from JeeLabs article: 407 // jeelabs.org/2012/05/04/measuring-vcc-via-the-bandgap/ 408 // Code from Adafruit TimeSquare project, added Trinket support. 409 // In a pinch, the poi code can work on a 3V Trinket, but the battery 410 // monitor will not work correctly (due to the 3.3V regulator), so 411 // maybe just comment out any reference to this code in that case. 412 uint16_t readVoltage() { 413 int i, prev; 414 uint8_t count; 415 uint16_t mV; 416 417 // Select AVcc voltage reference + Bandgap (1.8V) input 418 #ifdef __AVR_ATtiny85__ 419 ADMUX = _BV(MUX3) | _BV(MUX2); 420 #else 421 ADMUX = _BV(REFS0) | 422 _BV(MUX3) | _BV(MUX2) | _BV(MUX1); 423 #endif 424 ADCSRA = _BV(ADEN) | // Enable ADC 425 _BV(ADPS2) | _BV(ADPS1) | _BV(ADPS0); // 1/128 prescaler (125 KHz) 426 // Datasheet notes that the first bandgap reading is usually garbage as 427 // voltages are stabilizing. It practice, it seems to take a bit longer 428 // than that. Tried various delays, but still inconsistent and kludgey. 429 // Instead, repeated readings are taken until four concurrent readings 430 // stabilize within 10 mV. 431 for(prev=9999, count=0; count<4; ) { 432 for(ADCSRA |= _BV(ADSC); ADCSRA & _BV(ADSC); ); // Start, await ADC conv. 433 i = ADC; // Result 434 mV = i ? (1100L * 1023 / i) : 0; // Scale to millivolts 435 if(abs((int)mV - prev) <= 10) count++; // +1 stable reading 436 else count = 0; // too much change, start over 437 prev = mV; 438 } 439 ADCSRA = 0; // ADC off 440 return mV; 441 }