//------------------------------------------------------ // Dodecahedron Spaceship Thing I Dunno // 2026.06.06 RSP // Target: RP2040-Zero (use "Raspberry Pi Pico" board, not Waveshare RP2040-Zero) // (10) hex panels, 7 neopixels each // (?) waistband neopixels // 433 MHz radio receiver module // // User interface: // RF Remote: // Power - toggles on/off // Mode - changes mode // Speed - changes speed // Color - changes color // Onboard LED: // GRN 1/sec heartbeat // RED blinks error code // 1 = DMA channel unavailable // WHITE = learn mode // Press and hold LEARN button for >1 second to start learn mode. // While the LED is white, press and hold the upper left corner button on the remote. //------------------------------------------------------ //#define SERIAL_CONSOLE // enable serial console & debug output #define USE_DMA true // use DMA for long WS2812 string output, otherwise millis() is inaccurate //------------------------------------------------------ // coroutine macros #define coBegin { switch(_state_) { case 0:; #define coEnd _state_ = 0; }} #define coDelay(msec) { _state_ = __LINE__; _tm_=millis(); return; case __LINE__: if (millis()-_tm_ < msec) return; } //------------------------------------------------------------- // buttons const uint LEARN_BTN_PIN = 6; //------------------------------------------------------------ // RC-Switch for RP2040 #include "radioswitch.h" // https://github.com/oshawa-connection/rc-switch-pico RCSwitch rcSwitch = RCSwitch(); const uint RADIO_RECEIVER_PIN = 5; // keypad base address is stored in EEPROM #include // 1306113 uint32_t KEY_BASE = 1306113; // each RF remote has a different base #define KYX(y,x) (y*3+x) // referenced from upper left #define KEY_ON KYX(0,0) #define KEY_MODE_UP KYX(1,1) #define KEY_SPEED_DN KYX(2,0) #define KEY_DEMO KYX(2,1) #define KEY_SPEED_UP KYX(2,2) #define KEY_COLOR_UP KYX(3,0) #define KEY_MODE_DN KYX(3,1) #define KEY_BRITE_UP KYX(3,2) #define KEY_COLOR_DN KYX(4,0) #define KEY_WHI KYX(4,1) #define KEY_BRITE_DN KYX(4,2) #define KEY_RED KYX(5,0) #define KEY_GRN KYX(5,1) #define KEY_BLU KYX(5,2) #define KEY_YEL KYX(6,0) #define KEY_AQUA KYX(6,1) #define KEY_PINK KYX(6,2) //------------------------------------------------------------- #define NUM_MODES 4 #define MIN_BRIGHTNESS 10 #define MAX_BRIGHTNESS 64 #define MIN_SPEED_PCT 10 #define MAX_SPEED_PCT 1000 #define MODE_SOLID 0 #define MODE_WHEEL 1 #define MODE_CHASER 2 #define MODE_MIX 3 struct { boolean onoff; uint mode; uint brightness; uint speed_pct; uint color_index; } app = { false, 0, MIN_BRIGHTNESS, 100, 0 }; //------------------------------------------------------------- // WS2812 LED strings #define PANEL_PIX_PIN 3 #define WAIST_PIX_PIN 4 #define ONBOARD_PIX_PIN 16 #define NUM_PANEL_PIXELS (7*10) #define NUM_WAIST_PIXELS 63 // DMA support // We have 3 pixels strings; the status LED, the panels, and the waist LEDs. // The status LED is one pixel and no problem. // The other two strings are fed by DMA to do the pixel data transfer in background. #if USE_DMA #define PANEL_PIXELS_SHOW startDMA(0); #define WAIST_PIXELS_SHOW startDMA(1); #else #define PANEL_PIXELS_SHOW panel_pixels.show(); #define WAIST_PIXELS_SHOW waist_pixels.show(); #endif #include // Adafruit (blocking) library used for this string since it's only 1 pixel Adafruit_NeoPixel status_pixel(1, ONBOARD_PIX_PIN, NEO_RGB + NEO_KHZ800); // DMA strings // (we let the Adafruit library initialize the PIO, then hack into it) Adafruit_NeoPixel panel_pixels(NUM_PANEL_PIXELS, PANEL_PIX_PIN, NEO_RGB + NEO_KHZ800); Adafruit_NeoPixel waist_pixels(NUM_WAIST_PIXELS, WAIST_PIX_PIN, NEO_RGB + NEO_KHZ800); // DMA output buffers uint32_t panel_DMA_buff[ 2 ][ 3*NUM_PANEL_PIXELS ]; uint32_t waist_DMA_buff[ 2 ][ 3*NUM_WAIST_PIXELS ]; // LED string control struct { int dma_chan; Adafruit_NeoPixel *ada_pix; uint32_t *DMA_buff; uint32_t *pend_buff; boolean pend; } pixstring[2] = { { -1, &panel_pixels, panel_DMA_buff[0], panel_DMA_buff[1], false }, { -1, &waist_pixels, waist_DMA_buff[0], waist_DMA_buff[1], false } }; //------------------------------------------------------ // Heartbeat & Error Indicator #define E_NO_DMA 1 class coStatusLED { private: int _state_ = 0; uint32_t _tm_; // required coroutine state variables uint status = 0; // no error uint blinks; public: void loop() // blink the heartbeat or error { coBegin if (status == 0) { status_pixel.setPixelColor( 0, 0x020000 ); // GRN status_pixel.show(); coDelay(100) status_pixel.setPixelColor( 0, 0 ); status_pixel.show(); } else { for (blinks = status; blinks; blinks--) { status_pixel.setPixelColor( 0, 0x000200 ); // RED status_pixel.show(); coDelay(100) status_pixel.setPixelColor( 0, 0 ); status_pixel.show(); coDelay(250) } } coDelay(900) coEnd } void fault( uint error_code ) // set error, start blinking { #ifdef SERIAL_CONSOLE if (error_code != status) { Serial.print("FAULT "); Serial.println(error_code); } #endif status = error_code; } }; coStatusLED statusLEDtask; //------------------------------------------------------ // Helpers void startDMA( uint index ) // start pixel DMA output { if (pixstring[index].dma_chan == -1) // fallback pixstring[index].ada_pix->show(); else { // if DMA is busy, store the pending update uint32_t *pDMA_buff; if (dma_channel_is_busy( pixstring[index].dma_chan )) { pDMA_buff = pixstring[index].pend_buff; pixstring[index].pend = true; } else // DMA not busy, send it { pDMA_buff = pixstring[index].DMA_buff; pixstring[index].pend = false; } // transfer Adafruit pixels to DMA output buffer uint n = pixstring[index].ada_pix->numPixels(); uint8_t *p = pixstring[index].ada_pix->getPixels(); for (uint i = 0; i < n*3; i++) *(pDMA_buff++) = (*(p++)) << 24; if (pixstring[index].pend) return; // start DMA transfer dma_channel_set_read_addr (pixstring[index].dma_chan, pixstring[index].DMA_buff, false); dma_channel_set_trans_count(pixstring[index].dma_chan, n * 3, true); // start transfer } } uint32_t brightness_adj( uint32_t c, uint brightness ) { uint r = (uint8_t)(c >> 16), g = (uint8_t)(c >> 8), b = (uint8_t)c; r = (r * brightness) >> 8; g = (g * brightness) >> 8; b = (b * brightness) >> 8; return (r << 16) + (g << 8) + b; } //------------------------------------------------------ #define NUM_COLORS 7 uint32_t palette( uint color_index ) { switch (color_index) { case 0: return 0x0000FF; case 1: return 0x00FF00; case 2: return 0xFF0000; case 3: return 0xFF00FF; case 4: return 0x00FFFF; case 5: return 0xFFFF00; case 6: return 0xFFFFFF; default: return 0; } } uint32_t Wheel(uint8_t WheelPos) { WheelPos = 255 - WheelPos; // Section 1: Red to Green transition if (WheelPos < 85) { return ((uint32_t)(255 - WheelPos * 3) << 16) | // Red decreases ((uint32_t)(0) << 8) | // Green is off ((uint32_t)(WheelPos * 3)); // Blue increases } // Section 2: Green to Blue transition else if (WheelPos < 170) { WheelPos -= 85; return ((uint32_t)(0) << 16) | // Red is off ((uint32_t)(WheelPos * 3) << 8) | // Green increases ((uint32_t)(255 - WheelPos * 3)); // Blue decreases } // Section 3: Blue to Red transition else { WheelPos -= 170; return ((uint32_t)(WheelPos * 3) << 16) | // Red increases ((uint32_t)(255 - WheelPos * 3) << 8) | // Green decreases ((uint32_t)(0)); // Blue is off } } //------------------------------------------------------ class sprite_solid { private: int _state_ = 0; uint32_t _tm_; // required coroutine state variables Adafruit_NeoPixel *ada_pix = NULL; uint dma_index; // must align with ada_pix public: void begin( Adafruit_NeoPixel *pix, uint dma ) { _state_ = 0; ada_pix = pix; dma_index = dma; } void loop() { if (ada_pix == NULL) return; coBegin do // forever { // fill solid color ada_pix->setBrightness( app.brightness ); ada_pix->fill( palette( app.color_index ) ); startDMA(dma_index); // pixels.show coDelay( 100 ) // pick up brightness changes } while (1); coEnd } }; class sprite_wheel { private: int _state_ = 0; uint32_t _tm_; // required coroutine state variables Adafruit_NeoPixel *ada_pix = NULL; uint dma_index; // must align with ada_pix uint pixel_size; uint step_msec,adjusted_msec; uint index, shift; uint n,i,j,c; public: void begin( Adafruit_NeoPixel *pix, uint dma, uint pixel_blocksize, uint msec ) { _state_ = 0; ada_pix = pix; dma_index = dma; pixel_size = pixel_blocksize; step_msec = msec; n = ada_pix->numPixels(); } void loop() { if (ada_pix == NULL) return; coBegin do // forever { // fill ada_pix->setBrightness( app.brightness ); for (i=0, shift=0; i < n; i += pixel_size, shift+=50) { c = Wheel( (index+shift) % 256 ); for (j=0; j < pixel_size; j++) if (i+j < n) ada_pix->setPixelColor( i+j, c ); } startDMA(dma_index); // pixels.show adjusted_msec = step_msec * 100 / app.speed_pct; coDelay( adjusted_msec ) // color shift index = (index+1) % 256; } while (1); coEnd } }; struct chaser_pixel { uint color_source; // 0 = app.color int brightness; }; class sprite_chaser { private: int _state_ = 0; uint32_t _tm_; // required coroutine state variables Adafruit_NeoPixel *ada_pix = NULL; uint dma_index; // must align with ada_pix uint n,x,i,b,c,pattern_size,spacer_pixels,msec,adjusted_msec; int j; boolean direction; chaser_pixel *chaser_pattern; public: void begin( Adafruit_NeoPixel *pix, uint dma, uint spacing, chaser_pixel *pattern, boolean dir, uint step_msec ) { _state_ = 0; ada_pix = pix; dma_index = dma; n = ada_pix->numPixels(); for (pattern_size=0; chaser_pattern[pattern_size].brightness != -1; pattern_size++) ; spacer_pixels = spacing; chaser_pattern = pattern; direction = dir; msec = step_msec; } void loop() { if (ada_pix == NULL) return; coBegin do { for (b=0; b < n; b++) { // pick up brightness & color changes ada_pix->setBrightness( app.brightness ); c = palette( app.color_index ); // fill with chaser pattern/s ada_pix->clear(); for (x=0; x + pattern_size < n; x += pattern_size + spacer_pixels) { for (i=0; i < pattern_size; i++) { j = (b+x-i); if (!direction) j = -j; j = (j+n+n) % n; ada_pix->setPixelColor( j, brightness_adj( c, chaser_pattern[i].brightness) ); } } startDMA(dma_index); // pixels.show adjusted_msec = msec * 100 / app.speed_pct; coDelay( adjusted_msec ) } } while (1); coEnd } }; //- - - - - - - - - - - - - - - - sprite_solid sSolid [2]; sprite_wheel sWheel [2]; sprite_chaser sChaser[2]; //- - - - - - - - - - - - - - - - //-- Mode 1 is solid color void mode1Task( boolean reset = false ) { if (reset) { sSolid[0].begin(&panel_pixels, 0); sSolid[1].begin(&waist_pixels, 1); } else { sSolid[0].loop(); sSolid[1].loop(); } } //-- Mode 2 is color wheel void mode2Task( boolean reset = false ) { if (reset) { sWheel[0].begin(&panel_pixels, 0, 7, 1000); sWheel[1].begin(&waist_pixels, 1, 1, 100); } else { sWheel[0].loop(); sWheel[1].loop(); } } //-- Mode 3 is chasers chaser_pixel chaser_single[] = { {0,256}, {0,-1} }; chaser_pixel chaser_w_tail[] = { {0,256}, {0,125}, {0,75}, {0,50}, {0,25}, {0,-1} }; void mode3Task( boolean reset = false ) { if (reset) { sChaser[0].begin(&panel_pixels, 0, 10,chaser_w_tail,true,25); sChaser[1].begin(&waist_pixels, 1, 5,chaser_single,true,25); } else { sChaser[0].loop(); sChaser[1].loop(); } } //-- Mode 4 is wheel panels plus waist chaser void mode4Task( boolean reset = false ) { if (reset) { sWheel [0].begin(&panel_pixels, 0, 7*5, 1000); sChaser[1].begin(&waist_pixels, 1, 10,chaser_w_tail,true,25); } else { sWheel [0].loop(); sChaser[1].loop(); } } void (*modeDispatch[NUM_MODES])(boolean) = { mode1Task, mode2Task, mode3Task, mode4Task }; //------------------------------------------------------ void setup() { #ifdef SERIAL_CONSOLE Serial.begin(115200); delay(10000); Serial.println("Started"); #endif // 433 MHz remote control rcSwitch.enableReceive(RADIO_RECEIVER_PIN); // get keypad address EEPROM.begin(256); EEPROM.get( 0, KEY_BASE ); // buttons pinMode( LEARN_BTN_PIN, INPUT_PULLUP ); // addressable LED strings status_pixel.begin(); status_pixel.clear(); status_pixel.show(); // we'll use adafruit show() for the status pixel panel_pixels.begin(); panel_pixels.clear(); panel_pixels.show(); // Initializes PIO state machine waist_pixels.begin(); waist_pixels.clear(); waist_pixels.show(); // Initializes PIO state machine #if USE_DMA // hook into Adafruit library's PIO // C:\Users\...\Documents\Arduino\libraries\Adafruit_NeoPixel/Adafruit_NeoPixel.h // has been modified to make pio & pio_sm public! for (uint i=0; i < 2; i++) // 2 strings { // show() should have already been called (once) to set up the pio PIO pio = pixstring[i].ada_pix->pio; uint sm = pixstring[i].ada_pix->pio_sm; // claim and configure a DMA channel pixstring[i].dma_chan = dma_claim_unused_channel(false); if (pixstring[i].dma_chan == -1) statusLEDtask.fault(E_NO_DMA); else { dma_channel_config cfg = dma_channel_get_default_config(pixstring[i].dma_chan); channel_config_set_transfer_data_size(&cfg, DMA_SIZE_32); // 32 bit transfers, only high 8 bits are used channel_config_set_read_increment(&cfg, true); channel_config_set_write_increment(&cfg, false); channel_config_set_dreq(&cfg, pio_get_dreq(pio, sm, true)); dma_channel_configure( pixstring[i].dma_chan, &cfg, &pio->txf[sm], NULL, // no transfer address for now 0, // zero count for now false // don't start yet ); } } // end of DMA -> PIO setup, she's armed now #endif // display POST panel_pixels.fill( 0x100000 ); PANEL_PIXELS_SHOW // pixels.show delay(500); panel_pixels.fill( 0x001000 ); PANEL_PIXELS_SHOW // pixels.show delay(500); panel_pixels.fill( 0x000010 ); PANEL_PIXELS_SHOW // pixels.show delay(500); panel_pixels.clear(); PANEL_PIXELS_SHOW // pixels.show delay(500); waist_pixels.fill( 0x100000 ); WAIST_PIXELS_SHOW // pixels.show delay(500); waist_pixels.fill( 0x001000 ); WAIST_PIXELS_SHOW // pixels.show delay(500); waist_pixels.fill( 0x000010 ); WAIST_PIXELS_SHOW // pixels.show delay(500); waist_pixels.clear(); WAIST_PIXELS_SHOW // pixels.show delay(10); // POST completed // initialize LED animation tasks startMode(); } //------------------------------------------------------ void loop() { //-- run animations if (app.onoff) if (app.mode < NUM_MODES) modeDispatch[app.mode](false); //-- monitor RF remote if (rcSwitch.available()) { if (rcSwitch.getReceivedProtocol() == 1 && rcSwitch.getReceivedBitlength() == 24) { // RF command received, try to change a setting if (handleKey( rcSwitch.getReceivedValue() - KEY_BASE )) { ; //Serial.println("KEY HANDLED"); } else #ifdef SERIAL_MONITOR Serial.print("UNRECOGNIZED CODE: "); Serial.println(rcSwitch.getReceivedValue()); #else { ; } #endif } rcSwitch.resetAvailable(); } //-- montor LEARN button // monitor learn button - must hold down for at least 1 second { static uint32_t learn_tm = 0; if (digitalRead(LEARN_BTN_PIN)) learn_tm = millis(); else if (millis()-learn_tm > 1000UL) { // show learn mode indication status_pixel.setPixelColor( 0, 0x101010 ); status_pixel.show(); // wait for several consecutive button codes, which should be the base address uint8_t n = 0; uint32_t addr = 0; for (uint32_t tm=millis(); millis()-tm < 10000UL; ) { if (rcSwitch.available()) { if (rcSwitch.getReceivedValue() != addr) { addr = rcSwitch.getReceivedValue(); n = 0; } else if (++n >= 3) { // update base address & save to EEPROM KEY_BASE = addr; EEPROM.put( 0, KEY_BASE ); EEPROM.commit(); break; } rcSwitch.resetAvailable(); } } status_pixel.setPixelColor( 0, 0 ); status_pixel.show(); } } //-- flush hanging LED string updates for (uint index=0; index < 2; index++) if (pixstring[index].pend) if (!dma_channel_is_busy( pixstring[index].dma_chan )) { // get pending update uint n = pixstring[index].ada_pix->numPixels() * 3; for (uint i=0; i < n; i++) pixstring[index].DMA_buff[i] = pixstring[index].pend_buff[i]; // start DMA transfer dma_channel_set_read_addr (pixstring[index].dma_chan, pixstring[index].DMA_buff, false); dma_channel_set_trans_count(pixstring[index].dma_chan, n * 3, true); // start transfer pixstring[index].pend = false; } //-- heartbeat/error blink statusLEDtask.loop(); } //- - - - - - - - - - - - - - - - void startMode() { if (!app.onoff) { panel_pixels.clear(); PANEL_PIXELS_SHOW // pixels.show waist_pixels.clear(); WAIST_PIXELS_SHOW // pixels.show } else if (app.mode < NUM_MODES) modeDispatch[app.mode](true); } boolean handleKey( int k ) // returns true if key was handled { static uint32_t tm = 0; // repeat defeat timer static uint8_t repeat = 0; // repeat enable if (millis() - tm > 750UL) repeat = 0; switch (k) { case KEY_WHI : app.color_index = 0; app.onoff = true; startMode(); break; case KEY_RED : app.color_index = 1; app.onoff = true; startMode(); break; case KEY_GRN : app.color_index = 2; app.onoff = true; startMode(); break; case KEY_BLU : app.color_index = 3; app.onoff = true; startMode(); break; case KEY_YEL : app.color_index = 4; app.onoff = true; startMode(); break; case KEY_AQUA: app.color_index = 5; app.onoff = true; startMode(); break; case KEY_PINK: app.color_index = 6; app.onoff = true; startMode(); break; case KEY_DEMO: app.onoff = true; // woof //coWaistTask.begin( waist_demo_animation ); break; case KEY_ON : // case KEY_OFF : if (millis() - tm > 500UL) { app.onoff = !app.onoff; startMode(); } tm = millis(); break; case KEY_SPEED_UP: case KEY_BRITE_UP: case KEY_COLOR_UP: case KEY_MODE_UP: case KEY_SPEED_DN: case KEY_BRITE_DN: case KEY_COLOR_DN: case KEY_MODE_DN: if (repeat==0 && millis() - tm > 500UL) { // first press app.onoff = true; bump_value(k); repeat = 1; tm = millis(); } if (repeat==1 && millis() - tm > 500UL) { repeat = 2; } if (repeat == 2) // auto repeat { bump_value(k); tm = millis(); } break; default: return false; } return true; } void bump_value( int k ) { switch (k) { case KEY_MODE_UP: { app.mode = (app.mode+1) % NUM_MODES; startMode(); break; } case KEY_MODE_DN: { app.mode = (app.mode+(NUM_MODES-1)) % NUM_MODES; startMode(); break; } case KEY_COLOR_UP: { if (app.mode == MODE_SOLID || app.mode == MODE_CHASER) app.color_index = (app.color_index+1) % NUM_COLORS; else app.mode = MODE_SOLID; startMode(); break; } case KEY_COLOR_DN: { if (app.mode == MODE_SOLID || app.mode == MODE_CHASER) app.color_index = (app.color_index+(NUM_COLORS-1)) % NUM_COLORS; else app.mode = MODE_SOLID; startMode(); break; } case KEY_BRITE_UP: { if (app.brightness < MAX_BRIGHTNESS) app.brightness++; break; } case KEY_BRITE_DN: { if (app.brightness > MIN_BRIGHTNESS) app.brightness--; break; } case KEY_SPEED_UP: { if (app.speed_pct < MAX_SPEED_PCT) app.speed_pct += 10; break; } case KEY_SPEED_DN: { if (app.speed_pct > MIN_SPEED_PCT) app.speed_pct -= 10; break; } } }