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 }