Arduino-based KAP transmitter supporting AutoKAP and a 360° servo for panning


This page describes an Arduino-based KAP transmitter which supports a 360° servo for panning and an AutoKAP switch that makes the Arduino read the values of a set of 8 DIP switches. The HoVer rig it controls is described here. A simpler version, that does not have AUTOKap capability and uses a geared standard servo for panning, is described here.

I've written some illustrated construction notes for the TX.

The TX design has gone through various iterations - an earlier version using an MCP23008 port extender failed to work properly, as did a version using a 10-turn pot to trim the pan servo. Here's a circuit diagram of the current (working) transmitter (ably drawn by Bill Blake):

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:

Switch 8 sets the direction of panning clockwise or anti-clockwise.

TX Component List

I used a wire wrap tool for all the connections to the Arduino - when I've soldered these connections I've sometimes caused damage to the Arduino.

I used the following components for the TX:

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.

A zip file containing the sketch and libraries is available for downloading.


  Sketch for Spektrum DSM TX - Dave Mitchell
  September 2017 - amended to add AutoKAP and 360 servo capability
  
  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 'trim right' button attached to pin 2
     a 'trim left' button attached to pin 3
     a shutter button attached to pin 4 
     a 'pan right' button attached to pin 5
     a 'pan left' button attached to pin 6
     a HoVer switch attached to pin 7
     a tilt potentiometer attached to pin 8
     an AutoKAP switch attached to pin 9 
     an 8-way DIP switch connected to pins 10, 16, 14, 15, 18, 19, 20 and 21   

  A rocker switch pans left or right. Panning controls a 360 degree continuous rotation 
  servo. It is very hard to adjust such servos so they don't move at all for a neutral 
  signal, so a pair of trim buttons are used to adjust the neutral point. Creep can also
  be eliminated by removing the servo's pot and replacing it with a small multi-turn pot 
  to the servo. See http://www.gentles.ltd.uk/control/servomods/index.htm
  In this sketch the neutral point (panadjust) is initialised to 470 in the setup() routine.

  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 of the pot results in an analog reading
  by the Arduino 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). To avoid jerks, tilts are slewed, 
  in a series of small steps using a "successive approximation" algorithm (thanks to James
  Gentles):
  		   new_tilt = ((current_tilt*15) + target_tilt)/16;
  Given the arduino invokes the loop routine around 300 times a second, the 30-50 steps the
  algorithm typically takes to slew does not impact things much.
  
  The sketch supports a 'HoVer' switch to rotate the camera beween Portrait and Landscape.
 
  It also supports a set of dipswitches to set up and turn on various AutoKAP modes in which
  the TX sends pan, tilt and shoot commands autonomously. These switches are connected to 
  pins 10, 16, 14, 15, 18, 19, 20 and 21 of the Arduino. 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 and 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 9.
    
  The X1TX0 DSM board I used came from a Spektrum DX4e transmitter. This board expects
  serial packets (not PPM signals). The Spektrum DX5e (and DX6i?) use the same r/f board.
     
  Note: with modifications the code should work with DSMX versions too.
     
*/

#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 to dsm2_tx.cpp:
//  1. delete the line
//		#include <cstring>
//  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 in the sketch  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 TRIM_LEFT      2 
#define TRIM_RIGHT     3 
#define SHUTTER_BUTTON 4 
#define HOVER_SW       7 
#define TILT_POT       8
#define AUTO_KAP       9
// define the pins the DIP switches are connected to
#define DIP1	       10
#define DIP2	       16
#define DIP3	       14
#define DIP4	       15
#define DIP5	       18
#define DIP6	       19
#define DIP7	       20
#define DIP8	       21


// 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 PAN_NEUTRAL    (475)    // this is the neutral point for my 360 servo

#define DIFF  (25)		//	pan bump up/down from middle (stop) position - determines panning rate

//  defs for IR Gentled shutter control (USB needs different values)
#define SHUTTER_UP     (170) 	// defines the 'press the shutter' signal 
#define SHUTTER_DOWN   (800)  	// defines the 'release the shutter' signal
#define SHUTTER2_UP   (512)  	// defines the second 'release the shutter' signal
#define SHUTTER2_DOWN (170) 	// defines the second 'press the shutter' signal 

#define HOVER_LAND     (50)   	// defines the HoVer landscape value 
#define HOVER_PORT     (850)  	// defines the Hover portrait value

#define DEG3           (27)		// 3 degrees tilt = 27

// 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 P_SCALE        (40)	    // scaling factor for AutoKAP pan 



// define the R/C channels we use (Spektrum names - other TX's have different assignments)

// The sketch supports two shutter channels. The Throttle channel is set up for 
// an IR Gentled, while the Gear channel is set up for a USB Gentled.
#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
#define S2_CHANNEL (4)// Gear used for second Shutter channel

/*
 * 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;   	// current pan
	int panadjust;	// pan value to give stationary servo
	bool panleft;	// left switch value
	bool panright;	// right switch value
	bool shutter;	// shutter switch value
	bool hover;		// HoVer switch value
	bool autokap;	// AutoKAP switch value
	bool oldauto;	// used to detect AutoKAP switch change

	// AutoKAP script variables
	int  curscript;	// the current AutoKAP script
	int  scriptx;	// index of current action
	unsigned long atime;	// current time
	unsigned long lasta;	// time of last action
	long ptime;		// pan pulse timer
	unsigned long stepdelay;// time in millis to next step
	unsigned long basedelay;// time in millis between shots
	int  panvalue;	// pan pulse size
	int  pandir;	// pan direction

	int panvals[4] = {10, 20, 30, 40};		// in degrees?>
	
// at runtime each shutter press is delayed by PRESHOOT so the rig has steadied and each 
// shutter press release takes SHOOTDELAY, so we will need to subtract PRESHOOT+SHOOTDELAY 
// so the delay between shots is correct
long delayvals[4] = {2000, 4000, 6000, 8000};	// in ms
		
    int DIPswitches[8] = {DIP1, DIP2, DIP3, DIP4, DIP5, DIP6, DIP7, DIP8};

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

// script action object   
	struct ScriptItem_s {
	  int action;	// action type
	  int value;	// action value
	};

// the 8 ClickPanPro scripts
// each script consists of up to 28 actions
struct ScriptItem_s scripts[8][28] =
{
  { // off, off, off - sequence 0
    {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 - sequence 1
    {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 - sequence 2
    {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 - sequence 3
    {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 - sequence 4
    {AK_TILT, 20},
    {AK_SHOOT, 0},
    {AK_SHOOTOFF, 0},
    {AK_PANTILT, 80},
    {AK_SHOOT, 0},
    {AK_SHOOTOFF, 0},
    {AK_NULL, 0}
  },
  { // on off on - sequence 5
    {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 - sequence 6
    {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 - sequence 7
    {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);
Bounce bouncerTL= Bounce(TRIM_LEFT, BOUNCE_DELAY);
Bounce bouncerTR= Bounce(TRIM_RIGHT,BOUNCE_DELAY);


// routine to get all the input values and store them
void getInputs() {
  int act;
  bouncerS.update();
  bouncerL.update();
  bouncerR.update();
  bouncerH.update();
  bouncerA.update();
  bouncerTL.update();
  bouncerTR.update();
  

// any pan trimming required?
  if (bouncerTL.read() == LOW) {
     if (trim != 1) {
	    panadjust++;
	    trim = 1;       
	    Serial.print("Panadjust=");
	    Serial.println(panadjust);
     }
  } else if (bouncerTR.read() == LOW) {
     if (trim != -1) {
	    panadjust--;
	    Serial.print("Panadjust=");
	    Serial.println(panadjust);
	    trim = -1;
     }
  } else {
     trim = 0;
  }
  
  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();
    if (panright)
       pan = panadjust + DIFF;
    else if (panleft)
       pan = panadjust - DIFF;
    else
       pan = panadjust;

  } else {
    if (!oldauto) {
    // AUTOKAP just switched on
      Serial.println("AutoKAP on");
   
      readDIPs(); // sets curscript etc
      oldauto = true;
      scriptx = 0;
      stepdelay = getDelay(curscript, scriptx);
      lasta = millis() - stepdelay; // start straight away
		
//      Serial.print("Starting sequence ");
//      Serial.println(curscript);
    }
    // compute new autokap values
    atime = millis();
    if ((ptime > 0) && ((atime - ptime) > panvalue*P_SCALE)) { // 200, 400, 600 or 800 ms pulse
      ptime = -2;	// turn  panning off for this step
      pan = panadjust;
    }
  if ((atime - lasta) > stepdelay) {
    act = scripts[curscript][scriptx].action;
    if (act == AK_NULL) {
      // ignore - it's the end of a script
    } 
    if (act == AK_TILT || act == AK_PANTILT) {
      tilt = (scripts[curscript][scriptx].value * DEG3) / 3;
    } 
    if (act == AK_PAN || act == AK_PANTILT) {
      if (ptime == -1) {
        ptime = millis(); // start panning  and pan timer
        pan = panadjust + DIFF * pandir;
         
      } else {
         ptime = -1;  // allow panning again
      }
    }
    if (act == AK_SHOOT) {
      shutter = true;
    } 
    if (act == AK_HO) {
      hover = false;
    } 
    if (act == AK_VER) {
      hover = true;
    } 
    if (act == AK_SHOOTOFF) {
      shutter = false;
    }

    lasta = millis();
    scriptx++;
    if (scripts[curscript][scriptx].action == AK_NULL) {
      scriptx = 0; // end of script - starting again
    }
    stepdelay = getDelay(curscript, scriptx);
  }
}
  
// routine to get delay for line ix of script sx
unsigned long getDelay(int sx, int ix) {
  unsigned long d;
  int 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 = basedelay-PRESHOOT-SHOOTDELAY;
  else
    d = 0;
  return d;
}


// routine to scale and adjust tilt
int adjustTilt() {
  int t;
  t = ((t  * T_SCALE) / 90) * 9;
  if (t < 0)
    t = 0;
  
  // ensure tilt servo does not move too far beyond 90 degrees   
  if (t > T_MAX)
    t = T_MAX; 
  return t;
}

	    
// routine to use input values to update model state
void updateModel() { 
  model.shutter = shutter;
  model.pan = pan;
  if (newtilt != tilt)
     tilt = slew(tilt, newtilt);
  model.tilt = T_MAX - tilt; 	// reverse tilt servo
  model.hover = hover; 
}

	    
// routine to slew tilt movements - v is the current tilt and target the desired tilt
// for a typical tilt change, this routine takes 30 to 50 calls to get to the target.
int slew(int v, int target) {
   int newv;
   if (v == target)
      return target;
   if (v > target && (target + 16) > v) 
      return target;       
   if (v < target && (target < v + 16)) 
      return target;        
   newv = ((v*15) + target)/16; // compute next tilt value
   return newv;      
}


// routine to convert model state into a transmitter frame and send it
void transmitFrame() {
  int s, s2, h;
  if (model.shutter) {
    s = SHUTTER_DOWN;
    s2 = SHUTTER2_DOWN;
  } else {
    s = SHUTTER_UP;
    s2 = SHUTTER2_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);
  tx.set_channel(S2_CHANNEL, s2);

  // we just set the other two to Shutter value
  tx.set_channel(5, 0, s);
  tx.send_frame(modelid);
}

// routine to read DIP switches and set AutoKAP values
void readDIPs() {
  int m = 1;
  dipbits = 0;
  for (byte i = 0; i < 8; i++) {
     dipbits += (digitalRead(DIPswitches[i]) == 0)*m;
     m = m*2;
  }  
  dipdelay = (dipbits & 0x03);
  dippan =   (dipbits & 0x0c) / 4;
  dipsequence = (dipbits & 0x70) / 16;
  dippandir = (dipbits & 0x80)/64; // 2 or 0
  pandir = dippandir - 1; // +1 or -1
 
  curscript = dipsequence;
  basedelay = delayvals[dipdelay];
  panvalue = panvals[dippan];
  Serial.print("DIP=");
  Serial.println(dipbits);
}


// 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;
  pandir = 1;

  ptime = -1;
  tilt = T_MIDDLE;
  model.tilt = tilt;
  model.pan = pan;
  model.shutter = shutter;
  model.hover = hover;
  modelid = 0;
  panadjust = PAN_NEUTRAL; // neutral point for 360 servo
  	
  tx.begin();
  

  /* at start try binding if shutter button pressed (i.e. OFF) */
  bouncerS.update();
  shutter  = (bouncerS.read() == LOW);
  Serial.println("shutter=");
  Serial.println(shutter);
  if (!shutter) {
    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();
}

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

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