/ Kinetic_POV / poi / poi.ino
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  }