/ Kinetic_POV / dblstaff / dblstaff.ino
dblstaff.ino
  1  // SPDX-FileCopyrightText: 2019 Phillip Burgess for Adafruit Industries
  2  //
  3  // SPDX-License-Identifier: MIT
  4  
  5  /*------------------------------------------------------------------------
  6    POV LED double staff sketch.  Uses the following Adafruit parts
  7    (X2 for two staffs):
  8  
  9    - Pro Trinket 5V https://www.adafruit.com/product/2000
 10    - 2200 mAh Lithium Ion Battery https://www.adafruit.com/product/1781
 11    - LiPoly Backpack https://www.adafruit.com/product/2124
 12    - Tactile On/Off Switch with Leads https://www.adafruit.com/product/1092
 13    - 144 LED/m DotStar strip (#2328 or #2329)
 14      (ONE METER is enough for ONE STAFF, TWO METERS for TWO staffs)
 15    - Infrared Sensor: https://www.adafruit.com/product/157
 16    - Mini Remote Control: https://www.adafruit.com/product/389
 17      (only one remote is required for multiple staffs)
 18  
 19    Needs Adafruit_DotStar library: github.com/adafruit/Adafruit_DotStar
 20  
 21    This is based on the LED poi code (also included in the repository),
 22    but ATtiny-specific code has been stripped out for brevity, since the
 23    staffs pretty much require Pro Trinket or better (lots more LEDs here).
 24  
 25    Adafruit invests time and resources providing this open source code,
 26    please support Adafruit and open-source hardware by purchasing
 27    products from Adafruit!
 28  
 29    Written by Phil Burgess / Paint Your Dragon for Adafruit Industries.
 30    MIT license, all text above must be included in any redistribution.
 31    See 'COPYING' file for additional notes.
 32    ------------------------------------------------------------------------*/
 33  
 34  #include <Arduino.h>
 35  #include <Adafruit_DotStar.h>
 36  #include <avr/power.h>
 37  #include <avr/sleep.h>
 38  #include <SPI.h>
 39  
 40  typedef uint16_t line_t;
 41  
 42  // CONFIGURABLE STUFF ------------------------------------------------------
 43  
 44  #include "graphics.h" // Graphics data is contained in this header file.
 45  // It's generated using the 'convert.py' Python script.  Various image
 46  // formats are supported, trading off color fidelity for PROGMEM space.
 47  // Handles 1-, 4- and 8-bit-per-pixel palette-based images, plus 24-bit
 48  // truecolor.  1- and 4-bit palettes can be altered in RAM while running
 49  // to provide additional colors, but be mindful of peak & average current
 50  // draw if you do that!  Power limiting is normally done in convert.py
 51  // (keeps this code relatively small & fast).
 52  
 53  // Ideally you use hardware SPI as it's much faster, though limited to
 54  // specific pins.  If you really need to bitbang DotStar data & clock on
 55  // different pins, optionally define those here:
 56  //#define LED_DATA_PIN  0
 57  //#define LED_CLOCK_PIN 1
 58  
 59  // Empty and full thresholds (millivolts) used for battery level display:
 60  #define BATT_MIN_MV 3350 // Some headroom over battery cutoff near 2.9V
 61  #define BATT_MAX_MV 4000 // And little below fresh-charged battery near 4.1V
 62  
 63  boolean autoCycle = true; // Set to true to cycle images by default
 64  #define CYCLE_TIME 15     // Time, in seconds, between auto-cycle images
 65  
 66  #define IR_PIN     3      // MUST be INT1 pin!
 67  
 68  // Adafruit IR Remote Codes:
 69  //   Button       Code         Button  Code
 70  //   -----------  ------       ------  -----
 71  //   VOL-:        0x0000       0/10+:  0x000C
 72  //   Play/Pause:  0x0001       1:      0x0010
 73  //   VOL+:        0x0002       2:      0x0011
 74  //   SETUP:       0x0004       3:      0x0012
 75  //   STOP/MODE:   0x0006       4:      0x0014
 76  //   UP:          0x0005       5:      0x0015
 77  //   DOWN:        0x000D       6:      0x0016
 78  //   LEFT:        0x0008       7:      0x0018
 79  //   RIGHT:       0x000A       8:      0x0019
 80  //   ENTER/SAVE:  0x0009       9:      0x001A
 81  //   Back:        0x000E
 82  
 83  #define BTN_BRIGHT_UP    0x0002
 84  #define BTN_BRIGHT_DOWN  0x0000
 85  #define BTN_RESTART      0x0001
 86  #define BTN_BATTERY      0x0004
 87  #define BTN_FASTER       0x0005
 88  #define BTN_SLOWER       0x000D
 89  #define BTN_OFF          0x0006
 90  #define BTN_PATTERN_PREV 0x0008
 91  #define BTN_PATTERN_NEXT 0x000A
 92  #define BTN_NONE         0xFFFF
 93  #define BTN_AUTOPLAY     0x0009
 94  
 95  // -------------------------------------------------------------------------
 96  
 97  #if defined(LED_DATA_PIN) && defined(LED_CLOCK_PIN)
 98  // Older DotStar LEDs use GBR order.  If colors are wrong, edit here.
 99  Adafruit_DotStar strip = Adafruit_DotStar(NUM_LEDS,
100    LED_DATA_PIN, LED_CLOCK_PIN, DOTSTAR_BGR);
101  #else
102  Adafruit_DotStar strip = Adafruit_DotStar(NUM_LEDS, DOTSTAR_BGR); 
103  #endif
104  
105  void     imageInit(void),
106           IRinterrupt(void),
107           showBatteryLevel(void);
108  uint16_t readVoltage(void);
109  
110  void setup() {
111    strip.begin(); // Allocate DotStar buffer, init SPI
112    strip.clear(); // Make sure strip is clear
113    strip.show();  // before measuring battery
114  
115    showBatteryLevel();
116    imageInit();   // Initialize pointers for default image
117  
118    attachInterrupt(1, IRinterrupt, CHANGE); // IR remote interrupt
119  }
120  
121  void showBatteryLevel(void) {
122    // Display battery level bargraph on startup.  It's just a vague estimate
123    // based on cell voltage (drops with discharge) but doesn't handle curve.
124    uint16_t mV  = readVoltage();
125    uint8_t  lvl = (mV >= BATT_MAX_MV) ? NUM_LEDS : // Full (or nearly)
126                   (mV <= BATT_MIN_MV) ?        1 : // Drained
127                   1 + ((mV - BATT_MIN_MV) * NUM_LEDS + (NUM_LEDS / 2)) /
128                   (BATT_MAX_MV - BATT_MIN_MV + 1); // # LEDs lit (1-NUM_LEDS)
129    for(uint8_t i=0; i<lvl; i++) {                  // Each LED to batt level...
130      uint8_t g = (i * 5 + 2) / NUM_LEDS;           // Red to green
131      strip.setPixelColor(i, 4-g, g, 0);
132      strip.show();                                 // Animate a bit
133      delay(250 / NUM_LEDS);
134    }
135    delay(1500);                                    // Hold last state a moment
136    strip.clear();                                  // Then clear strip
137    strip.show();
138  }
139  
140  // GLOBAL STATE STUFF ------------------------------------------------------
141  
142  uint32_t lastImageTime = 0L, // Time of last image change
143           lastLineTime  = 0L;
144  uint8_t  imageNumber   = 0,  // Current image being displayed
145           imageType,          // Image type: PALETTE[1,4,8] or TRUECOLOR
146          *imagePalette,       // -> palette data in PROGMEM
147          *imagePixels,        // -> pixel data in PROGMEM
148           palette[16][3];     // RAM-based color table for 1- or 4-bit images
149  line_t   imageLines,         // Number of lines in active image
150           imageLine;          // Current line number in image
151  volatile uint16_t irCode = BTN_NONE; // Last valid IR code received
152  
153  const uint8_t PROGMEM brightness[] = { 15, 31, 63, 127, 255 };
154  uint8_t bLevel = sizeof(brightness) - 1;
155  
156  // Microseconds per line for various speed settings
157  const uint16_t PROGMEM lineTable[] = { // 375 * 2^(n/3)
158    1000000L /  375, // 375 lines/sec = slowest
159    1000000L /  472,
160    1000000L /  595,
161    1000000L /  750, // 750 lines/sec = mid
162    1000000L /  945,
163    1000000L / 1191,
164    1000000L / 1500  // 1500 lines/sec = fastest
165  };
166  uint8_t  lineIntervalIndex = 3;
167  uint16_t lineInterval      = 1000000L / 750;
168  
169  void imageInit() { // Initialize global image state for current imageNumber
170    imageType    = pgm_read_byte(&images[imageNumber].type);
171    imageLines   = pgm_read_word(&images[imageNumber].lines);
172    imageLine    = 0;
173    imagePalette = (uint8_t *)pgm_read_word(&images[imageNumber].palette);
174    imagePixels  = (uint8_t *)pgm_read_word(&images[imageNumber].pixels);
175    // 1- and 4-bit images have their color palette loaded into RAM both for
176    // faster access and to allow dynamic color changing.  Not done w/8-bit
177    // because that would require inordinate RAM (328P could handle it, but
178    // I'd rather keep the RAM free for other features in the future).
179    if(imageType == PALETTE1)      memcpy_P(palette, imagePalette,  2 * 3);
180    else if(imageType == PALETTE4) memcpy_P(palette, imagePalette, 16 * 3);
181    lastImageTime = millis(); // Save time of image init for next auto-cycle
182  }
183  
184  void nextImage(void) {
185    if(++imageNumber >= NUM_IMAGES) imageNumber = 0;
186    imageInit();
187  }
188  
189  void prevImage(void) {
190    imageNumber = imageNumber ? imageNumber - 1 : NUM_IMAGES - 1;
191    imageInit();
192  }
193  
194  // MAIN LOOP ---------------------------------------------------------------
195  
196  void loop() {
197    uint32_t t = millis(); // Current time, milliseconds
198  
199    if(autoCycle) {
200      if((t - lastImageTime) >= (CYCLE_TIME * 1000L)) nextImage();
201      // CPU clocks vary slightly; multiple poi won't stay in perfect sync.
202      // Keep this in mind when using auto-cycle mode, you may want to cull
203      // the image selection to avoid unintentional regrettable combinations.
204    }
205  
206    // Transfer one scanline from pixel data to LED strip:
207  
208    // If you're really pressed for graphics space and need just a few extra
209    // scanlines, and know for a fact you won't be using certain image modes,
210    // you can comment out the corresponding blocks below.  e.g. disabling
211    // PALETTE8 and TRUECOLOR support can free up nearly 200 bytes of extra
212    // image storage.
213  
214    switch(imageType) {
215  
216      case PALETTE1: { // 1-bit (2 color) palette-based image
217        uint8_t  pixelNum = 0, byteNum, bitNum, pixels, idx,
218                *ptr = (uint8_t *)&imagePixels[imageLine * NUM_LEDS / 8];
219        for(byteNum = NUM_LEDS/8; byteNum--; ) { // Always padded to next byte
220          pixels = pgm_read_byte(ptr++);  // 8 pixels of data (pixel 0 = LSB)
221          for(bitNum = 8; bitNum--; pixels >>= 1) {
222            idx = pixels & 1; // Color table index for pixel (0 or 1)
223            strip.setPixelColor(pixelNum++,
224              palette[idx][0], palette[idx][1], palette[idx][2]);
225          }
226        }
227        break;
228      }
229  
230      case PALETTE4: { // 4-bit (16 color) palette-based image
231        uint8_t  pixelNum, p1, p2,
232                *ptr = (uint8_t *)&imagePixels[imageLine * NUM_LEDS / 2];
233        for(pixelNum = 0; pixelNum < NUM_LEDS; ) {
234          p2  = pgm_read_byte(ptr++); // Data for two pixels...
235          p1  = p2 >> 4;              // Shift down 4 bits for first pixel
236          p2 &= 0x0F;                 // Mask out low 4 bits for second pixel
237          strip.setPixelColor(pixelNum++,
238            palette[p1][0], palette[p1][1], palette[p1][2]);
239          strip.setPixelColor(pixelNum++,
240            palette[p2][0], palette[p2][1], palette[p2][2]);
241        }
242        break;
243      }
244  
245      case PALETTE8: { // 8-bit (256 color) PROGMEM-palette-based image
246        uint16_t  o;
247        uint8_t   pixelNum,
248                 *ptr = (uint8_t *)&imagePixels[imageLine * NUM_LEDS];
249        for(pixelNum = 0; pixelNum < NUM_LEDS; pixelNum++) {
250          o = pgm_read_byte(ptr++) * 3; // Offset into imagePalette
251          strip.setPixelColor(pixelNum,
252            pgm_read_byte(&imagePalette[o]),
253            pgm_read_byte(&imagePalette[o + 1]),
254            pgm_read_byte(&imagePalette[o + 2]));
255        }
256        break;
257      }
258  
259      case TRUECOLOR: { // 24-bit ('truecolor') image (no palette)
260        uint8_t  pixelNum, r, g, b,
261                *ptr = (uint8_t *)&imagePixels[imageLine * NUM_LEDS * 3];
262        for(pixelNum = 0; pixelNum < NUM_LEDS; pixelNum++) {
263          r = pgm_read_byte(ptr++);
264          g = pgm_read_byte(ptr++);
265          b = pgm_read_byte(ptr++);
266          strip.setPixelColor(pixelNum, r, g, b);
267        }
268        break;
269      }
270    }
271  
272    if(++imageLine >= imageLines) imageLine = 0; // Next scanline, wrap around
273  
274    while(((t = micros()) - lastLineTime) < lineInterval) {
275      if(irCode != BTN_NONE) {
276        if(!strip.getBrightness()) { // If strip is off...
277          // Set brightness to last level
278          strip.setBrightness(pgm_read_byte(&brightness[bLevel]));
279          // and ignore button press (don't fall through)
280          // effectively, first press is 'wake'
281        } else {
282          switch(irCode) {
283           case BTN_BRIGHT_UP:
284            if(bLevel < (sizeof(brightness) - 1))
285              strip.setBrightness(pgm_read_byte(&brightness[++bLevel]));
286            break;
287           case BTN_BRIGHT_DOWN:
288            if(bLevel)
289              strip.setBrightness(pgm_read_byte(&brightness[--bLevel]));
290            break;
291           case BTN_FASTER:
292            if(lineIntervalIndex < (sizeof(lineTable) / sizeof(lineTable[0]) - 1))
293              lineInterval = pgm_read_word(&lineTable[++lineIntervalIndex]);
294            break;
295           case BTN_SLOWER:
296            if(lineIntervalIndex)
297              lineInterval = pgm_read_word(&lineTable[--lineIntervalIndex]);
298            break;
299           case BTN_RESTART:
300            imageNumber = 0;
301            imageInit();
302            break;
303           case BTN_BATTERY:
304            strip.clear();
305            strip.show();
306            delay(250);
307            strip.setBrightness(255);
308            showBatteryLevel();
309            strip.setBrightness(pgm_read_byte(&brightness[bLevel]));
310            break;
311           case BTN_OFF:
312            strip.setBrightness(0);
313            break;
314           case BTN_PATTERN_PREV:
315            prevImage();
316            break;
317           case BTN_PATTERN_NEXT:
318            nextImage();
319            break;
320           case BTN_AUTOPLAY:
321  	  autoCycle = !autoCycle;
322            break;
323          }
324        }
325        irCode = BTN_NONE;
326      }
327    }
328  
329    strip.show(); // Refresh LEDs
330    lastLineTime = t;
331  }
332  
333  
334  void IRinterrupt() {
335    static uint32_t pulseStartTime = 0, pulseDuration = 0;
336    static uint8_t  irValue, irBits, irBytes, irBuf[4];
337    uint32_t t = micros();
338    if(PIND & 0b00001000) { // Low-to-high (start of new pulse)
339      pulseStartTime = t;
340    } else {                // High-to-low (end of current pulse)
341      uint32_t pulseDuration = t - pulseStartTime;
342      if((pulseDuration > 4000) && (pulseDuration < 5000)) { // ~4.5 ms?
343        irValue = irBits = irBytes = 0; // IR code start, reset counters
344      } else if(pulseDuration < 2500) { // Data bit?
345        irValue >>= 1;                  // Shift data in, LSB first
346        if(pulseDuration >= 1125) irValue |= 0x80; // Longer pulse = 1
347        if((++irBits == 8) && (irBytes < 4)) { // Full byte recv'd?
348          irBuf[irBytes] = irValue;            // Store byte
349          irValue = irBits = 0;                // and reset counters
350          if((++irBytes == 4) && ((irBuf[2] ^ irBuf[3]) == 0xFF)) {
351            uint16_t code = 0xFFFF;
352            if((irBuf[0] ^ irBuf[1]) == 0xFF) {
353              irCode = (irBuf[0] << 8) | irBuf[1];
354            } else if((irBuf[0] == 0) && (irBuf[1] == 0xBF)) {
355              irCode = irBuf[2];
356            }
357          }
358        }
359      }
360    }
361  }
362  
363  // Battery monitoring idea adapted from JeeLabs article:
364  // jeelabs.org/2012/05/04/measuring-vcc-via-the-bandgap/
365  // Code from Adafruit TimeSquare project, added Trinket support.
366  // In a pinch, the poi code can work on a 3V Trinket, but the battery
367  // monitor will not work correctly (due to the 3.3V regulator), so
368  // maybe just comment out any reference to this code in that case.
369  uint16_t readVoltage() {
370    int      i, prev;
371    uint8_t  count;
372    uint16_t mV;
373  
374    // Select AVcc voltage reference + Bandgap (1.8V) input
375    ADMUX  = _BV(REFS0) |
376             _BV(MUX3)  | _BV(MUX2) | _BV(MUX1);
377    ADCSRA = _BV(ADEN)  |                          // Enable ADC
378             _BV(ADPS2) | _BV(ADPS1) | _BV(ADPS0); // 1/128 prescaler (125 KHz)
379    // Datasheet notes that the first bandgap reading is usually garbage as
380    // voltages are stabilizing.  It practice, it seems to take a bit longer
381    // than that.  Tried various delays, but still inconsistent and kludgey.
382    // Instead, repeated readings are taken until four concurrent readings
383    // stabilize within 10 mV.
384    for(prev=9999, count=0; count<4; ) {
385      for(ADCSRA |= _BV(ADSC); ADCSRA & _BV(ADSC); ); // Start, await ADC conv.
386      i  = ADC;                                       // Result
387      mV = i ? (1100L * 1023 / i) : 0;                // Scale to millivolts
388      if(abs((int)mV - prev) <= 10) count++;   // +1 stable reading
389      else                          count = 0; // too much change, start over
390      prev = mV;
391    }
392    ADCSRA = 0; // ADC off
393    return mV;
394  }