Arduino-based DSM KAP Controller with AutoKAP facility


This page describes a variation on the Arduino-based radio controller described here that adds an AutoKAP facility. The variation involves the following changes:

Note: this version supports a standard servo for panning. I hope to produce a modified version (for Bill Blake) that supports a 360° pan servo.

The 8 DIP switches are assigned like this:

Switches 1 and 2 set the delay time between steps as follows:
2 1 Delay (secs)
Off Off 2
Off On 4
On Off 6
On On 8

Switches 3 and 4 set the pan angle in degrees as follows:
4 3 Pan Angle (°)
Off Off 10
Off On 20
On Off 30
On On 40

Switches 5, 6 and 7 set the 8 Tilt sequence as follows:

I've provided a downloadable zipfile containing all the source code including the two libraries I used.

I bought the following bits of hardware:

The Transmitter Changes

Here's a picture of the transmitter showing the AutoKAP/Manual switch on the top. The DIP switches will be fitted to the side of the box.


New TX Circuit Diagram

Here's the modified TX circuit diagram. .

The Arduino Sketch

I used the standard Arduino IDE (Version 1.6.4) to compile and upload the sketch. Note that Bill and I both failed to compile the sketch using the latest 1.8 version but luckily the 1.6.4 version is available here. Once again, it's important to make sure you select the Sparkfun Pro Micro 3.3V/8MHz board (under the Tools/Board ... menu). The sketch looks like this:


  Sketch for Spektrum DSM TX - Dave Mitchell
  September/November 2016
  April 2017 - added AutoKAP facility
  
  This simple sketch is written for a 3.3v Arduino Pro Micro and assumes:
     a SPEKTRUM X1TX0 2.4GHz transmitter board attached to the TXD serial output pin
     a shutter button attached to pin A3
     a 'pan left' button attached to pin A2
     a 'pan right' button attached to pin A1
     a HoVer switch attached to pin A0
     a tilt potentiometer attached to pin A10
     an AutoKAP switch attached to pin A9
     an MCP23008 port expander connected to pins SDA and SCL and to an 8-way DIP switch
  
  At the receiver, the shutter signal typically operates the shutter of a Canon P&S
  camera via a GentLED device (either using InfraRed or the USB port via CHDK or SDM).
  Panning controls a normal servo geared 4:1 so a 90 degree servo movement translates into 
  a 360 degree pan. A rocker switch pans left or right at a rate of approximately 15 degrees
  a second (4 degrees for the servo). As written, when an attempt is made to rotate past 
  360 degrees, the servo rotation is reversed and slews through 360 degrees instead. 
  A video downlink is advised if the rig is far from the KAPer, otherwise it may be hard 
  to know in which direction the rig is pointing.
  
  The tilt pot is arranged so that a 90 degree turn moves between 0 and some positive value 
  (typically around 320). The code scales this so the actual value sent to the servo varies 
  between 0 and 800 (a 90 degree servo movement). The scale value used here is 2.5 (25/10). 
  
  The sketch supports a 'HoVer' switch to rotate the camera beween Portrait and Landscape.

  It also supports a set of DIP switches to set up and turn on various AutoKAP modes in which
  the TX  sends pan, tilt and shoot commands autonomously. The 8 dipswitches are arranged
  as follows:
  	  1, 2 define 4 delay options (2, 4, 6 or 8 seconds)
  	  3, 4 define 4 pan options (10, 20, 30 or 40 degrees each step)
  	  5, 6, 7 define 8 tilt sequence options
 	  8 determines the pan direction (clockwise or anticlockwise)

  The 8 tilt sequences are the same as the 8 provided by the Gentles ClickPanPro - see
  	http://www.gentles.ltd.uk/clickpan/pro.htm
  AutoKAP is turned on or off by a switch attached to pin A9.
     
  The X1TX0 DSM board I used was extracted from a Spektrum DX4e transmitter. This board 
  expects serial packets (not PPM signals).
     
  Note: with modifications the code should work with DSMX versions too.
     
*/



// The Arduino Wire library uses the SDA and SCL pins to control an MCP23008 port expander
// chip to read the positions of 8 DIP switches.  
#include "Wire.h"


#include "Arduino.h"


// the radio board is controlled by the DSM2-tx library written by Erik Elmore
// see https://github.com/IronSavior/dsm2_tx
// to compile the library I had to make a few changes:
//  1. delete the line
//		#include 
//  2. change all references to 'Serial.' to 'Serial1' (since Serial on an Arduino Pro Micro
//     outputs to the console whereas Serial1 outputs to the TXD pin
       
// Note that there are a few debug outputs to Serial to check that things are working       

#include "dsm2_tx.h"

 DSM2_tx tx(6);	// define a 6 channel transmitter

// buttons are debounced using the Bounce library written by Thomas Fredericks
// See http://www.arduino.cc/playground/Code/Bounce
#include "Bounce.h"

#define BOUNCE_DELAY (10) // in milliseconds

// define the pins we use
#define SHUTTER_BUTTON (A3)
#define PAN_LEFT       (A2)
#define PAN_RIGHT      (A1)
#define HOVER_SW       (A0)
#define TILT_POT       (A10)
#define AUTO_KAP       (A9)


// define some magic numbers that may need modifying
#define T_MIDDLE       (512)	// defines the default for unused channels
#define T_MAX          (800)	// defines the maximum allowed tilt servo value
#define T_SCALE        (25)     // scaling factor for tilt 
#define P_MAX          (1000)	// defines the maximum allowed pan servo value
#define P_MIN          (20)     // defines the minimum allowed pan servo value

#define P_MIDDLE       (510)	// defines the initial pan servo value

#define PADJ           (5)      // pan bump up/down value (15 degrees/sec approx)
#define SADJ           (20)     // slew bump up/down value (60 degrees/sec approx)
#define PINT           (100)    // pan bump interval (100 ms)
#define PRD            (-1)     // pan right/down
#define PSLEWOFF       (0)      // pan slew off
#define PSU            (1)      // pan left/up

#define SHUTTER_UP     (170) 	// defines the 'press the shutter' signal 
#define SHUTTER_DOWN   (800)  	// defines the 'release the shutter' signal
#define HOVER_LAND     (50)   	// defines the HoVer landscape value 
#define HOVER_PORT     (850)  	// defines the Hover portrait value

#define DEG90          (750)	// 90 degrees tilt = 750 
#define DEG3           (25)	// 3 degrees tilt = 25

// define some AUTOKAP values
#define AK_NULL        (0)	// defines a no-op (end of a sequence)
#define AK_TILT        (1)	// defines a tilt command
#define AK_PAN         (2)	// defines a pan command
#define AK_PANTILT     (3)	// defines a pan then tilt command
#define AK_SHOOT       (4)	// defines a shoot command
#define AK_SHOOTOFF    (5)	// defines a shoot off command
#define AK_HO	       (6)	// defines a horizontal command (unused at present
#define AK_VER	       (7)	// defines a vertical command (unused at present)
#define PRESHOOT       (500)	// 1/2 sec settle time before shoot
#define SHOOTDELAY     (50)     // 50ms before release of shutter button


// define the R/C channels we use (Spektrum names - other TX's have different assignments)
#define S_CHANNEL (0) // Throttle used for Shutter
#define P_CHANNEL (1) // Aileron used for Panning
#define T_CHANNEL (2) // Elevator used for Tilting
#define H_CHANNEL (3) // Rudder used for HoVer

/*
 * This callback for the bind process is defined by the DSM2_tx library 
 * It's designed to control the UI during the bind process (not that we have a UI!)
 * Bind completed:  state = 0
 * Bind in progress:  state = 1
 * Bind error:  state = 2
 */
void bind_cb( int state, byte model_id ) {
  if (state == 0 ) {
    Serial.write("Bind complete");
  } else if (state == 1) {
    Serial.write("Bind in progress");
  } else if ( state == 2 ) {
    Serial.write("Bind Error");
  }
}

// state variables
    int tilta; // actual pot reading
	int tilt;  // computed tilt value
	int pan;
	bool panleft;
	bool panright;
	int pslew;
	bool shutter;
	bool hover;
	bool autokap;
	bool oldauto;
	
	unsigned long interval = PINT; 	// 1/10 sec)
	int panadjst  = PADJ;           // approx 15 degrees pan a sec) 
	int slewadjst = SADJ;			 
	unsigned long pst;              // slew timers
	unsigned long mst;
	unsigned long pt;               // pan timers
	unsigned long mt;
	
	int curscript;
	int scriptx;
	long atime;
	long lasta;
	int stepdelay;
	int preshoot;
	int shootdelay;
	int panvalue;
	

// AutoKAP pan and delay values 
	int panvals[4] = {10, 20, 30, 40};
	int delayvals[4] = {2000, 4000, 6000, 8000};

	byte  dipbits;
	int   dipdelay;
	int   dippan;
	int   dipsequence;

struct ScriptItem_s {
  int action;
  int value;
};


// set of 8 ClickPanPro tilt sequence scripts
struct ScriptItem_s scripts[8][28] =
{
  { // off, off, off
    {AK_TILT, 0},
    {AK_SHOOT, 0},
    {AK_SHOOTOFF, 0},
    {AK_PAN, 0},
    {AK_SHOOT, 0},
    {AK_SHOOTOFF, 0},
    {AK_TILT, 30},
    {AK_SHOOT, 0},
    {AK_SHOOTOFF, 0},
    {AK_TILT, 60},
    {AK_SHOOT, 0},
    {AK_SHOOTOFF, 0},
    {AK_TILT, 90},
    {AK_SHOOT, 0},
    {AK_SHOOTOFF, 0},
    {AK_PANTILT, 60},
    {AK_SHOOT, 0},
    {AK_SHOOTOFF, 0},
    {AK_TILT, 30},
    {AK_SHOOT, 0},
    {AK_SHOOTOFF, 0},
    {AK_NULL, 0}
  },
  { // off, off, on
    {AK_TILT, 0},
    {AK_SHOOT, 0},
    {AK_SHOOTOFF, 0},
    {AK_PAN, 0},
    {AK_SHOOT, 0},
    {AK_SHOOTOFF, 0},
    {AK_TILT, 30},
    {AK_SHOOT, 0},
    {AK_SHOOTOFF, 0},
    {AK_TILT, 60},
    {AK_SHOOT, 0},
    {AK_SHOOTOFF, 0},
    {AK_TILT, 90},
    {AK_SHOOT, 0},
    {AK_SHOOTOFF, 0},
    {AK_PAN, 0},
    {AK_SHOOT, 0},
    {AK_SHOOTOFF, 0},
    {AK_TILT, 60},
    {AK_SHOOT, 0},
    {AK_SHOOTOFF, 0},
    {AK_TILT, 30},
    {AK_SHOOT, 0},
    {AK_SHOOTOFF, 0},
    {AK_NULL, 0}
  },
  { // off on off
    {AK_TILT, 0},
    {AK_SHOOT, 0},
    {AK_SHOOTOFF, 0},
    {AK_PAN, 0},
    {AK_SHOOT, 0},
    {AK_SHOOTOFF, 0},
    {AK_TILT, 30},
    {AK_SHOOT, 0},
    {AK_SHOOTOFF, 0},
    {AK_TILT, 90},
    {AK_SHOOT, 0},
    {AK_SHOOTOFF, 0},
    {AK_PAN, 0},
    {AK_SHOOT, 0},
    {AK_SHOOTOFF, 0},
    {AK_TILT, 30},
    {AK_SHOOT, 0},
    {AK_SHOOTOFF, 0},
    {AK_NULL, 0}
  },
  { // off on on
    {AK_TILT, 0},
    {AK_SHOOT, 0},
    {AK_SHOOTOFF, 0},
    {AK_PAN, 0},
    {AK_SHOOT, 0},
    {AK_SHOOTOFF, 0},
    {AK_TILT, 20},
    {AK_SHOOT, 0},
    {AK_SHOOTOFF, 0},
    {AK_PANTILT, 80},
    {AK_SHOOT, 0},
    {AK_SHOOTOFF, 0},
    {AK_TILT, 20},
    {AK_SHOOT, 0},
    {AK_SHOOTOFF, 0},
    {AK_NULL, 0}
  },
  { //on off off
    {AK_TILT, 20},
    {AK_SHOOT, 0},
    {AK_SHOOTOFF, 0},
    {AK_PANTILT, 80},
    {AK_SHOOT, 0},
    {AK_SHOOTOFF, 0},
    {AK_NULL, 0}
  },
  { // on off on
    {AK_TILT, 30},
    {AK_SHOOT, 0},
    {AK_SHOOTOFF, 0},
    {AK_TILT, 0},
    {AK_SHOOT, 0},
    {AK_SHOOTOFF, 0},
    {AK_PAN, 0},
    {AK_SHOOT, 0},
    {AK_SHOOTOFF, 0},
    {AK_NULL, 0}
  },
  { // on on off
    {AK_TILT, 0},
    {AK_SHOOT, 0},
    {AK_SHOOTOFF, 0},
    {AK_PAN, 0},
    {AK_SHOOT, 0},
    {AK_SHOOTOFF, 0},
    {AK_TILT, 45},
    {AK_SHOOT, 0},
    {AK_SHOOTOFF, 0},
    {AK_PANTILT, 90},
    {AK_SHOOT, 0},
    {AK_SHOOTOFF, 0},
    {AK_TILT, 45},
    {AK_SHOOT, 0},
    {AK_SHOOTOFF, 0},
    {AK_NULL, 0}
  },
  { // on on on
    {AK_TILT, 0},
    {AK_SHOOT, 0},
    {AK_SHOOTOFF, 0},
    {AK_PAN, 0},
    {AK_SHOOT, 0},
    {AK_SHOOTOFF, 0},
    {AK_TILT, 45},
    {AK_SHOOT, 0},
    {AK_SHOOTOFF, 0},
    {AK_TILT, 90},
    {AK_SHOOT, 0},
    {AK_SHOOTOFF, 0},
    {AK_PAN, 0},
    {AK_SHOOT, 0},
    {AK_SHOOTOFF, 0},
    {AK_TILT, 45},
    {AK_SHOOT, 0},
    {AK_SHOOTOFF, 0},
    {AK_NULL, 0}
  },
};
	
	    
// model holds current state - set by updateModel()
	struct PanTilt_s {
    	  int pan;
    	  int tilt;
    	  bool hover;
    	  bool shutter;
	} model;
	
    byte modelid;   // DSM transmitters can be used to control multiple models (with
                    // different trims). We're only using one here.

// define the debounced buttons						 
Bounce bouncerS = Bounce(SHUTTER_BUTTON, BOUNCE_DELAY); 
Bounce bouncerL = Bounce(PAN_LEFT,  BOUNCE_DELAY); 
Bounce bouncerR = Bounce(PAN_RIGHT, BOUNCE_DELAY); 
Bounce bouncerH = Bounce(HOVER_SW, BOUNCE_DELAY); 
Bounce bouncerA = Bounce(AUTO_KAP, BOUNCE_DELAY); 

// routine to get all the input values and store them
void getInputs() {
  bouncerS.update();
  bouncerL.update();
  bouncerR.update();
  bouncerH.update();
  bouncerA.update();
  
  autokap  = (bouncerA.read() == LOW);
  if (!autokap) {
    if (oldauto) {
      oldauto = false;
//      Serial.println("AutoKAP off");
    }
	shutter  = (bouncerS.read() == LOW); 	
  	hover    = (bouncerH.read() == LOW); 	
  	panright = (bouncerR.read() == LOW); 	
  	panleft  = (bouncerL.read() == LOW); 

  	// scale so that 90 degree movement of tilt pot equals 90 degree movement of tilt servo
  	tilta = analogRead(TILT_POT);
  	adjustTilt();
  	computePan();      
  } else {
    
  	// we're in AUTOKAP mode
     if (!oldauto) {
      // just switched on
//      Serial.println("AutoKAP on");
      readDIPs(); // sets curscript etc
      oldauto = true;
      lasta = millis();
      scriptx = 0;
      delay = getDelay(curscript, scriptx);
//      Serial.print("First delay ");
//      Serial.println(delay);
    }
        
    // compute new autokap values
    atime = millis();
    if ((atime - lasta) > delay) {
//      Serial.print("Time to do script entry ");
//      Serial.println(scriptx);
      act = scripts[curscript][scriptx].action;
      if (act == AK_NULL) {
        // ignore
      } 
      if (act == AK_TILT || act == AK_PANTILT) {
        tilt = 90 - ( scripts[curscript][scriptx].value * DEG3) / 3;
      } 
      if (act == AK_PAN || act == AK_PANTILT) {
        pan += (DEG3 * panvalue) / 12; // 1 degree at servo = 4 degrees at picavet
        if (pan > P_MAX)
          pan = P_MIN;
      }
      if (act == AK_SHOOT) {
        shutter = true;
//        Serial.println("Shutter true ");
      } 
      if (act == AK_HO) {
        hover = false;
//        Serial.println("Horizontal mode");
      } 
      if (act == AK_VER) {
        hover = true;
//        Serial.println("Vertical mode");
      } 
      if (act == AK_SHOOTOFF) {
        shutter = false;
//        Serial.println("Shutter false");
      }
//      Serial.print("action=");
//      Serial.println(scripts[curscript][scriptx].action);

      lasta = millis();
      scriptx++;
      if (scripts[curscript][scriptx].action == AK_NULL) {
        scriptx = 0;
//        Serial.println("end of script, starting again");
      }
      delay = getDelay(curscript, scriptx);
//      Serial.print("Delaying ");
//     Serial.println(delay);
    }

  }
}  


// routine to get delay for line ix of script sx
int getDelay(int sx, int ix) {
  int d, a;
  a = scripts[sx][ix].action;
  if (a == AK_SHOOT)
    d = preshoot;
  else if (a == AK_SHOOTOFF)
    d = shootdelay;
  else if (a == AK_PAN || a == AK_TILT || a == AK_PANTILT)
    d = stepdelay;
  else
    d = 0;
  return d;
}


// routine to scale and adjust tilt
void adjustTilt() {
  tilt = ((tilta  * T_SCALE) / 90) * 9;
  if (tilt < 0)
     tilt = 0;

  // ensure tilt servo does not move too far beyond 90 degrees   
  if (tilt > T_MAX)
     tilt = T_MAX; 
}

// routine to compute pan servo from state of pan rocker
void computePan() {
  int pa, psa;
  if (pslew != PSLEWOFF) {
//     Serial.println("Slewing");
     psa = pslew * slewadjst;
     mst = millis();
     if (pst == 0) {	// slew switch only just set
//    	Serial.println("Slewing started");
	    pst = mst;
	    pan += psa;
     } else if ((mst - pst) > interval) {	// has it been on long enough for another pan slew?
		   pan += psa;
//    	Serial.print("Slew Pan =");
//    	Serial.println(pan);
		   pt = mt;
     }      
     if (pslew == PSU && pan > P_MAX - SADJ)   // go roughly 360 degrees
       pslew = PSLEWOFF;
     else if (pslew == PRD && pan < P_MIN + SADJ)   
       pslew = PSLEWOFF;    
  } else if (panleft || panright) {
     pa = (panleft ? PLU : PRD) * panadjst;
     mt = millis();
     if (pt == 0) {	// pan switch only just set
//    	Serial.println("Pan Left or Right pressed");
	    pt = mt;
	    pan += pa;
     } else {	
	    if ((mt - pt) > interval) {	// has it been on long enough for another pan bump?
		   pan += pa;
//    	Serial.print("Pan =");
//    	Serial.println(pan);
		   pt = mt;
        }      
     }
     
     // the following tests ensure that rotation can be effectively continuous - on reaching 
     // a stopping point the servo slews forward or back roughly 90 degrees (and the rig 360 degrees) 
     if (pan < P_MIN )
        pslew = PLU;
     else if (pan>P_MAX)
        pslew = PRD;
 } else {
     pt = 0;
     pst = 0;
  }     
}  

// routine to use input values to update model state
void updateModel() { 

//  if (shutter) {
//    Serial.println("Shutter pressed");
//  }		
//  if (hover) {
//  	Serial.println("Hover pressed");
//  }
//  if (model.shutter != shutter)
//     Serial.println("shutter changed");		
//  if (model.pan != pan)
//     Serial.println("pan changed");		
//  if (model.tilt != tilt)
//     Serial.println("tilt changed");		
//  if (model.hover != hover)
//     Serial.println("hover changed");		

  model.shutter = shutter;
  model.pan = pan;
  model.tilt = tilt; 
  model.hover = hover; 
}

// routine to convert model state into a transmitter frame and send it
void transmitFrame() {
  int s, h;
  if (model.shutter)
     s = SHUTTER_DOWN;
  else
     s = SHUTTER_UP;  
  if (model.hover)
     h = HOVER_LAND;
  else 
     h = HOVER_PORT;       
  tx.set_channel(S_CHANNEL,s);
  tx.set_channel(P_CHANNEL, model.pan);
  tx.set_channel(T_CHANNEL, model.tilt);
  tx.set_channel(H_CHANNEL, h);

  // we just set the other two channels to Hover value
  for( byte i = 4; i < tx.channel_count; i++ ) {
    tx.set_channel(i, 0, h);
  }
  tx.send_frame(modelid);
}

// routine to read DIP switches and set AutoKAP values
void readDIPs() {
  Wire.beginTransmission(0x20); 	//starts talking to slave device
  Wire.write(0x09); 			//selects the GPIO pins
  Wire.endTransmission(); 		//stops talking to device
  Wire.requestFrom(0x20, 1); 		// requests one byte of data from MCP23008
  dipbits=Wire.read(); 			// store the incoming byte into inputs
  
  dipdelay = (dipbits & 0x03);		// switches 1 and 2
  dippan =   (dipbits & 0x0c) / 4;	// switches 3 and 4
  dipsequence = (dipbits & 0x70) / 16;	// switches 5, 6 and 7
  dippandir = (dipbits & 0x80)/64; 	// 2 or 0
  pandir = dippandir - 1; 		// +1 or -1
  
  curscript = dipsequence;
  stepdelay = delayvals[dipdelay];
  panvalue = panvals[dippan];
}

// routine executed just once at startup to set the initial state. Note that the buttons 
// and switches are all connected to ground (LOW) when pressed. Defining them as INPUT_PULLUP 
// means that when not pressed they will read HIGH.
void setup() {  
  pinMode(SHUTTER_BUTTON, INPUT_PULLUP);	
  pinMode(PAN_LEFT, INPUT_PULLUP);
  pinMode(PAN_RIGHT, INPUT_PULLUP);
  pinMode(HOVER_SW, INPUT_PULLUP);	
  pinMode(AUTO_KAP, INPUT_PULLUP);	
 
  panleft = panright = shutter = hover = autokap = oldauto = false;
  
  preshoot = PRESHOOT;
  shootdelay = SHOOTDELAY;

  pan = P_MIDDLE;
  tilt = T_MIDDLE;
  mt = pt = mst = pst = 0;
  pslew = PSLEWOFF;
  model.tilt = tilt;
  model.pan = pan;
  model.shutter = shutter;
  model.hover = hover;
  modelid = 0;	
  tx.begin();

// set up DIP switches 
  Wire.begin(); 		//creates a Wire object
  Wire.beginTransmission(0x20); //begins talking to the slave device
  Wire.write(0x00); 		//selects the IODIRA register
  Wire.write(11111111); 	//this sets all 8 pins as inputs 
  Wire.endTransmission(); 	//stops talking to device

  /* at start try binding if shutter button pressed (i.e. OFF) */
  if (!digitalRead(SHUTTER_BUTTON)) {
    	Serial.println("Bind started");
        tx.bind(bind_cb);
       	Serial.println("Bind stopped");
  }  
  Serial.println("setup ended"); 
}

// after setup has executed, this routine is called repeatedly until the TX is switched OFF.
void loop() {
  getInputs();
  updateModel();
  transmitFrame();
}

Version History

Comments, suggestions and bug reports welcome. Dave@zenoshrdlu.com.

For other SDM and CHDK-related stuff of mine, see here and here.