Our Great Maker Giveaway competition is open. $8000 of Maker Favourites up for grabs! × [hide]

empowering creative people

Infinity Mirror Kit - Assembly and tutorials

The Infinity Mirror Kit is a desk-top display that creates a dazzling optical illusion - a tunnel of light that tears through space!


Assembly

This video will guide you through assembling your Infinity Kit. If you prefer working off paper, you can download the instruction sheet. The steps are:

  • Preparation and tools required 
  • Prepare front panel (0:00 - 1:17)
  • Prepare front mirror (1:17 - 5:35)
  • Assemble mirror-box (5:35 - 6:16)
  • Prepare the LED strip (6:16 - 10:15)
  • Mount the LED strip (10:15 - 14:08)
  • Close the mirror-box (14:08 - 15.37)
  • Route wiring (15:37 - 16:45)
  • Assemble control-box (16:45 - 18:32)
  • Prepare potentiometer (18:32 - 19:56)
  • Solder Photon connections (19:56 - 23:54)
  • Test and complete (23:54 - end)

You did it! Nothing beats the feeling of a completed project! Your Infinity Mirror Kit will serve as a mesmerising piece of desk-top tech with it's default program. Check out the following tutorials If you'd like to learn how to upload your own code to your Infinity Kit.


Tutorial 1 - Claiming your Photon and flashing code

In this tutorial you’ll set up a Particle account, claim the Particle Photon that drives your Infinity Kit, and see how to flash a program to your Infinity Kit by making a slight tweak to its default code.

You will need:

  • An Infinity Mirror Kit
  • A Wi-Fi network with internet access
  • A smartphone, with the Particle App installed – this is for managing the claiming process.

If you don’t have a smartphone available, check out the Particle docs for help connecting over USB.

Plug in your Infinity Kit and rotate the knob all the way to the left (OFF). Press the SETUP button briefly. Your Photon’s status LED should start blinking blue – it’s now in listening mode, waiting for your input to connect to Wi-Fi.

On your phone, download and open the Particle app. Create a Particle account (or log in if you have one already) and follow the prompts to add a device to your account.

During this process you’ll provide Wi-Fi credentials to your Photon, allowing it to connect to the internet. You can also give your Photon a name. I find it helpful to use names related to the project, so “InfinityKit” works for me.

Once your Photon has been successfully claimed, briefly pressing the SETUP button will cause your Photon to connect to WiFi - it will flash green then breathe cyan once connected.

Navigate to www.build.particle.io and log in with your Particle account, then open the IDE. You will be greeted with a blank Particle App.

Setting up a project

Start by giving your project a title (top-left). We're going to make some small edits to the default firmware. Copy the code attached with this tutorial and paste it on top of the empty setup() and loop() functions. Next, navigate to the libraries menu particle-ide-libraries and include the neopixel library in your project. We now have all the ingredients that make our Infinity Kit work. The main code controls how the Infinity Kit behaves, and the library contains instructions and methods for the Photon to control the LED strip.

Check that your Photon is connected, in the devices menu  particle-ide-devices . A breathing, cyan dot next to your Photon's name indicates it is connected. A yellow start next to your Photon's name indicates if it is selected to flash code to.

Uploading the following code won't do anything, because it is the same code your Infinity Kit is currently running. Scroll down to line 229. This should read as: delay(42). Here, 42 is the time, in milliseconds, between frames of animation for the rainbow mode. Changing the 42 to a small number like 1 or 0 will make the rainbow mode very fast, while changing it to something large like 100 or more will make it visibly slower. For now, change this number to zero, then click the Flash button particle-ide-flash.

After a moment of compiling and uploading, your Infinity Kit should behave exactly as before, albiet with a different speed for the rainbow mode! Revel in this moment, for you have somehow programmed a piece of hardware from your internet browser!

Next up, we'll take a closer look at the Infinity Kit code!

The code for this video is the default firmware that your Infinity Kit shipped with.

/*
* This program drives the Core Electronics Infinity Kit
* http://coreelec.io/infinitykit
* 2017
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program.  If not, see <http://www.gnu.org/licenses/>.
*
*/

#include "Particle.h"
#include <math.h>
#include <neopixel.h>


/*
 * AUTOMATIC: Photon must have a valid WiFi network to begin running this code.
 * SEMI_AUTOMATIC: Photon will run this code immediately, and attempt to connect to a WiFi network only if SETUP button is pressed.
 */
SYSTEM_MODE(SEMI_AUTOMATIC);



/*******************************************************************************
* Hardware Definitions
* You won't need to change these
******************************************************************************/
const int strip_pin = 0; // The digital pin that drives the LED strip
const int num_leds = 44; // Number of LEDs in the strip. Shouldn't need changing unless you hack the hardware
const int pot = 0; // Potentiometer pin selects the mode
const int ADC_precision = 4095; // Particle use 12bit ADCs. If you wish to port to a different platform you might need to redefine the ADC precision eg: 1023 for Arduino UNO


/*******************************************************************************
* Global Variables
*
******************************************************************************/
// States for the state-machine
enum statevar {
  state_off,
  state_rainbow,
  state_brightness,
  state_comet,
  state_solid,
  state_scroll,
};

static uint8_t state; // The state that the user demands
static uint8_t state_current; // The state currently being executed

Adafruit_NeoPixel strip(num_leds, strip_pin, WS2812B);
static uint32_t ledBuffer[num_leds]; // Buffer for storing (and then scaling if necessary) LED R,G,B values.
static float userBright = 1.0; // User-set brightness [0 - 1] // TODO roll into LED-strip object


/*******************************************************************************
* SETUP
*
******************************************************************************/

void setup()
{
  strip.begin();
  update(); // Initialize all pixels to off
}



/*******************************************************************************
* LOOP
*
******************************************************************************/
void loop()
{
  static uint32_t userColour = Wheel(0); // User-set colour TODO roll into LED-strip object

  connectWIFIonButtonPress();

  // Read potentiometer values for user-input
state = getState(pot); // Select the operation mode // State Machine. switch(state){ case state_off: clearStrip(); // "Off" state. delay(50); break; case state_rainbow: rainbow(); // Adafruit's rainbow demo, modified for seamless wraparound. We are passing the Pot # instead of the option because delay value needs to be updated WITHIN the rainbow function. Not just at the start of each main loop. break; case state_brightness: brightness(userColour); break; case state_comet: comet(userColour); // An under-construction comet demo. break; case state_solid: solid(userColour); break; case state_scroll: userColour = scroll(); break; default: // If getState() returns some unexpected value, this section will execute break; } } /******************************************************************************* * Functions * ******************************************************************************/ /* * Connect to stored WiFi credentials. Only useful if you have claimed your * particle photon to your particle account: https://build.particle.io */ void connectWIFIonButtonPress() { if (System.buttonPushed() > 1) { if( !Particle.connected() ) Particle.connect(); } } // Break potentiometer rotation into sectors for setting mode // This is the function that determines the order that settings are available from the user-input pot. uint8_t getState(int pot){ // TODO: find better, more flexible method of defining pot sectors? Remove magic number? float val = float(analogRead(pot)) / float(ADC_precision); if (val < 0.05) { return state_off; } else if (val < 0.25) { return state_rainbow; }else if (val < 0.5) { return state_scroll; } else if (val < 0.75) { return state_comet; } else if (val < 0.95) { return state_solid; } else { return state_brightness; } } /* Run the comet demo * This feature is largely experimental and quite incomplete. * The idea is to involve multiple comets that can interact by colour-addition */ void comet(uint32_t colour){ state_current = state_comet; uint16_t i, j, k; uint16_t ofs = 15; for (j=0; j<strip.numPixels(); j++){ clearStrip(); drawComet(j, colour); update(); delay(30); if(getState(pot) != state_current) break; // Check if mode knob is still on this mode } } /* * Draw a comet on the strip and handle wrapping gracefully. * TODO: * - Handle multiple comets */ void drawComet(uint16_t pos, uint32_t colour) { float headBrightness = 255; // Brightness of the first LED in the comet uint8_t bright = uint8_t(headBrightness); // Initialise the brightness variable uint16_t len = 20; // Length of comet tail double lambda = 0.3; // Parameter that effects how quickly the comet tail dims double dim = lambda; // initialise exponential decay function // Get colour of comet head, and prepare variables to use for tail-dimming uint8_t headR, headG, headB, R, G, B; colourToRGB(colour, &headR, &headG, &headB); R = headR; G = headG; B = headB; setPixel(pos, colour); // Head of comet for(uint16_t i=1; i<len; i++){ // Figure out if the current pixel is wrapped across the strip ends or not, light that pixel if( pos - i < 0 ){ // Wrapped setPixel(strip.numPixels()+pos-i, strip.Color(R,G,B)); } else { // Not wrapped setPixel(pos-i, strip.Color(R,G,B)); } R = uint8_t(headR * exp(-dim)); // Exponential decay function to dim tail LEDs G = uint8_t(headG * exp(-dim)); B = uint8_t(headB * exp(-dim)); dim += lambda; } } void rainbow() { // uint16_t j; float i, baseCol; float colStep = 256.0 / strip.numPixels(); for(baseCol=0; baseCol<256; baseCol++) { // Loop through all colours for(i=0; i<strip.numPixels(); i++) { // Loop through all pixels setPixel( i, Wheel(int(i*(colStep)+baseCol) & 255) ); // This line seamlessly wraps the colour around the table. } update(); delay(42); if(getState(pot) != state_rainbow) break; // Check if mode knob is still on this mode } } // Input a value 0 to 255 to get a color value. // The colours are a transition r - g - b - back to r. uint32_t Wheel(byte WheelPos) { if(WheelPos < 85) { return strip.Color(WheelPos * 3, 255 - WheelPos * 3, 0); } else if(WheelPos < 170) { WheelPos -= 85; return strip.Color(255 - WheelPos * 3, 0, WheelPos * 3); } else { WheelPos -= 170; return strip.Color(0, WheelPos * 3, 255 - WheelPos * 3); } } // Display a single colour on all LEDs. While accessible from the state machine, this function does not set/check // the state variables, because it is called by other states. void solid(uint32_t colour){ // Do not set state_current here. uint16_t i; // User-defined colour for(i=0; i<strip.numPixels(); i++){ setPixel(i, colour); } update(); delay(50); } // Scroll through the colour wheel for all LEDs. Also allows user to set a desired colour for other modes. uint32_t scroll() { static int colPos = 0; // Position in colour wheel uint32_t colour = Wheel(colPos++ & 255); // Show the colour solid(colour); return colour; // Return the set colour for use in other sequences. } // Fade the brightness up down and update a brightness parameter for other modes. void brightness(uint32_t col) { state_current = state_brightness; const float maxBright = 1.0; // Leave this as 1. Taking it above 1 won't make things brighter. const float minBright = 0.01; // must be strictly greater than zero // glow on to max for ( userBright; userBright < maxBright; userBright += 0.05*userBright ){ solid(col); if(getState(pot) != state_current) break; // Check if mode knob is still on this mode } userBright = min(userBright, maxBright); // Prevent overshooting 1.0 // hold at max for a moment for (int i = 0; i < 20; i++) { if(getState(pot) != state_current) break; // Check if mode knob is still on this mode solid(col); } // glow down to min for ( userBright; userBright > minBright; userBright -= 0.05*userBright ){ solid(col); if(getState(pot) != state_current) break; // Check if mode knob is still on this mode } userBright = max(minBright, userBright); // Prevent undershoot userBright = max(userBright, 0); // Prevent dead-locking at zero, just in case user ignored minBright requirements // hold at min for a moment for (int i = 0; i < 20; i++) { if(getState(pot) != state_current) break; // Check if mode knob is still on this mode solid(col); } } /*************************************************************************************************** 8888888b. 8888888888P 888 "Y88b d88P 888 888 d88P 888 888 8888b. 88888b. .d88b. .d88b. 888d888 d88P .d88b. 88888b. .d88b. 888 888 "88b 888 "88b d88P"88b d8P Y8b 888P" d88P d88""88b 888 "88b d8P Y8b 888 888 .d888888 888 888 888 888 88888888 888 d88P 888 888 888 888 88888888 888 .d88P 888 888 888 888 Y88b 888 Y8b. 888 d88P Y88..88P 888 888 Y8b. 8888888P" "Y888888 888 888 "Y88888 "Y8888 888 d8888888888 "Y88P" 888 888 "Y8888 888 Y8b d88P "Y88P" Changing the code below this line could REALLY DAMAGE your Infinity Mirror. ***************************************************************************************************/ /** * Current-Limiting code * As it stands, if the user manually drives the LED strip, there exists the ability to drive the strip to ~1.5 A. * The LED strip is powered from the Vin pin, which can supply only 1.0 A. * The following code serves as wrappers around Adafruit's NeoPixel function calls that scales the user-demanded * LED values if they would result in LEDs drawing too much current * */ // Wrapper for LED buffer void setPixel(int ledIndex, uint32_t colour){ ledBuffer[ledIndex] = colour; } // Wrapper for safe pixel updating - prevent user from requesting too much current // TODO refactor, retain brightness adjusted calculations through the function to avoid re-computing and improve readability void update(){ uint8_t R, G, B; const float iLim = 0.87; // [A] Current limit (0.9A) for external power supply or 1A capable computer USB port. // const float iLim = 0.35; // [A] Current limit for 500mA computer USB port // const float iLim = 10; // DANGER effectively DISABLE current limiting. const float FSDcurrentCorrection = 0.8824; // "Full-scale deflection" correction. The LED response is nonlinear i.e. Amp/LeastSignificantBit is not a constant. This is an attempt to allow the user to specify maximum current as a real value. float lsbToAmp = 5.06e-5; // [LSB/Ampere] the relationship between an LED setting and current float sum = 0; // Initial sum of currents // Sum the LED currents for(uint8_t i=0; i<strip.numPixels(); i++) { // Separate the 32bit colour into 8bit R,G,B then scale to the desired brightness colourToRGB(ledBuffer[i], &R, &G, &B); applyBrightness(&R,&G,&B); sum += float(R + G + B) * lsbToAmp; // Add LED[i]'s current } sum = sum * FSDcurrentCorrection; float scale = float(iLim)/float(sum); if ( sum > iLim ) { // Too much current requested for(uint8_t i=0; i<strip.numPixels(); i++) { // Separate the 32bit colour into 8bit R,G,B then scale to the desired brightness colourToRGB(ledBuffer[i], &R, &G, &B); applyBrightness(&R,&G,&B); // Scale down to current limit R = floor(R * scale); G = floor(G * scale); B = floor(B * scale); strip.setPixelColor(i, R, G, B); } } else { for(uint8_t i=0; i<strip.numPixels(); i++) { // Separate the 32bit colour into 8bit R,G,B then scale to the desired brightness colourToRGB(ledBuffer[i], &R, &G, &B); applyBrightness(&R,&G,&B); strip.setPixelColor(i, R, G, B); } } strip.show(); } // INPUT:32-bit colour and OUTPUT: 8-bit R,G,B void colourToRGB(uint32_t col, uint8_t *R, uint8_t *G, uint8_t *B) { *B = col & 0xFF; *G = (col >> 8) & 0xFF; *R = (col >> 16) & 0xFF; } // Scale the demanded colour by the user set brightness 0-100% void applyBrightness(uint8_t *R, uint8_t *G, uint8_t *B) { *B = userBright * *B; *G = userBright * *G; *R = userBright * *R; } // Clear all leds and update. Clears LED buffer too. void clearStrip(void){ for(uint8_t i=0; i<strip.numPixels(); i++){ setPixel(i, strip.Color(0,0,0)); } update(); }


Tutorial 2 - Create your own mode

If you're thinking of doing more than just tuning some parameters, this tutorial is for you. We'll dive into the default firmware, and write our own mode from scratch.

You can get the full firmware for this tutorial here, but we reccommend following the tutorial and making the changes yourself.

First, we have to add our new mode the the list of modes that exist. This is handled by an enum, which assigns some constant numbers to names of our choosing. 

enum statevar {
state_off,
state_rainbow,
state_brightness,
state_comet,
state_solid,
state_scroll,
state_edge
};

In this case, state_off has adopted the value of 1, state_rainbow has adopted 2 and so on. Note that we don't really care what the numbers are, so we don't explicitly set them. The point here is that we can refer to modes by their names, rather than some arbitrary number which is not important. I've already added our new mode (state_edge) to the end of the enum.

Let's take a look at the structure of the main loop. The first thing to do is determine which mode the user has set.

// Read potentiometer values for user-input
state = getState(pot); // Select the operation mode

The state variable tells the Photon what mode we want to be in (I'll use the words mode and state interchangeably throughout this video). The getState function reads the angle of the potentiometer, and writes some value into state. We'll have to modify getState to return a value for our new mode.

Next up, we hit the "State Machine"

  // State Machine.
switch(state){
case state_off:
clearStrip(); // "Off" state.
delay(50);
break;

case state_rainbow:
rainbow(); // Adafruit's rainbow demo, modified for seamless wraparound. We are passing the Pot # instead of the option because delay value needs to be updated WITHIN the rainbow function. Not just at the start of each main loop.
break;

.
.
.

default: // If getState() returns some unexpected value, this section will execute
break;

}

The state machine just runs whichever mode the user demanded with the potentiometer position. The switch-case statement checks the value of state, and if it matches any of the predefined constants state_off, state_rainbow etc. then that mode is entered. Switch-case is similar to a long if-else chain, but a lot more scalable - we just have to create a case entry for our new mode. Insert the following into the switch-case:

case state_edge:
edge(userColour);
break;

Finally, there's the writing of the new mode. In this case it's just a function with some loops.

void edge(uint32_t colour){
  state_current = state_edge;
  int edgeLength = strip.numPixels()/4;

  // grow line along 4 edges
  for(int i = 0; i < edgeLength; i++){
    setPixel(i, colour);                // First edge
    setPixel(i +   edgeLength, colour); // second edge
    setPixel(i + 2*edgeLength, colour); // third edge
    setPixel(i + 3*edgeLength, colour); // fourth edge
    update();
    delay(50);
    if(getState(pot) != state_current) break; // Check if mode knob is still on this mode
  }

  // shrink line along 4 edges
  for(int i = 0; i < edgeLength; i++){
    setPixel(i, strip.Color(0,0,0));
    setPixel(i +   edgeLength, strip.Color(0,0,0));
    setPixel(i + 2*edgeLength, strip.Color(0,0,0));
    setPixel(i + 3*edgeLength, strip.Color(0,0,0));
    update();
    delay(50);
    if(getState(pot) != state_current) break; // Check if mode knob is still on this mode
  }

}

That's the structure of adding new modes to the default code! To recap, we need to:

  • Add our new mode to the enum call. This is the list of all modes that can exist.
  • Have getState to return the new mode when some condition is met eg. potentiometer is in the correct position.
  • Add an entry for our mode into the switch-case state machine.
  • Actually write some function to drive the mode.

This might seem like a lot of unnecessary work, but it allows us to scale our program easily.

Writing your own code (a warning)

Of course, you don't have to stick with the above structure if you don't want to - if you want to hack your Infinity Kit to do something specific there's a good chance you'll have to move away from the default firmware. To go fully offroad and write your own application there's a couple of things you need:

  • The neopixel library
  • Code that's in the Danger Zone

The Danger Zone is the code that you'll find towards the bottom of the default firmware - it handles driving the LEDs safely.

If you've used neopixels / WS2812 LEDs before, you might be familiar with the neopixel library which lets you drive the LEDs with calls to setPixelColour() and show().

With the Infinity Kit, you should not call those functions directly - We've written our own functions that wrap around setPixelColour() and show() which you should call instead. This is because it is possible to drive the LEDs with more current than the Photon is capable of supplying, and these wrapper-functions handle limiting the current to a safe amount.

In summary:

  • Use: setPixel(), update()
  • Don't use: setPixelColour, show()


Resources

  • Github repository - Get the latest code or even contribute for some sweet maker-cred!
  • Particle docs - An indispensable resource when working with Particle gear. Particularly userful sections are linked below.

Attachment: assembly-instructions-171109.pdf

The Infinity Mirror Kit is a desk-top display that creates a dazzling optical illusion - a tunnel of light that tears through...