Before starting explanation about this project I would like to apologized for low quality image and video , but honestly it is really hard to took a sharp and clear image from running POV with normal camera like my mobile camera. It need very fast diaphragm optical lens to capture true motion, But I will upload the better video when I pass my budget limitation and buy CANON camera

What is the POV

POV stand for Persistence Of Vision Globe which is related to phenomenon of human vision. Light stimulus lingers as an aftereffect on the retina for about 1/10 of a second. When light stimuli are sequenced in rapid succession, they merge into one continuous image. In fact it’s the basis for film and television devices, . POV make such illusion (deceive us) and create the image by rotation the array of LED lights around a single point or axis

How POV works

POV displays, a linear (1-dimensional) array of LED lights rotates around a single point, like a bike wheel. By measuring their rotation rate and controlling their flashes with millisecond precision, we can create the illusion of a 2or 3-dimensional image lingering in thin air. Let’s Consider the single frame of any effect (image , text,…), each frame consist of many pixel and hence many lines in plane or spherical area, POV display this imagewith single line of image which is position changed along with its rotation to fill that image , so the problem is how to precisely control LED pixel color in manner of time and space so it could create whole image

POV are categorized base on axis of rotation, type of effect can display and how much color can create.

planar POV

cylindrical POV

spherical POV


in this project 1 meter of 144 APA102 LED strip used to create motion effect and full color image. each image converted to 200 lines of strip by following java code

PImage img,black_b,image_load;
PrintWriter output;
 
int SQL;
float led_t;
byte[] pov_data; 
int line_num=200;
String _OUTPUT="";



void setup()
{
output = createWriter(_OUTPUT); 
black_b= createImage(SQL, SQL, RGB);
black_b.loadPixels();
for (int i = 0; i < black_b.pixels.length; i++) {  black_b.pixels[i] = color(0, 0, 0); }
black_b.updatePixels(); 
background(black_b);
 img = loadImage("myImage.jpg");
}
int l=0;
void draw() 
{
  if(l>=line_num) {noLoop();output.flush();output.close();}
  background(black_b);
  pushMatrix();
             imageMode(CENTER);  
             translate(SQL/2, SQL/2);
              rotate(radians(l*360/line_num));
              image(img, 0, 0);
  popMatrix();
  pushMatrix();
       for(int i=0;i<144;i++)
       {
         color c = get(int(i*led_t+led_t/2), int(SQL/2));
         output.print((char)red(c)+""+(char)green(c)+""+(char)blue(c));     
        // print((char)red(c)+""+(char)green(c)+""+(char)blue(c)+";");
          fill(c);
          rect(i*led_t, (SQL/2)-(led_t/2),led_t,led_t);
      }
    //  println();
   popMatrix();
  // delay(500);
  l++; 
}

void keyPressed() 
{
  output.flush();  // Writes the remaining data to the file
  output.close();  // Finishes the file
  exit();  // Stops the program
}



void mousePressed(){  loop();}

after files pixelized then is ready to upload to the Wifi Controller. in the controller code each file read as per pre defined scenario and will display in the LED strip line by line . to keep POV display fix, hal sensor has been used to check whether new rotation completed. we can display each frame in the every beginning the rotation. more detail available in the following code

static byte L=0;
File FILe;
static uint8_t * _4kbuffer,LED_BUFFER;
bool StartUpShow=true,SHOW=false,Start_new_round=false;
String IMAGES[30]={};
byte  IMAGES_Prior[]={0,1,2,3,4},IMAGE_NUM=0;
int DURATION[]={5,5,5,5,5};
const int IMAGES_LINES=200;
static int image_index=0, Current_imageLine=0;
 
volatile uint32_t t_start_round=0,t_stop_round=0,OpenlastTime=0,lastLineShow=0,lastFrameShow=0;
volatile uint32_t lineInterval=400;

void wireLED(byte Brightness)
{
for (int i = 0; i < 4; i++)SPI.transfer(0);
for (int i = 0; i < LED_NUM*3; )
                            {                            
                              SPI.transfer(0xE0 | Brightness);
                              SPI.transfer(LED_BUFFER[i++]);
                              SPI.transfer(LED_BUFFER[i++]);
                              SPI.transfer(LED_BUFFER[i++]);                               
                            }
for (int i = 0; i < (1 + LED_NUM / 32) * 4; i++) SPI.transfer(0xFF);    //((LED_NUM*3 / 4) + 15) / 16;   ((LED_NUM+1)/16)*4       22- 7-  36          
}

String read_line(String _path,byte line_num){
File f = SPIFFS.open(_path, "r");
if (!f) return "" ; 
byte counter=0; 
String line="";
while(f.available())//1AAA@THAT54545646|1$15:
{ 
 line = f.readStringUntil('\n'); 
 line.trim();
                      
 if(line.length() >2 && counter==line_num) break;
 counter++;
}
 f.close();  
 return line;
}
void Rotate_completed(void)
{
  cli();
  t_stop_round=micros();
   uint32_t   ST_L=(t_stop_round-t_start_round)/(IMAGES_LINES);//spesific time for each line
  lineInterval=(lineInterval+ST_L)/2;  
  t_start_round=t_stop_round;
  Start_new_round=true;
 
  sei();
}
bool read_line(uint16_t _line_num,byte _Image_index)
{
  String path="/SHOW/",line="";
  uint16_t char_count=0;
  byte r=0,g=0,b=0,rgb=0,i=0;
  path+=IMAGES[_Image_index];
 
  File f = SPIFFS.open(path, "r"); 
  if(!f){return false;}
   while(f.available()) { 
                            char c=f.read(); 
                             if( char_count >=(     _line_num*(NUM_LEDS*3+3)   ) )
							{ //line+=c; 
									   if(rgb==0) r=byte(c);
								  else if(rgb==1) g=byte(c);
								  else if(rgb==2) b=byte(c);
								  else           { rgb=0;
												  
												   leds[i] = CRGB(r,g,b);
												   i++; 
												   }
								  rgb++;                             
							}
                             if( char_count > (( _line_num+1)*(NUM_LEDS*3+3)-3 ) ) break;
                           char_count++;
                          
              }
   f.close();   
 
  return true;
}

void sort_image(void)
{
  String copy="";
  byte   due=0,pri=0;
  for(int i=0;i<IMAGE_NUM;i++) IMAGES_Prior[i]=(byte)EEPROM.read(i+20);
  for(int i=0;i<IMAGE_NUM;i++)     DURATION[i]=(byte)EEPROM.read(i+70);
  for(int i=0;i<IMAGE_NUM;i++){
                for(int j=0;j<IMAGE_NUM-1;j++)
                              {
                                if(IMAGES_Prior[j]>IMAGES_Prior[j+1])
                                                  {
                                                    
                                                    copy=IMAGES[j+1];
                                                    due=DURATION[j+1];
                                                    pri=IMAGES_Prior[j+1];
                                                      IMAGES[j+1]=IMAGES[j];
                                                     DURATION[j+1]=DURATION[j];
                                                     IMAGES_Prior[j+1]=IMAGES_Prior[j];
                                                   IMAGES[j]=copy;
                                                   DURATION[j]=due;
                                                   IMAGES_Prior[j]=pri;
                                                  
                                                  }
                               
                              }
              }
}
byte read_image(String _path){
    Dir dir = SPIFFS.openDir(_path);
    int j=0;
    while (dir.next()) IMAGES[j++] = dir.fileName().substring(6);    
    
  return j;    
}
 void readFile(String testFile){ 
 
 uint32_t startTime = millis();
 File f = SPIFFS.open("/SHOW/"+testFile, "r");
 if(!f) {  return; } 
size_t i = f.size();
 
 while(i > 3888){
         f.read(_4kbuffer, 3888);
         optimistic_yield(10000); 
          i -= 3888;
         } 
 f.read(_4kbuffer, i); 
 
 f.close(); 
 uint32_t timeTaken = millis() - startTime;
 
 } 

 void main() {
 SPIFFS.begin();
  IMAGE_NUM=read_image("/SHOW");
  sort_image();
 attachInterrupt(digitalPinToInterrupt(HalSensor), Rotate_completed, FALLING);
 OpenlastTime=millis();
 _4kbuffer = (uint8_t *)malloc(3888); if(_4kbuffer == NULL){ while(1) delay(1000); }  
LED_BUFFER =(uint8_t *)malloc(144*3); if(LED_BUFFER == NULL){ while(1) delay(1000); } 
 for(byte i=0; i<IMAGE_NUM;i++) readFile(IMAGES[i]);
 
 while(1)
 {
	if( (millis()- OpenlastTime) > DURATION[image_index]*1000L )
        {
          
          FILe.close();
          image_index++; 
          if(image_index>IMAGE_NUM)    image_index=0;
           FILe=SPIFFS.open("/SHOW/"+IMAGES[image_index], "r");         
          size_t i = FILe.size();         
          OpenlastTime=millis();
          Current_imageLine = 0;
           L=0;
           finish_chunk=true;
        }
        if(finish_chunk)
        {
                  unsigned long  c=micros();
      FILe.read(_4kbuffer, 3888);
          /*
         int i = 0; 
           while( (i < NUM_LEDS*3*9) && FILe.available())
          { 
               i += FILe.readBytes( ((char*)_4kbuffer) + i, (NUM_LEDS*3*9)-i);
            }*/
         //  
         finish_chunk=false; 
        }
        if(/*Start_new_round && */(micros()-lastLineShow)> 100)
        {
          
          byte *p=&_4kbuffer[L*NUM_LEDS];
          memcpy(LED_BUFFER,p,NUM_LEDS); 
          wireLED(0xFF); // Display the line
          lastLineShow=micros();
          Current_imageLine++;          
          L++;
          if(L>=9) 
                {
                  L=0;
                  finish_chunk=true;
                }                                      
        }
     if(Current_imageLine >= IMAGES_LINES)   
      {        
        Current_imageLine = 0;
        lastFrameShow=micros(); 
        Start_new_round=false; 
        FILe.seek(0, SeekSet);
       // FILe=SPIFFS.open("/SHOW/"+IMAGES[image_index], "r");
       } 
         
      } 
 }
 

What’s my innovation?

I build my first POV about year ago with my former colleague, it built up with one teensy 2.1 board as controller and one SD slat to store image files, although it was successful but I came to notice it’s not very easy to update the images or define effect scenario. For each change it should plug off the power and eject the SD card and re program again.Most POV sign install in the height or roof expose to environment and it is so hard just clime up to change program. So I decided build my new version with Wifi module which so powerful enough and have webserver , so it can easily upload your image and compose any scenario has been desired , following is the snapshot ofwebhost setting

In status setting can modify priority along with duration of each scene and create any desire scenario and sequence , also it shows current Wifi status, with show bottom can run or stop defined scenario

In file setting tab, can upload and remove any image to host, also we could choose for each image which frame location should assigned

In Wifi setting tab, can chooseWifi mode(AP or Station), authorized user for security and other network setting which is normally used for any Wifi module

how to build POV

At first I need the structure to mount POV hub, making the metal or non metal structure is matter of what you have in hands. You can make it with any available material to install it on a wall or add legs to stand alone. My friend makes the simple tripod and mounts the timing belt mechanism to reduce DC motor RPM around 500.

Ideal hub RPM is around 800 and with this RPM each revolution took about 120 ms. my POV has two branches so fps equal around 16 which is little far from ideal 25 fps but the effect quality still good with this setting. My next project will be making POV to show animation and in that case I should use faster motor but let’s put it aside for now.

In the next step piece of PVC cylinder Milled to hold the LED bar.

To connect the hub with pulley shaft one M10 bolt has been bolted to back of PCV part

Two Cupper ring installed on pulley shaft to transmission 5 volts DC to the board and LED strip

For safety power switching installed in sealed plastic box

following is the POV in motion which is recorded by mobile camera, as it stated earlier it is so hard to capture such motion with normal camera