Sensor display on a TFT Touchscreen

Here’s a demo of data from the SensorPacks being displayed on a TFT Touchscreen/shield I picked up from Adafruit. Writing a simple UI via graphics primitives reminded me of building UI’s back in the day, where all you have are lines, circles, and x/y coordinates to play with. There wasn’t a lot of data space left over to build functionality, but I managed to get a quick-and-dirty analog clock, as well as the ability to store data for 4 individual SensorPacks. The XBee listens for data, and when it recognizes a packet from a SensorPack, it either updates the entry, or adds it to the list.

I ended up with about 200 bytes of storage left, after porting and streamlining my sensor reading code from the other displays I built, so adding more functionality will require a bit of code rework, but I’m pretty happy with this so far. Will likely add gauges, and the ability to show history at some point.


#include <NewSoftSerial.h>
#include "TFTLCD.h"
#include "TouchScreen.h"

boolean firstRun = true;

//#define DEBUG 2
//#define DEBUG 0

#if not defined USE_ADAFRUIT_SHIELD_PINOUT 
 #error "For use with the shield, make sure to #define USE_ADAFRUIT_SHIELD_PINOUT in the TFTLCD.h library file"
#endif

// These are the pins for the shield!
#define YP A1  // must be an analog pin, use "An" notation!
#define XM A2  // must be an analog pin, use "An" notation!
#define YM 7   // can be a digital pin
#define XP 6   // can be a digital pin

#define TS_MINX 150
#define TS_MINY 120
#define TS_MAXX 920
#define TS_MAXY 940

// For better pressure precision, we need to know the resistance
// between X+ and X- Use any multimeter to read it
// For the one we're using, its 300 ohms across the X plate
TouchScreen ts = TouchScreen(XP, YP, XM, YM, 300);

// The control pins can connect to any pins but we'll use the 
// analog lines since that means we can double up the pins
// with the touch screen (see the TFT paint example)
#define LCD_CS A3    // Chip Select goes to Analog 3
#define LCD_CD A2    // Command/Data goes to Analog 2
#define LCD_WR A1    // LCD Write goes to Analog 1
#define LCD_RD A0    // LCD Read goes to Analog 0

// you can also just connect RESET to the arduino RESET pin
#define LCD_RESET A4

/* For the 8 data pins:
Duemilanove/Diecimila/UNO/etc ('168 and '328 chips) microcontoller:
D0 connects to digital 8
D1 connects to digital 9
D2 connects to digital 2
D3 connects to digital 3
D4 connects to digital 4
D5 connects to digital 5
D6 connects to digital 6
D7 connects to digital 7

For Mega's use pins 22 thru 29 (on the double header at the end)
*/

// Color definitions
#define	BLACK           0x0000
#define	BLUE            0x001F
#define	RED             0xF800
#define	GREEN           0x07E0
#define CYAN            0x07FF
#define MAGENTA         0xF81F
#define YELLOW          0xFFE0 
#define WHITE           0xFFFF

TFTLCD tft(LCD_CS, LCD_CD, LCD_WR, LCD_RD, LCD_RESET);

#define MINPRESSURE 10
#define MAXPRESSURE 1000

#define BOXSIZE 40
#define PENRADIUS 4

int tftRotation = 1;

#define MENU 1
#define DETAIL 2
#define CLOCK 3
#define DETAILGRAPH 4

unsigned int displayMode = MENU;
unsigned int detailID = 0;

NewSoftSerial xbSerial(2,3);

// a id t T h p c dd rt reset

#define COMMANDS "aidtTt0t1t2t3hpcddrtreset" // commands we know about, mushed together

#define MAXTOKEN 32     // Maximum number of parsable tokens per packet
#define MAXTOKENLEN 32  // No one token's command or data should exceed this
#define MAXSTRING 80    // SoftSerial buffer is 64 bytes long, so we can't get more than that at once.
                        // This leaves a little buffer in case we get a couple packets in a row.

#define INVALID -99

void sClear(char *theString, int len=MAXSTRING);   // Clears a string, optionally a different length than MAXSTRING                       
//void tftPrint(char *text, uint16_t color=GREEN);

// Define sensor IDs and their relative index positions 
#define SENSORONE 0
#define SENSORTWO 1
#define MAXSENSORS 4
uint8_t nsid = SENSORONE; // Which sensor do we want to display data for?
char *sid = {"VoltBarn Default SensorID"}; // Until we get packets with sensor ID's, use a default

// Globals
uint16_t mpt[MAXSENSORS] = {INVALID,INVALID,INVALID,INVALID}; // Temperature, per sensor
uint16_t mph[MAXSENSORS] = {INVALID,INVALID,INVALID,INVALID}; // Humidity, per sensor
uint16_t mpp[MAXSENSORS] = {INVALID,INVALID,INVALID,INVALID}; // Pressure, per sensor
uint16_t mpu[MAXSENSORS] = {INVALID,INVALID,INVALID,INVALID}; // Time of last update per sensor
uint16_t mpc[MAXSENSORS] = {CYAN, GREEN, YELLOW, MAGENTA};
char *mpi[MAXSENSORS] = {"VoltBarnSensorID1", "VoltBarnSensorID2", "VoltBarnSensorID3", "VoltBarnSensorID4"};
float mpat[MAXSENSORS][4] = { {-99, -99, -99, -99},
                            {-99, -99, -99, -99},
                            {-99, -99, -99, -99},
                          //  {-99, -99, -99, -99},
                            {-99, -99, -99, -99} }; // Auxillary temp sensors per SensorPack

uint16_t rx1[MAXSENSORS], rx2[MAXSENSORS], ry1[MAXSENSORS], ry2[MAXSENSORS];
  
uint8_t maxID = 0;

short int tempSid = 0;
uint16_t ctime = 1200; // current time
uint16_t   pt = 500, ph = 500, pp = 2950; // last temp, humidity, pressure received
float ft = INVALID, fh = INVALID, fp = INVALID; // Set to "no data yet" values (> 9999) for temp, humidty, pressure

char thePacket[MAXSTRING]; // Primary info, the packet received
char tempString[MAXSTRING]; // used in subString
char t[MAXSTRING]; // used in sTrim

uint16_t idx, idx1; // Loop variable for routines that don't call others

long int lastUpdate;

uint8_t * heapptr, * stackptr;
void check_mem() {
  stackptr = (uint8_t *)malloc(4);          // use stackptr temporarily
  heapptr = stackptr;                     // save value of heap pointer
  free(stackptr);      // free up the memory again (sets stackptr to 0)
  stackptr =  (uint8_t *)(SP);           // save value of stack pointer
  if (heapptr > stackptr) {
    Serial.print("ERROR**************************************************heap: ");
    Serial.print((int)heapptr);
    Serial.print(" > stackptr: ");
    Serial.println((int)stackptr);
  }
  #ifdef DEBUG >= 3
  Serial.print("**************************************************heap: ");
  Serial.print((int)heapptr);
  Serial.print(" < stackptr: ");
  Serial.println((int)stackptr);
  #endif
}

void setup(void) {
  char incomingByte;
  
  xbSerial.begin(9600);  // XBee
    
  //Serial.begin(9600);
  Serial.begin(115200);
   
  tft.reset();
  
  uint16_t identifier = tft.readRegister(0x0);
  if (identifier == 0x9325) {
    Serial.println("Found ILI9325");
  } else if (identifier == 0x9328) {
    Serial.println("Found ILI9328");
  } else {
    Serial.print("Unknown driver chip ");
    Serial.println(identifier, HEX);
    while (1);
  }  
 
  tft.initDisplay();
    
  //tft.setRotation(tftRotation); 
  tft.fillScreen(BLACK);
  tft.setCursor(0, 0);
  tft.setRotation(tftRotation);
  tft.setTextColor(RED);
  tft.setTextSize(2);
  tft.println("VoltBarn SensorPack");
  
  delay(1500);
  xbSerial.print("+++");
  delay(1500);
  if (xbSerial.available() == 0)
    xbSerial.print("+++");
  delay(1000);
  while (xbSerial.available()) { // Clear out the "OK"
    incomingByte = xbSerial.read();
  }
  /*
  tft.println("XBee info:");
  tftXbee("ATMY");
  tftXbee("ATID");
  tftXbee("ATDL");
  delay(1000);
  */
  xbSerial.print("ATCN\r");
 
  lastUpdate = millis();
  check_mem();
}

/*
void tftXbee(String command) {
  char incomingByte;
  int b;
  
  xbSerial.print(command);
  xbSerial.print('\r');
  tft.print(command);
  tft.print(":");
  delay(1000);
  while (xbSerial.available() > 0) {
    incomingByte = xbSerial.read();
    tft.print(incomingByte);
  }
  tft.println("");
  delay(1000);
}
*/

// ***********************************
// ************** loop
// ***********************************

void loop() { 

  char incomingByte;
  char cmd[MAXTOKENLEN], data[MAXTOKENLEN];
  byte ntok = 0;
  //char displayItem;
  boolean tokenFound;
  uint16_t i;
  char xtime[6];
  uint16_t tftWidth, tftHeight; 
  boolean touched = false;
  uint8_t bytesA = 0, bytesR = 0;

#ifdef DEBUG >= 2
  // Sample data
  if (firstRun) {
    //sCopy(thePacket,":id:test1-01:t:32.0:p:26.02:h:27.0:");
    //splitTokens();
    sCopy(thePacket,":id:foo1-01:t:32.0:p:26.02:h:27.0:");
    splitTokens();
    sCopy(thePacket,":id:bar1-01:t:32.0:p:26.02:h:27.0:c:0250:");
    splitTokens();
    firstRun = false;
  }
#endif
  
  tftWidth = tft.width();
  tftHeight = tft.height();
  
  // First thing, check for a touch
  
  tft.setRotation(0); // make sure we're at the default rotation to read the input
  digitalWrite(13, HIGH);
  Point p = ts.getPoint();
  digitalWrite(13, LOW);
  
  pinMode(XM, OUTPUT);
  pinMode(YP, OUTPUT);
  
  if (p.z > MINPRESSURE && p.z < MAXPRESSURE) {
    
    // turn from 0->1023 to tft.width
    p.x = map(p.x, TS_MINX, TS_MAXX, tft.width(), 0);
    p.y = map(p.y, TS_MINY, TS_MAXY, tft.height(), 0);
    
    // x, y input are absolute, regardless of rotation
    // x, y output changes, depending on rotation
      
    Serial.print("[");Serial.print(tftWidth);Serial.print(",");Serial.print(tftHeight);Serial.print("] ");
    Serial.print("X = "); Serial.print(p.x); 
    Serial.print("\tY = "); Serial.print(p.y);
    Serial.print("\tPressure = "); Serial.println(p.z);
    
    if (displayMode == CLOCK || displayMode == DETAIL) {
      displayMenu();
      displayMode = MENU;
    } else {
      for (i = 0; i < maxID; i++) {
        if (p.x > rx1[i] && p.x < rx2[i] && p.y > ry1[i] && p.y < ry2[i]) {
          //Serial.println("In bounding box");
          if (displayMode == MENU) {
            tft.fillRect(rx1[i], ry1[i], BOXSIZE, (BOXSIZE*8)-10, mpc[i]);
            detailID = i;
            displayDetail(detailID);
            displayMode = DETAIL;
            touched = true;
          } else {
            displayMenu();
            displayMode = MENU;
            touched = false;
          } // else
        } // if
     } // for
     if (touched == false && p.x > tftWidth-40) {
       Serial.println("Touched the clock");
       displayClockAnalog();
       displayMode = CLOCK;
       touched = true;
      } // if
    } // else
    
   if (p.y < (TS_MINY-5)) {
      Serial.println("erase");
      // press the bottom of the screen to erase 
      //tft.fillRect(0, BOXSIZE, tft.width(), tft.height()-BOXSIZE, BLACK);     
    }
  }

  // Now, we grab packets and process them.
  sClear(thePacket); // Null out the packet when the loop starts
  tokenFound = false;
  while (bytesA = xbSerial.available()) {
    bytesR++;
#if DEBUG >= 1
    Serial.print((int)bytesA);
    Serial.print(" Byte(s) available (");
    Serial.print((int)bytesR);
    Serial.println("):");
#endif
    if (bytesR > MAXSTRING) { // Too much data coming in at once, bail and process what we've got
      thePacket[MAXSTRING-2] = ':';
      thePacket[MAXSTRING-1] = '\0';
      break;
    }
    // read the incoming byte:
    incomingByte = xbSerial.read();
    // Ignore any cruft we get until we get our first token separator. ACK errors cause havok
    // and resetting
    if ((incomingByte == ':') || (tokenFound)) {
      tokenFound = true;
      sAppend(thePacket,incomingByte);
#if DEBUG >= 1
      Serial.print("I received: ");
      Serial.print(incomingByte);
      Serial.print("[");
      Serial.print(incomingByte,HEX);
      Serial.println("]");
#endif
      if (incomingByte == 0xD) // Started getting a corrupted packet with a 0xD in the middle, bail
        break;
    } else {
      Serial.print("#");
    }
  }
  if (sLength(thePacket) > 0) {
    check_mem();
    lastUpdate = millis();
    Serial.println(thePacket);
    splitTokens(); // break into an array of tokenized strings    
    switch (displayMode) {
      case MENU: displayMenu();
      break;
      case CLOCK: displayClockAnalog();
      break;
      case DETAIL: displayDetail(detailID);
      break;
    }
  }
}

// ***********************************
// ************** displayDetail
// ***********************************
void displayDetail (uint16_t i) {
  tft.fillScreen(BLACK);
  tft.setCursor(0, 0);
  tft.setRotation(tftRotation);
  tft.setTextColor(WHITE); 
  tft.setTextSize(3);
  tft.print("ID:");
  tft.println(mpi[i]);
  tft.print("Temp:");
  tft.print(mpt[i]/((mpt[i] > 999) ? 100: 10));
  tft.print(".");
  tft.print(mpt[i]%((mpt[i] > 999) ? 100: 10));
  tft.println("F");
    for (idx = 0; idx < 4; idx++) {
    if (mpat[i][idx] != -99) {
      tft.print("Temp");
      tft.print(idx);
      tft.print(":");
      tft.print(mpat[i][idx]);
      tft.println("F");
    }
  }
  tft.print("Pressure:");
  tft.print(mpp[i]/((mpp[i] > 999) ? 100: 10));
  tft.print(".");
  tft.print(mpp[i]%((mpp[i] > 999) ? 100: 10));
  tft.println("Hg");
  tft.print("Humidity:");
  tft.print(mph[i]/((mph[i] > 999) ? 100: 10));
  tft.print(".");
  tft.print(mph[i]%((mph[i] > 999) ? 100: 10));
  tft.println("%");
  tft.print("Last update:");
  displayTime(mpu[i], 3);
  tft.print("Time: ");
  displayTime(ctime, 3);
}
  
// ***********************************
// ************** displayMenu
// ***********************************
void displayMenu () {
  char xtime[6];
  
  // Print tft info:
  tft.fillScreen(BLACK);
  tft.setCursor(0, 0);
  tft.setRotation(tftRotation);
  tft.setTextSize(4);
  tft.setTextColor(RED);
  tft.print("Time: ");
  displayTime(ctime, 4);
  tft.setRotation(0);
  
  // 0,0 is bottom left and 240, 0 is upper right in this orientation 180,120,60
  for (idx = 0;idx < maxID; idx++) {
    for (uint8_t ii = 0; ii < 8; ii++) {
      tft.drawRect(((BOXSIZE*(4-idx))-(ii/2)), (ii/2), BOXSIZE, (BOXSIZE*8)-10, WHITE);
    }
    rx1[idx] = (BOXSIZE*(4-idx));
    ry1[idx] = 0;
    rx2[idx] = rx1[idx] + BOXSIZE;
    ry2[idx] = BOXSIZE * 8;      
  }
  
  tft.setRotation(tftRotation);
  for (idx = 0; idx < maxID; idx++) { 
    tft.setTextColor(mpc[idx]); 
    tft.setTextSize(2);
    tft.print("\n "); // left hand padding  
    tft.setTextSize(3);
    tft.print(mpi[idx]);
    Serial.println(mpi[idx]);
    Serial.print(" Temp: ");
    Serial.println(mpt[idx]);
    tft.print(":");
    tft.print(mpt[idx]/((mpt[idx] > 999) ? 100: 10));
    tft.print(".");
    tft.print(mpt[idx]%((mpt[idx] > 999) ? 100: 10));
    tft.println("F");
  }  
}

// ***********************************
// ************** displayClock
// ***********************************
/*
void displayClock () {
  tft.fillScreen(BLACK);
  tft.setCursor(0, 0);
  tft.setRotation(tftRotation);
  tft.setTextSize(10);
  tft.println("");
  displayTime(ctime,11);
  tft.setTextSize(2);
  tft.setRotation(0);
}
*/

// ***********************************
// ************** displayTime
// ***********************************
void displayTime (uint16_t time,uint8_t tsize) {
  uint16_t ttime;
  char xtime[6];
  short int idx; // Override the global with one that can go negative
  
  #ifdef DEBUG >= 3
  Serial.print("In displayTime with: ");
  Serial.println(time);
  #endif
  
  if (millis() > lastUpdate + 120000)
    tft.setTextColor(YELLOW);
  else
    tft.setTextColor(RED);
  tft.setTextSize(tsize);

  // Make sure we print a 4 digit time
  for (idx = 4, ttime=time; idx >= 0; idx--) {
    if (idx == 2)
      xtime[idx] = ':';
    else {
      xtime[idx] = 48 + (ttime % 10);       
      ttime /= 10;
    }
  }
  xtime[5] = '\0';
  tft.println(xtime);
  Serial.println("Exiting displayTime");
}

// ***********************************
// ************** displayClockAnalog
// ***********************************
void displayClockAnalog () {

  // Set rotation, so everything is drawn in perspective
  tft.setRotation(tftRotation);

  uint16_t x2, y2;
  short int i;
  
#define HOUR (ctime / 100) % 12
#define MINUTE (ctime % 100) % 60
#define MIDPX tft.width()/2
#define MIDPY tft.height()/2

  Serial.print("Displaying hour: ");
  Serial.print(HOUR);
  Serial.print(" minute: ");
  Serial.println(MINUTE);
  
  // Rotate hands 90 degrees, 0,0 bottom left
  //tft.setRotation(0);
  //hour = (hour + 3) % 12;
  //minute = (minute + 15) % 60;
  
  // Or, reset rotation
  // tft.setRotation(tftRotation);
  
  tft.fillScreen(BLACK);
  tft.setCursor(0, 0);
  for (idx = 0; idx < 5; idx++) {
    tft.drawCircle(MIDPX, MIDPY, (tft.height() < tft.width()) ? MIDPY - idx: MIDPX - idx, RED);
  }

    // Draw hour hand, compensating for how many minutes past the hour, so the hand doesn't just "jump" every hour

    for (i = -2; i < 3; i++) {    
      x2 = MIDPX - 75 * sin((3.141596 / 180.0 * -((HOUR + (MINUTE/60.0)) * 30.0)));
      y2 = MIDPY - 75 * cos((3.141596 / 180.0 * -((HOUR + (MINUTE/60.0)) * 30.0)));
      tft.drawLine(MIDPX+i, MIDPY+i, x2+i, y2+i, BLUE);
      
    }

  for (i = -2; i < 3; i++) {    
    x2 = MIDPX - 100 * sin((3.141596 / 180 * -(MINUTE * 6)));
    y2 = MIDPY - 100 * cos((3.141596 / 180 * -(MINUTE * 6)));
    tft.drawLine(MIDPX+i, MIDPY+i, x2+i, y2+i, GREEN);
  }
}

// ***********************************
// ************** parseToken
// ***********************************

void parseToken(char *tok, char *val, int wackaSid) {
  Serial.print("Parsing for token: ");
  Serial.print(tok);
  Serial.print(" with value: ");
  Serial.println(val);

  if (sEquals(tok,"c")) { // clock time
    ctime = atoi(val);
    Serial.print("Time: ");
    Serial.println(ctime);
  }
  if ((sEquals(tok,"a")) || (sEquals(tok,"id"))) { // sensor id
    sid = val;
    tempSid = pickSensor(sid);
    if (tempSid < 0)
      return;
  }
  Serial.print("Parse token, sid is ");
  Serial.println(tempSid);
  mpu[tempSid] = ctime; // Set the last-updated time for this sensor to now
  if (sEquals(tok,"t")) { // temperature
    ft = atof(val); // set temp to null terminated string
    mpt[tempSid] = int(ft * 10);
    pt = mpt[tempSid];
  } else if (sEquals(tok,"T")) { // alternate temperature
    //ft = atof(val);
  } else if (sEquals(tok,"h")) { // humidity
    fh = atof(val);
    mph[tempSid] = int(fh * 10);
    ph = mph[tempSid];
  } else if (sEquals(tok,"p")) { // pressure
    fp = atof(val);
    mpp[tempSid] = int(fp * 100);
    pp = mpp[tempSid];
  } else if (sEquals(tok,"reset")) {
    if (sEquals(val,"reset")) {
      //resetDefaults(); // we got a "reset/reset" token, undo any customizations
    }
  } else if (sEquals(tok, "t0")) {
    mpat[tempSid][0] = atof(val);
  } else if (sEquals(tok, "t1")) {
    mpat[tempSid][1] = atof(val);
  } else if (sEquals(tok, "t2")) {
    mpat[tempSid][2] = atof(val);
  } else if (sEquals(tok, "t3")) {
    mpat[tempSid][3] = atof(val);
  } 
}

// ***********************************
// ************** splitTokens
// ***********************************
int splitTokens() {
  char tmpString[MAXSTRING];
  int ntok = 0;
  char *token = ":";
  //char **outString = theTokens;
  //char *theString = thePacket;
  char cmd[MAXTOKENLEN], data[MAXTOKENLEN];
  boolean getCommand = true;
  int mySid = 0;
  
  #ifdef DEBUG
  Serial.println("Entering splitTokens...");
  Serial.println(thePacket);

  if (sContains(thePacket, token)) 
    Serial.println("Got a token");
  else
    Serial.println("no tokens");
  #endif
  
  while (sContains(thePacket, token)) {
    #ifdef DEBUG
    Serial.println("Getting token");
    #endif
    check_mem();
    if (sIndexOf(thePacket, token) > 0) {
      if (getCommand) {
        sClear(cmd);
        sCopy(cmd, sSubstring(thePacket, 0, sIndexOf(thePacket, token)));
        sTrim(cmd);
        if (sContains(COMMANDS, cmd)) // Make sure this is a command
          getCommand = false;
      } else {
        sClear(data);
        sCopy(data, sSubstring(thePacket, 0, sIndexOf(thePacket, token)));
        sTrim(data);
        getCommand = true;
        parseToken(cmd, data, tempSid);
      } 
      ntok++;
    }
    sClear(tmpString);
    sCopy(tmpString,sSubstring(thePacket, sIndexOf(thePacket, token) + 1, 
                                          sLength(thePacket)));
    
    sClear(thePacket);
    sCopy(thePacket, tmpString);
    
// This indicates a corrupt string, indicating we've run out of memory somewhere
    if (sLength(thePacket) > MAXSTRING) {
      Serial.println("");
      Serial.print(sLength(thePacket));
      Serial.println("******** CORRUPT STRING ERROR *********");
      sClear(thePacket);
    }
  }
  // Check to see if we're at the end of the string, and if so
  // make sure and grab the last token
  if (sLength(thePacket) > 0) {
    ntok++;
  }
  Serial.println("Exiting");
  sClear(thePacket);
  return(ntok);
}

// ***********************************
// ************** Supporting string functions
// ***********************************

// ***********************************
// ************** sSubstring
// ***********************************
char *sSubstring(char *theString, int s, int e) {
  int ii = 0;
  
  #ifdef DEBUG >= 3
  Serial.print("entering sSubstring");
  check_mem();
  #endif

  sClear(tempString);

  for (int i = s,ii = 0; i < e; i++, ii++) {
    tempString[ii] = theString[i];
  }

  #ifdef DEBUG >= 3
  Serial.print("leaving sSubstring");
  check_mem();
  #endif
  return(tempString);
}

// ***********************************
// ************** sAppend
// ***********************************
void sAppend(char theString[], char token) {
  theString[sLength(theString)] = token;

}

// ***********************************
// ************** sCopy
// ***********************************
void sCopy(char *theString, char *newString) {

  for (idx = 0; idx < sLength(newString); idx++)
    theString[idx] = newString[idx];
  theString[sLength(newString)] = '\0';
#ifdef DEBUG >= 3
  Serial.print("In sCopy ");
  check_mem();
  Serial.print("Copied: \"");
  Serial.print(newString);
  Serial.print("\" To: \"");
  Serial.print(theString);
  Serial.println("\"");
#endif
}

// ***********************************
// ************** sIndexOf
// ***********************************
int sIndexOf(char *theString, char *token) {
  for (idx = 0; idx < MAXSTRING; idx++) {
    if (theString[idx] == token[0])
      return(idx);
  }
  return (-1);
}

// ***********************************
// ************** sContains
// ***********************************
boolean sContains(char *theString, char *token) {
  uint8_t ii = 0;
  if (sLength(theString) == 0)
    return false;
  for (idx = 0; idx < sLength(theString); idx++) {
    if (theString[idx] == token[ii]) {
      for (ii = 1; ii < sLength(token); ii++, idx++) {
        if (theString[idx] != token[ii])
          break;
      }
      return(true);
    }
  }
  return(false);
}

// ***********************************
// ************** sLength
// ***********************************
byte sLength(char theString[]) {
  for (idx1 = 0; idx1 < MAXSTRING; idx1++) {
    if (theString[idx1] == '\0') {
      return(idx1);
    }
  }
  return (-1);
}

// ***********************************
// ************** sClear
// ***********************************
void sClear(char *theString, int len) {
  for (idx = 0; idx < len; idx++)
    theString[idx] = '\0';
}

// ***********************************
// ************** sEquals
// ***********************************
boolean sEquals (char *theString, char *token) {
  if (sLength(theString) != sLength(token))
    return(false);
  for (idx = 0; idx < sLength(theString); idx++) {
    if (theString[idx] != token[idx]) {
      return(false);
    }
  }
  return(true);
}

// ***********************************
// ************** sTrim
// ***********************************
void sTrim(char *theString) {

  boolean s = false;
  uint8_t ix = 0, ii;
  uint8_t b;
  
  sClear(t);
  ii = sLength(theString);
  #ifdef DEBUG >= 2
  Serial.print("In sTrim");
  check_mem();
  /*
  Serial.print("in: i - ");
  Serial.print((int)ix);
  Serial.print(" ii - ");
  Serial.println((int)ii);
  */
  #endif
  while ((ix < ii) && (theString[ix] == ' '))
    ix++;
    
    //Serial.print("{");
    b = theString[ii];
    //Serial.print(b,HEX);
    //Serial.println("}");  
  while ((ii > 0) && ((theString[ii] == 0x20) || (theString[ii] == 0x00))) {
    theString[ii] = 0x00;
    ii--;
  }
#if DEBUG >=3
  Serial.print("trim ");
  Serial.print((int)ix);
  Serial.print(" to ");
  Serial.print((int)ii);
  Serial.print(" with '");
  Serial.print(theString);
  Serial.print("' to '");
#endif
  sCopy(t, sSubstring(theString,ix,sLength(theString)));
#ifdef DEBUG >= 3
  Serial.print("Trimmed to '");
  Serial.print(t);
  Serial.println("'");
  check_mem();
#endif
  sClear(theString);
  sCopy(theString, t);
}


// ***********************************
// ************** Other support functions
// ***********************************

// ***********************************
// ************** pickSensor
// ***********************************

short int pickSensor(char *sid) {
  
/* Update this to return a sensor ID if there is a map to any we've ever seen, 
   or create a new ID if it's not found. Need to change the fixed strings to an array that
   gets filled as sensors check in, and then use the array index as the ID */
   
  for (uint8_t idx = 0; idx < maxID; idx++) { // Override the idx global
    if (sContains(sid,mpi[idx])) { 
      #ifdef DEBUG >= 0
      Serial.print("I know this sensor '");
      Serial.print(sid);
      Serial.print("' is id: ");
      Serial.println(idx);
      #endif
      return idx;
    }
  }
  if (maxID < MAXSENSORS) {
    sCopy(mpi[maxID],sid);
    maxID++;
    #ifdef DEBUG > 0
    Serial.print("I've never seen this sensor '");
    Serial.print(sid);
    Serial.print("' is id: ");
    Serial.println(maxID-1);
    #endif
    return maxID-1;
  }
  else {
    return -1;
  }
}