OverviewFilesCommitsIssuesDocsReleasesPeers

cthuloid / Arduino / LightArm / LightArm.ino

1148 lines · ino ref:
// Arduino sketch for controlling multiple Dynamixel Servos over ethernet or serial.
// Apologies this file is so large; the Arduino IDE doesn't handle includes and macros
// very well, and I want to keep it easy for others to compile. 
// 
// Accept text commands via either ethernet or serial (see flag COMM_ETHERNET).
// Commands are listed in /LightArm/readme; see the definition of CommandsList in this file.
// Set amount of response text with command 'plevel' (see PrintLevel.h)
// See SerialCommand::Entry CommandsList for list of commands.  Each command will reply 
// with argument format when used incorrectly, unless plevel is set to "silent".
//
// Big TODO:
// - move network methods (including updating network settings) and LED methods into a separate module
//////////////////////////////////////////////////////////////////////////////////////

#include <ax12.h>
#include <BioloidController2.h>    // use the bug-fixed version 
#include <SC.h>
#include <PWM.h>

#include "eeprom.h"

// Comment this out to read and write from Serial instead of Ethernet.
// Arduino IDE is wigging out when selecting which ethernet library to use; see line 59 (#ifdef COMM_ETHERNET).
#define COMM_ETHERNET

// Comment this in to allow greater range of movement for the first servo.
//#define GIMBAL_MODE

/*#define COMM_ETHERNET_ENC28J60
//#define COMM_ETHERNET_SHIELD

/*#if defined COMM_ETHERNET_SHIELD || defined COMM_ETHERNET_ENC28J60
  #define COMM_ETHERNET
#endif*/


//////////////////////////////////////////////////////////////////////////////////////
// PWM globals
/////////////////////////////////////////////////////////////////////////////////////

// this flag will invert PWM output (255-output), for active-low devices
#define INVERT_HIGH_AND_LOW

#define MAX_PWM 65535
#define PWM_FREQ 1000

const int PWMPins[] = {11, 12, 13, 14, 15};
const int NumPWMPins = sizeof(PWMPins)/sizeof(*PWMPins);


// Ethernet via ENC28J60 
// Library: https://github.com/ntruchsess/arduino_uip
//////////////////////////////////////////////////////////////////////////////////////////////////////////
//
// Pinout      - Arduino Mega - Arduino Uno
// VCC (Green) -         3.3V -      
// GND (Gr Wh) -          GND -      
// SCK (Bl Wh) -       Pin 52 -      Pin 13
// SO  (Blue)  -       Pin 50 -      Pin 12
// SI  (Br Wh) -       Pin 51 -      Pin 11
// CS  (Brown) -       Pin 53 -      Pin 8   # Selectable with ether.begin()
#ifdef COMM_ETHERNET
  
  // For ENC28J60 card
  //#include <UIPEthernet.h>
  
  // For Arduino Ethernet Shield
  #include <SPI.h>
  #include <Ethernet.h>

  uint16_t Port             = 1337;
  uint8_t  IP[]             = {10, 0, 2, 254};    // also used in MAC address
  uint8_t  GatewayAddress[] = {10, 0, 0, 1};
  uint8_t  SubnetMask[]     = {255, 255, 0, 0};
  //uint8_t  MacAddress[6]    = {0x00, 0x01, IP[0], IP[1], IP[2], IP[3]};

  EthernetServer TCPserver(Port);

  // print to the current ethernet client if there is one
  EthernetClient Client;
  #define gPrinter Client
  
  void setupEthernet() {
    // mac address is function of our IP address
    uint8_t mac[6] = {0x00, 0x01, IP[0], IP[1], IP[2], IP[3]};
    
    Serial.print("Starting ethernet server on address: ");
    
    IPAddress ip = IPAddress(IP);
    IPAddress gateway = IPAddress(GatewayAddress);
    IPAddress subnet = IPAddress(SubnetMask);

    // (re-)instantiate the ethernet and tcp objects
    Ethernet.begin(mac, ip, gateway, gateway, subnet);
    TCPserver = EthernetServer(Port);
    TCPserver.begin();
    
    Serial.println(Ethernet.localIP());
  }
  
  
  // 6-byte string abbreviation for for Circus Automatic Light
  const char EEPROM_HEADER_STRING[] = {'C', 'i', 'r', 'c', 'A', 'L'};
  const uint16_t EEPROM_SETTINGS_VERSION = 0;
  
  struct EEPROMSettings {
    struct Header {
      char string[sizeof(EEPROM_HEADER_STRING)];
      uint16_t version;
    } header;
    
    // data
    uint32_t ipAddress;
    uint32_t subnetMask;
    uint32_t gatewayAddress;
    uint16_t port;
    //uint8_t macAddress[6];
        
    EEPROMSettings() {
      memcpy(header.string, EEPROM_HEADER_STRING, sizeof(EEPROM_HEADER_STRING));
      header.version = EEPROM_SETTINGS_VERSION;
    }
  };

  void invalidateSettings() {
    int32_t zero = 0;
    memcpyToEEPROM(0, &zero, sizeof(zero));
  }
  
  boolean loadSettings() {
    EEPROMSettings current;
    memcpyFromEEPROM(0, &current, sizeof(current));
  
    // check to see if valid settings are saved in EEPROM
    if (memcmp(current.header.string, EEPROM_HEADER_STRING, sizeof(EEPROM_HEADER_STRING))) {
      Serial.println("EEPROM does not contain valid settings.");
      return false;
    }
    // additionally, check for the current version
    if (current.header.version != EEPROM_SETTINGS_VERSION) {
      Serial.println("Settings stored in EEPROM have wrong version.");
      return false;
    }
    
    memcpy(IP, &current.ipAddress, sizeof(IP));
    memcpy(GatewayAddress, &current.gatewayAddress, sizeof(GatewayAddress));
    memcpy(SubnetMask, &current.subnetMask, sizeof(SubnetMask));
    //memcpy(MacAddress, &current.macAddress, sizeof(MacAddress));
    Port = current.port;
    
    return true;
  }
  
  void saveSettings() {
    EEPROMSettings h;
    
    memcpy(&h.ipAddress, IP, sizeof(IP));
    memcpy(&h.gatewayAddress, GatewayAddress, sizeof(GatewayAddress));
    memcpy(&h.subnetMask, SubnetMask, sizeof(SubnetMask));
    //memcpy(&h.macAddress, MacAddress, sizeof(MacAddress));
    h.port = Port;
    
    //printlnDebug("Writing settings to EEPROM...");
    memcpyToEEPROM(0, &h, sizeof(h));
  }
  
#else
  void invalidateSettings() {}
  boolean loadSettings() { return true; }
  void saveSettings() {}
#endif

#include <PrintLevel.h>

/////////////////////////////////////////////////////////////////////////////////////
// Servo globals
/////////////////////////////////////////////////////////////////////////////////////

const int MaxServos = 32;
BioloidController Servos(1000000);

const int BroadcastID = 254;

// When moving servos without interpolating, set this speed on the servos.
// The units are servo specific, range [1-1023].
int gServoSpeed = 40;

// When an interpolating move is requested, the servos will move at different speeds in order to
// arrive at the target position simultaneously. But movement time will be constrained so that
// servos will not violaten this maximum speed, constrained to [1-1000] angle-units per second
int gMaxInterpolationSpeed = 100;
// Speed is inverted to simplify calculation.
//int gMaxInterpolationSpeedInv = 10;  // max servo movement speed inverted, in ms per angle-unit

//bool relaxed = false;

const char *MsgPositionTupleFormatError = "Error: takes pairs in the form <ID>:<angle>";
const char *MsgPWMTupleFormatError = "Error: takes pairs in the form <index>:<PWM>";

/////////////////////////////////////////////////////////////////////////////////////////
// Command Manager and forward declarations for commands accessible from text interface
/////////////////////////////////////////////////////////////////////////////////////////

struct IDTuple { int id; long value; };

int parseID(const char *arg);
int parseAngle(const char *arg, int minAngle = 200, int maxAngle = 824);
boolean parsePositionTuple(char *s, IDTuple *out);
boolean parsePWMTuple(char *s, IDTuple *out);
int parseListOfIDs(int *outIDs, int maxIDs);

void cmdUnrecognized(const char *cmd);
void cmdPWMPins();
void readVoltage();
void readServoPositions();
void cmdSetPrintLevel();
void cmdSetMaxSpeed();
void cmdLoadPose();
void cmdMoveServos();
void cmdInterpolateServos();
void cmdInterpolateServosBinary();
void cmdRelax();
void cmdTorque();
void cmdCircle();
void cmdSetID();
void cmdSetIPAddress();
void cmdSetGatewayAddress();
void cmdSetPort();
void restartNetwork();
void cmdClearSettings();

static const SerialCommand::Entry CommandsList[] = {
  {"plevel",  cmdSetPrintLevel},
  {"pwm",     cmdPWMPins},
  {"v",       readVoltage},
  {"r",       readServoPositions},
  {"s",       cmdMoveServos},
  {"p",       cmdLoadPose},
//  {"i",       cmdInterpolateServos},
//  {"B",       cmdInterpolateServosBinary},
  {"speed",   cmdSetMaxSpeed},
  {"relax",   cmdRelax},
  {"torq",    cmdTorque},
  {"circle",  cmdCircle},
  {"id",      cmdSetID},
  {"ip",      cmdSetIPAddress},
  {"gateway", cmdSetGatewayAddress},
  {"port",    cmdSetPort},
  {"restart", restartNetwork},
  {"compliance", cmdSetCompliance},
  {"clear",   cmdClearSettings},
  {NULL,     NULL}
};

SerialCommand CmdMgr(CommandsList, cmdUnrecognized);


/////////////////////////////////////////////////////////////////////////////////////////////
// helpers
/////////////////////////////////////////////////////////////////////////////////////////////
/*
// Converts string to double; returns true if successful.
// Returns false if str contains any inappropriate characters, including whitespace.
// See strtod for accepted number formats.
boolean toDouble(const char *str, double *result) {
  char *end;
  if(*str == ' ') return false;
  *result = strtod(str, &end);

  // if end == str, str is empty or begins with an erroneous character
  // if end does not point at a null-terminator, the string contained errorneous characters
  return end != str && *end == '\0';
}

// Converts string to long; returns true if successful.
// Returns false if str contains any characters other than numbers or a negative sign.
boolean toLong(const char *str, long *result) {
  char *end;
  //if(*str == ' ') return false;
  *result = strtol(str, &end, 10);
  
  // if end == str, str is empty or begins with an erroneous character
  // if end does not point at a null-terminator, the string contained errorneous characters
  return end != str && *end == '\0';
}

// converts string to int; returns true if successful
// Returns false if str contains any characters other than numbers or a negative sign.
boolean toInt(const char *str, int *result) {
  char *end;
  //if(*str == ' ') return false;
  long r = strtol(str, &end, 10);
  
  // TODO check if out of int range
  //if((unsigned long)r >> (8*sizeof(*result))) return false;
  *result = r;
  
  // if end == str, str is empty or begins with an erroneous character
  // if end does not point at a null-terminator, the string contained errorneous characters
  return end != str && *end == '\0';
}
*/

// Takes a string of an integer (numeric chars only). Returns the integer on success.
// Prints error and returns 0 if there is a parse error or the ID is out of range.
int parseID(const char *arg) {
  char *end;
  int id = strtol(arg, &end, 10);

  if(*end != '\0' || id < 1 || id > 253) {
      printlnError("Error: ID must be between 1 and 253");
      return 0;
  }
  
  return id;
}

// Takes a string of an integer (numeric chars only). Returns the integer on success.
// Prints error and returns -1 if there is a parse error or the angle is out of range.
int parseAngle(const char *arg, int minAngle, int maxAngle) {
  char *end;
  int angle = strtol(arg, &end, 10);
  
  if(*end != '\0' || angle < minAngle || angle > maxAngle) {
    printError("Error: angle must be between ");
    printError(minAngle);
    printError(" and ");
    printlnError(maxAngle);
    return -1;
  }
  
  return angle;
}

// Takes a string of an integer (numeric chars only). Returns the integer on success.
// Prints error and returns -1 if there is a parse error or the PWM value is out of range.
long parsePWM(const char *arg) {
  char *end;
  long v = strtol(arg, &end, 10);

  if(*end != '\0' || v < 0 || v > MAX_PWM) {
      printlnError("Error: PWM value must be between 0 and ");
      printlnError(MAX_PWM);
      return -1;
  }
  
  return v;
}

// parse a list of IDs from CmdMgr into an array
// returns -1 on error
int parseListOfIDs(int *outIDs, int maxIDs) {
  int count = 0;
  
  while(char *arg = CmdMgr.next()) {
    if(count >= maxIDs) {
      printlnError("Error: too many IDs");
      return -1;
    }
    
    int id = parseID(arg);
    if(id == 0) return -1;
    outIDs[count++] = id;
  }
  
  return count;
}

// convert "<id>:<angle>" into 2 integers in a struct
// returns false on error
boolean parsePositionTuple(char *s, IDTuple *out) {
  char *sID = strtok(s, ":");
  if(sID == NULL) {
    printlnError(MsgPositionTupleFormatError);
    return false;
  }
  
  int id = parseID(sID);
  if(id == 0) return false;

  char *sAngle = strtok(NULL, ":");
  if(sAngle == NULL) {
    printlnError(MsgPositionTupleFormatError);
    return false;
  }
  
#ifdef GIMBAL_MODE
  // the first servo should allowed full movement
  int angle;
  if(id == 1) angle = parseAngle(sAngle, 0, 1023);
  else angle = parseAngle(sAngle);
#else
  int angle = parseAngle(sAngle);
#endif
  if(angle == -1) return false;
  
  out->id = id;
  out->value = angle;
  return true;
}

// convert "<index>:<value>" into 2 integers in a struct
// returns false on error
boolean parsePWMTuple(char *s, IDTuple *out) {
  char *sIndex = strtok(s, ":");
  if(sIndex == NULL) {
    printlnError(MsgPWMTupleFormatError);
    return false;
  }
  
  int index = parseID(sIndex);
  if(index == 0) return false;

  char *sPWM = strtok(NULL, ":");
  if(sPWM == NULL) {
    printlnError(MsgPWMTupleFormatError);
    return false;
  }
  
  long pwm = parsePWM(sPWM);
  if(pwm == -1) return false;
  
  out->id = index;
  out->value = (int)pwm;
  return true;
}

void broadcastSpeed() {
  ax12SetRegister2(BroadcastID, AX_GOAL_SPEED_L, gServoSpeed);
}

// a value of 128 seems to produce the smoothest servo motion
// see AX-12 Dynamixel manual
void setCompliance(int comp) {
  int ms = 20;
  
  //Serial.println("setting copmpliance...");
  ax12SetRegister(BroadcastID, AX_CW_COMPLIANCE_SLOPE, comp);   delay(ms); 
  ax12SetRegister(BroadcastID, AX_CCW_COMPLIANCE_SLOPE, comp);  delay(ms); 

  //Serial.print("AX_CW_COMPLIANCE_SLOPE: ");
  //Serial.print(ax12GetRegister(1, AX_CW_COMPLIANCE_SLOPE, 1));    delay(ms);
  //Serial.print(' ');
  //Serial.println(ax12GetRegister(2, AX_CW_COMPLIANCE_SLOPE, 1));  delay(ms);
  
  //Serial.print("AX_CCW_COMPLIANCE_SLOPE: ");
  //Serial.print(ax12GetRegister(1, AX_CCW_COMPLIANCE_SLOPE, 1));   delay(ms);
  //Serial.print(' ');
  //Serial.println(ax12GetRegister(2, AX_CCW_COMPLIANCE_SLOPE, 1)); delay(ms);
}

//////////////////////////////////////////////////////////////////////////////////
// commands accessible from text interface

void cmdUnrecognized(const char *cmd) {
  printlnError("unrecognized command");
}

void cmdSetCompliance() {
  if(char *arg = CmdMgr.next()) {
    if(CmdMgr.next()) {
      printlnError("Error: takes 0 or 1 arguments");
      return;
    }

    char *end;
    double comp = strtod(arg, &end);

    if(*end != '\0' || comp < 1 || comp > 128) {
      printlnError("Error: compliance must be between 1 and 128");
      return;
    }
    
    setCompliance((int)comp);
    printlnInfo("Compliance set");
  }

  else printlnError("No arguments");
}

void readVoltage() {
  float voltage = (ax12GetRegister (1, AX_PRESENT_VOLTAGE, 1)) / 10.0; 
  if(voltage < 0) {
    printlnError("Dynamixel error: system reported voltage error. May be servos with duplicate IDs.");
  }
  else {
    printError("Dynamixel ready. System voltage: ");
    printError(voltage);
    printlnError("V");
  }
}

// Relax a list of servos, or all if no arguments
void cmdRelax() {
  const int maxAngles = MaxServos;
  int ids[maxAngles];
  
  int count = parseListOfIDs(ids, maxAngles);
  if(count == -1) return;
  
  if(count == 0) {
    count = MaxServos;
    for(int i = 0; i < MaxServos; i++) {
      Relax(Servos.getId(i));
    }
  }
  else {
    for(int i = 0; i < count; i++) {
      Relax(ids[i]);
    }
  }
  
  // TODO not sure how useful a single flag is
  //relaxed = true;
  
  printAck("Relaxed ");
  printAck(count);
  printlnAck(" Servos");
}

void cmdTorque() {
  const int maxAngles = MaxServos;
  int ids[maxAngles];
  
  int count = parseListOfIDs(ids, maxAngles);
  if(count == -1) return;
  
  if(count == 0) {
    count = MaxServos;
    for(int i = 0; i < MaxServos; i++) {
      TorqueOn(Servos.getId(i));
    }
  }
  else {
    for(int i = 0; i < count; i++) {
      TorqueOn(ids[i]);
    }
  }
  
  // TODO not sure how useful a single flag is
  //relaxed = true;
  
  printAck("Enabled torque on ");
  printAck(count);
  printlnAck(" Servos");
}


// if no argument, prints the maximum speed
// if one argument, sets the maximum speed and then prints it
// Units to the user are angle-units per second, valid range [1-1000]
// Internally the speed is kept in ms per angle-unit, valid range [1000-1]
void cmdSetMaxSpeed() {
  if(char *arg = CmdMgr.next()) {
    if(CmdMgr.next()) {
      printlnError("Error: takes 0 or 1 arguments");
      return;
    }

    char *end;
    double speed = strtod(arg, &end);

    if(*end != '\0' || speed < 1 || speed > 1000) {
      printlnError("Error: speed is in angle-units per second; between 1 and 1000");
      return;
    }
    
    //gMaxInterpolationSpeed = (int)(1000.0 / speed);  // convert to ms per angle-unit
    gServoSpeed = round(speed);
  }
  
  broadcastSpeed();
  printAck("servo speed: ");
  printlnAck(gServoSpeed);
  
  //printAck("max speed in angle-units per second: ");
  //printlnAck(1000.0 / gMaxInterpolationSpeed);  // convert to angle-units per second
}

void readServoPositions() {
  printAck("Servo Readings:");
  printlnAck(MaxServos);
  
  //Servos.readPose();

  for(int i = 0; i < MaxServos; i++) {
    int id = Servos.getId(i);
    //int pos = Servos.getCurPose(id);
    int pos = Servos.readPose(i); //in case we've relaxed and some one manually moved the servos
    //Serial.println(pos);
    
    if(pos < 0 || pos > 1024) pos = 0;   // 8191-2 is invalid; replace with 0 because it's also invalid and shorter
    
    // print key:value pairs space delimited, but column-aligned
    char buf[16];
    int ixEnd = sprintf(buf, "ID:%d", id);
    while(ixEnd < 5) buf[ixEnd++] = ' ';
    buf[ixEnd] = '\0';

    printInfo(buf);
    printInfo(" pos:");
    printlnInfo(pos);
  }

  // python dictionary notation;
  // allocate 3 for {}\0, and 8 for each pos, though (4 + comma) should be max digits
  char dictBuf[3 + MaxServos * (3+1+4+1)] = "{}";
  int ix = 1;

  for(int i = 0; i < MaxServos; i++) {
    int id = Servos.getId(i);
    int pos = Servos.getCurPose(id);
    if(pos < 0 || pos > 1024) continue;

    ix += sprintf(dictBuf+ix, "%d:%d,", id, pos);
  }
  
  if(ix > 1) dictBuf[ix - 1] = '}';    //overwrite the final comma
  printlnAlways(dictBuf);
}


// arguments are index:angle pairs
void cmdMoveServos() {
  //broadcastSpeed();    // servos can forget their speed, or may have reset since we booted
  
  IDTuple tuple;
  int count = 0;
  
  char *arg = CmdMgr.next();
   if(arg == NULL) {
     printlnError("Error: no arguments");
     return;
   }

   // see what kind of arguments we have
   do {
    if(count >= MaxServos) {
      printlnError("Too many arguments");
      return;
    }

    if(parsePositionTuple(arg, &tuple) == false) return;
    count++;
    
    Servos.setCurPose(tuple.id, tuple.value);
  } while(arg = CmdMgr.next());
  
  Servos.writePose();
  Serial.print("Moving ");
  Serial.print(count);
  Serial.println(" servos");
}

//servo serial command format:
//"s <id>:<angle> <id>:<angle> ... \n"
//angle is 0 to 1024? or 1 to 1023?
// We shall constrain the angle to 200 to 824, and assume and consider 0 a no-op.
void cmdInterpolateServos() {
#if 0
  const int maxAngles = MaxServos;
  IDTuple angles[maxAngles];
  int count = 0;
  
  // parse any number of "id:angle" pairs
  while(char *arg = CmdMgr.next()) {
    if(count >= maxAngles) {
      printlnError("Too many arguments");
      return;
    }

    if(parsePositionTuple(arg, angles + count) == false) return;
    count++;
  }
  
  if(count == 0) {
    printlnError("Error: no arguments");
    return;
  }
  
  /*if(relaxed) {
    relaxed = false;
    Servos.readPose();
  }*/

  long msLongest = 0;
  for(int i = 0; i < count; i++) {
    int id = angles[i].id;
    int newAngle = angles[i].angle;
    int curAngle = Servos.getCurPose(id);
    
    if(curAngle < 1 || curAngle > 2000) continue;
    
    long time = abs(curAngle - newAngle) * gMaxSpeedInv;
    msLargestTimeToMove = max(msLargestTimeToMove, time);
    
    printInfo("moving id ");
    printInfo(id);
    printInfo(" from ");
    printInfo(curAngle);
    printInfo(" to ");
    printlnInfo(newAngle);

    Servos.setNextPose(id, newAngle);
  }
  
  printAck("Longest movement will take ");
  printAck(msLargestTimeToMove);
  printlnAck(" ms");
  
  Servos.interpolateSetup(msLongest);
#endif
}

// Takes an array of packed servo positions. Position == 0 is no-op.
void interpolateServosBinary(char *buffer, int len) {
#if 0
  int numAngles = min(len/2, MaxServos);
  long msLongest = 0;  // move time required by any move
  int maxSpeedInv = invertMaxSpeed();  // convert to ms per angle-unit to simplify calculation
  
  if(relaxed) {
    relaxed = false;
    Servos.readPose();
  }
  
  for(int i = 0; i < numAngles; i++) {
    // little endian
    int newAngle = (unsigned char)buffer[2*i];
    newAngle    += (int)((unsigned)buffer[2*i+1]) << 8;
    
    // 0 is no-op; acceptable range is [200, 824]
    if(newAngle == 0) continue;
    if(newAngle < 200 || newAngle > 824) {
      printError("Angle out of range: ");
      printlnError(newAngle);
      continue;
    }
    
    int id = Servos.getId(i);
    int curAngle = Servos.getCurPose(id);

    // do not calculate movement time for or move a servo if it is incommunicado 
    // or we don't know where it currently is
    if(curAngle < 1 || curAngle > 1024) continue;
    
    long time = abs(curAngle - newAngle) * maxSpeedInv;
    msLongest = max(msLongest, time);
    
    printInfo("moving id ");
    printInfo(id);
    printInfo(" from ");
    printInfo(curAngle);
    printInfo(" to ");
    printlnInfo(newAngle);

    Servos.setNextPose(id, newAngle);
  }
  
  printAck("Longest movement will take ");
  printAck(msLargestTimeToMove);
  printlnAck(" ms");
  
  Servos.interpolateSetup(msLongest);
#endif
}

// argument is number of angles to be transmitted
void cmdInterpolateServosBinary() {
  int numAngles = MaxServos;

  if(char *arg = CmdMgr.next()) {
    char *end;
    int n = strtol(arg, &end, 10);
    
    // keep going even if there's an error, because the host will probably send the data anyway?
    if(end != '\0' || n < 1 || n > 253) {
      printAlways("Error: argument is number of servo angles to be sent, should be in [1, 253]. Assuming ");
      printlnAlways(MaxServos);
      //return;
    }
    else {
      numAngles = n;
    }
  }
  
  printAck("Expecting ");
  printAck(numAngles*2);
  printlnAck(" bytes");
  CmdMgr.enterBinaryMode(numAngles*2, interpolateServosBinary);
}

// moves all servos to the specified position
void loadPose(int pos = 512) {

  for(int i = 0; i < MaxServos; i++) {
    int id = Servos.getId(i);
    Servos.setCurPose(id, pos);
  }

  broadcastSpeed();
  Servos.writePose();
  printAck("Moving ");
  printAck(MaxServos);
  printAck(" servos to position index: ");
  printlnAck(pos);
}

// debug tool: moves all servos to a preprogrammed position, selected by index 1-5
void cmdLoadPose() {
  int Positions[] = {490, 500, 512, 612, 712};
  int NumPositions = sizeof(Positions) / sizeof(*Positions);
  
  if(char *arg = CmdMgr.next()) {
    int index = *arg - '1';
    if(index < 0 || index >= NumPositions) return;
    loadPose(Positions[index]);
  }
}

int sinToServoPos(float sinx) {
  return round(512 + (sinx * 300));
}

// moves 2 servos in a circle
// - first command line arg is the ID of the lower servo
// - second command line arg is the number of seconds to complete the circle
void cmdCircle() {
  int id1 = parseID(CmdMgr.next());
  int id2 = id1 + 1;
  
  long seconds = parseID(CmdMgr.next());
  float msDuration = 1000 * seconds;
  
  long start = millis();
  long end = start + msDuration;
  long now;
  
  do {
    now = millis();
    float angle = (now - start) / msDuration * 2*3.14159;
    float sinx = sin(angle);
    float cosx = cos(angle);

    Servos.setCurPose(id1, sinToServoPos(sin(angle)));
    Servos.setCurPose(id2, sinToServoPos(cos(angle)));
    Servos.writePose();
    
    while(millis() < 30 + now);
    //printAlways(sinx); printAlways(", "); printlnAlways(cosx);

  } while(now < end);
}

// changes the ID of a servo from the first command line arg to the second
void cmdSetID() {

  const char *arg1 = CmdMgr.next();
  const char *arg2 = CmdMgr.next();
  
  if(arg1 == NULL || arg2 == NULL) {
    printlnError("Error: usage: <old ID> <new ID>");
    return;
  }

  int id = parseID(arg1);
  int newID = parseID(arg2);
  
  if(id == 0 || newID == 0) return;
  
  const int AX_START = 255;
  const int AX_ID_LENGTH = 4;
  
  int checksum = (~(id + AX_ID_LENGTH + AX_WRITE_DATA + AX_ID + newID))&0xFF;
  setTXall();
  ax12write(AX_START);                // Send Instructions over Serial
  ax12write(AX_START);
  ax12write(id);
  ax12write(AX_ID_LENGTH);
  ax12write(AX_WRITE_DATA);
  ax12write(AX_ID);
  ax12write(newID);
  ax12write(checksum);
  setRX(0);

  printlnInfo("Servo ID change message sent. Use r command to check.");
}

void restartNetwork() {
#ifdef COMM_ETHERNET
  Client.flush();
  Client.stop();  // hopefully this closes the connection gracefully
  setupEthernet();
#endif
}

void cmdClearSettings() {
#ifdef COMM_ETHERNET
  if(char *arg = CmdMgr.next()) {
    if(!strcmp(arg, "settings")) {
      invalidateSettings();
      
      // TODO provide a way to soft reset to restart with factory settings
      printlnAlways("Settings cleared. Hard reboot to start with factory settings.");
      return;
    }
  }

  printlnError("Error: argument must be \'settings\'.");
#endif
}

const char *RestartNetworkMessage = "Success. Type \"restart\" to restart network interface";

void cmdSetIPAddress() {
#ifdef COMM_ETHERNET
  char *arg = CmdMgr.next();
  if(arg == NULL) {
    printlnError("Error: expected IP address");
    return;
  }
  
  IPAddress address;
  if(address.fromString(arg) == false) {
    printlnError("Error: expected IP address in form XXX.XXX.XXX.XXX");
    return;
  }
  
  uint32_t u = address;
  memcpy(IP, &u, sizeof(IP));
  saveSettings();
  printlnAlways(RestartNetworkMessage);
#endif
}

void cmdSetGatewayAddress() {
#ifdef COMM_ETHERNET
  char *arg = CmdMgr.next();
  if(arg == NULL) {
    printlnError("Error: expected IP address");
    return;
  }
  
  IPAddress address;
  if(address.fromString(arg) == false) {
    printlnError("Error: expected IP address in form XXX.XXX.XXX.XXX");
    return;
  }
  
  uint32_t u = address;
  memcpy(GatewayAddress, &u, sizeof(GatewayAddress));
  saveSettings();
  printlnAlways(RestartNetworkMessage);
#endif
}

void cmdSetPort() {
#ifdef COMM_ETHERNET
  char *arg = CmdMgr.next();
  
  if (arg != NULL) {
    char *end;
    long port = strtol(arg, &end, 10);
    
    if(*end == '\0' && port > 0 && port <= 65535) {
      Port = port;
      saveSettings();
      printlnAlways(RestartNetworkMessage);
      return;
    }
  }

  printlnError("Error: port number expected between 1 and 65535"); 
#endif
}

// expects space-delimited ints 0-65535, or space delimited <index>:<pwm> pairs, where <index> is 1-based
void cmdPWMPins() {
  const char *SetPWMUsageMsg = "Error: takes up to 6 arguments between 0 and 65535, or <index>:<value> pairs where <index> starts at 1.";

  long channelValues[NumPWMPins];
  int count = 0;

  char *arg = CmdMgr.next();
  if(arg == NULL) {
    printlnError("Error: no arguments");
    return;
  }
  
  // see if we have a <index>:<value> tuple, or just an integer
  boolean parsingTuples = strstr(arg, ":") != NULL;
  
  do {
    int index;
    long value;
    
    if(count >= NumPWMPins) {
      printlnError(SetPWMUsageMsg);
      return;
    }

    // <index>:<PWM value>
    if(parsingTuples) {
      IDTuple tuple;
      if(parsePWMTuple(arg, &tuple) == false) return;
      index = tuple.id - 1;   // convert to zero-based index
      value = tuple.value;
      
      if(index < 0 || index >= NumPWMPins) {
        printError("Index must be between 1 and ");
        printlnError(NumPWMPins);
        return;
      }
    }
    
    // just a PWM value
    else {
      char *end;
      index = count;
      value = parsePWM(arg);
      if (value < 0) {
        printlnError(SetPWMUsageMsg);
        return;
      }
    }
    
    printInfo("Set pin ");
    printInfo(PWMPins[index]);
    printInfo(" to ");
    printlnInfo(value);
    channelValues[index] = value;
    count++;
  } while(arg = CmdMgr.next());
  
  for(int i = 0; i < count; i++) {
    long c = channelValues[i];
#ifdef INVERT_HIGH_AND_LOW
    c = MAX_PWM - c;
#endif
    //analogWrite(PWMPins[i], c);
    pwmWriteHR(PWMPins[i], c);
  }
  
  printAck("OK set ");
  printAck(count);
  printlnAck(" pins");
}

// changes the amount of response text; see PrintLevel.h
void cmdSetPrintLevel() {  
  if(char *arg = CmdMgr.next()) {
    if(CmdMgr.next()) {
      printlnAlways("Error: takes 0 or 1 arguments");
      return;
    }
    
    if(!PrintLevel::set(arg)) {
      PrintLevel::printErrorString();
      return;
    }
  }
  
  printAlways("print level ");
  printlnAlways(PrintLevel::toString());
}

///////////////////////////////////////////////////////////////////////////////////////////////////////////
// setup & loop
///////////////////////////////////////////////////////////////////////////////////////////////////////////

void setup() {
  Serial.begin(38400);
  loadSettings();

 // Initialize the high resolution PWM library.
  // Frequency: 10000 Hz
  // Run PWM.h example sketch to see all possible PWM settings for a given pin
  InitTimersSafe(); //initialize all timers except for 0, to save time keeping functions
  
  for(int i = 0; i < NumPWMPins; i++) {
    //pinMode(PWMPins[i], OUTPUT);
    //analogWrite(PWMPins[i], 0);
    Serial.print("setting PWM pin ");
    Serial.print(PWMPins[i]);
    Serial.print(" frequeny: ");
    Serial.println(SetPinFrequency(PWMPins[i], PWM_FREQ));    // TODO for all PWM pins on arduino mega
  }

  // query for and read in the position of each servo
  // TODO: mod Servos class to query for IDs, and store lowest ID that responds
  Servos.setup(MaxServos);//, 1);
  Servos.readPose();

  //setCompliance(128);   //default is 32?
  broadcastSpeed();

  
#ifdef COMM_ETHERNET
  setupEthernet();
#endif
  
  // Safe guard: the move interpolation code moves all servos, so if a servo starts
  // off-center, and we move another servo, the first servo will move to center (the default "next" position).
  // So set the target position of each servo to its current position
  /*for(int i = 0; i < MaxServos; i++) {
    int id = Servos.getId(i);
    Servos.setNextPose(id, Servos.getCurPose(id));
  }*/
  
  readVoltage();
  readServoPositions();
  
  //loadPose();
}

void loop() {
  //if(Servos.interpolating) Servos.interpolateStep();

// read from EITHER serial OR network; otherwise we'll need multiple read buffers...
#ifndef COMM_ETHERNET
  CmdMgr.readSerial();

#else
  // TODO: edit EthernetServer to separate accepting a connection
  // from reading from one, so we can great connectors?
  // Proper support for multiple connections? Yikes. Requires multiple buffers. Probably not.

  // see if a client wants to say something; if so, save them as the client to respond to
  if(EthernetClient client = TCPserver.available()) {
    //client.println("Allo");
    Client = client;
  }

  if(Client)
  {
    //bool gotData = false;
    while(Client.available() > 0)
    {
      char c = Client.read();
      CmdMgr.handleChar(c);
      //gotData = true;
    }
    //if(gotData) Client.println("DATA from Server!");
  }
#endif
}