Thursday, October 27, 2011

Homemade Geiger Counter - Part 3 -- The Arduino software

WARNING >>>>>>LONG>>>>>> POST

The core processor in the Geiger counter is a Arduino ATmega328. It has 2kBytes of sdram 32kB of Flash and 2kb of EEPROM. And its running at 16Khz. Consider this carefully.

It means that memory will be completely filled with the following;
* 2000 character string
* 400 word sentence (at a 5 char average per word )
* an array of 500 32 bit integers

It means that excessive overhead and code niceties are often sacrificed for the sake of code speed.

Since it is a waste of more memory to keep a table of malloced and freed areas of memory embedded software is generally designed to using compile time data structures or stack allocated variables rather than heap data.

The Geigers counters code is built in this manor, Its not petty but its functional and efficient. The code is divided into 2 files the header and main code file

The header contains the constants such as which parts of that hardware and wired to what pins.

If you look closely you might not that I made a mistake when i selected the pins for the LCD since A3(Analogue 3) is already in use by the WIZNET(wireless radio) chip.

Another point to note is that is the "state" and "LcdDisplayMode" enums, this clearly implies that the code makes use of at least 1 state machine. In actual fact there are several but more on that later. I often consider stateful code to be the poor-mans threads. Coding this way allows the low spec hardware to quickly juggle a rather large set of real-time tasks without taking too much time for each. All you have to do a a code is segment the code into manageable chunks and allocate a state for each subsection or task.

enum states
  {
    RESET     = 0,
    SOFT_RESTART,
    INIT_DHCP,
    OBTAIN_IP,
    INIT_CLIENT,
    NORMAL,
    CONNECT,
    SEND_DATA,
    RESP_DATA
  };

typedef enum {
  LCD_AVE_READING,
  LCD_INST_READING,
  LCD_STATE,
  LCD_UPTIME,
  LCD_MAX_MODE
} LcdDisplayMode;

// DEVICE SETTINGS -- REPLACE WITH A PROPER MAC ADDRESS
byte macAddress[] = { 0xDE, 0xAD, 0xBE, 0xEF, 0xBA, 0xBE };

// DEVICE SETTINGS NetRAD - this is specific to the NetRAD board
#define  GEIGER_INTERRUPT_PIN  1 // pin of the gieger counter 
#define  PIN_SPKR              6 // pin number of piezo speaker
#define  PIN_LED               7 // pin number of event LED
#define  WIZNET_RESET_PIN     A1
#define  RADIO_SELECT         A3

//DEVICE SETTINGS LCD Shield
#define BUTTON            A0
#define LCD_PINS          9,8,A2,A5,A3,A4

#define URI_MAX          20
#define LAN_ON_MASK      0x80
#define DHCP_ON_MASK     0x40
#define FAIL_COUNT_MASK  0x3f

You will note the "tubes" variable uses the array of structs trick so that I can statically allocate a mix of strings and integers into its initialization. This array provides the factory standard calibration constants for Caesium and the most commonly found Geiger tubes that are compatible with the NetRad shield, as you can see the board was built to be very flexible.

Also note that Arduinos PROGMEM macro keeps this data firmly out of SDRAM and EEPROM memory and in the flash. This makes it necessary to later load the data from the flash when needed but saves the SDRAM from instant overflow and frees up more working memory. I also encountered a problem with putting floats into the flash so dividing the mantissa and exponent become necessary.

#define MAX_TUBE 8
#define CUSTOM_TUBE 1
typedef struct 
{
  const char* label;
  uint32_t    convFactorMag;  //something breaks if u put floats into PROGMEM..
  int8_t      convFactorExp;
} TubeEntry;

// this is ugly but.. dont blame me http://arduino.cc/en/Reference/PROGMEM
prog_char tubeLabel1[] PROGMEM = "UNKNOWN";
prog_char tubeLabel2[] PROGMEM = "CUSTOM_TUBE";
prog_char tubeLabel3[] PROGMEM = "LND_712";
prog_char tubeLabel4[] PROGMEM = "SBM_20";
prog_char tubeLabel5[] PROGMEM = "J408GAMMA";
prog_char tubeLabel6[] PROGMEM = "J306BETA";
prog_char tubeLabel7[] PROGMEM = "INSPECTOR";
prog_char tubeLabel8[] PROGMEM = "CRM100";

//floats dont seem to be healthy in PROGMEM either!
// 1CPM = ?? uS/hr
PROGMEM TubeEntry tubes[MAX_TUBE] =
  {
    // label,    uSv/hr factor
    { tubeLabel1, 0,     0 },   // ???      - use nothing!
    { tubeLabel2, 0,     0 },   // ???      - use conversion factor without caring
    { tubeLabel3, 2333, -6 },   // 0.002333 - http://www.lndinc.com/products/348/
    { tubeLabel4, 57,   -4 },   // 0.0057   - http://www.libelium.com/wireless_sensor_networks_to_control_radiation_levels_geiger_counters
    { tubeLabel5, 0,     0 },   // ???      - North Optic
    { tubeLabel6, 0,     0 },   // ???      - North Optic
    { tubeLabel7, 29,   -4 },   // 0.0029   - 
    { tubeLabel8, 0,     0 },   // ???      - 
  }; 

And the definitions of the main memory stores in the system state_t and settings_t.
* state_t is the non-permanent set of operating conditions, it will exist in SDRAM
* settings_t is the permanent data that is loaded and save to the EEPROM. In theory I can make this consume up to 2k however it needs to be loaded into SDRAM when the device is operating, as a result you still need to use it sparingly, or break it into pages.

//static flashed settings
typedef struct
{
  //tube/counter settings
  float    conversionCoefficient; // The conversion coefficient from cpm to µSv/h
  uint32_t readingIntervalMillis;
  uint8_t  tubeModel;
  uint8_t  emaHalfLife;    // the number of samples before 50% redution for Expondential average comptation

  //ether setings
  uint8_t  lanDhcpFails;  //bit 1 lanOn, bit 2dhcpOn, bit 3-8 FailedConsCount
  uint8_t  ipAddr[4];
  uint8_t  gateway[4];
  uint8_t  subnet[4];
  
  //upload server settings
  uint8_t  uploadIPAddr[4];  
  char     uploadURI[URI_MAX];
} settings_t;

// Interrupt mode:
// * For most geiger counter modules:     FALLING
// * Geiger Counter Twig by Seeed Studio: RISING

//dynamic runtime settings
typedef struct
{
  //execution state
  uint8_t state;
  uint8_t lcd_mode;
  bool    lcd_refresh;

  //lan status
  uint8_t  connFailCnt;
  uint32_t dhcpMaintainceTime; // The last connection time to disconnect from the server after uploaded feeds
  uint32_t lastConnectionTime; // The last connection time to disconnect from the server after uploaded feeds  

  //time status
  uint32_t now;  // millisec accumulative counter -- prone to overflow!

  // uptime status..
  uint16_t upTimeMillis; 
  uint8_t  upTimeSec;
  uint8_t  upTimeMin;
  uint8_t  upTimeHours;
  uint8_t  upTimeDays;

  // reading data last 10.. 
  volatile uint8_t eventFlag;  // Event flag signals when a geiger event has occurred
  uint32_t         count;      // Value to store counts per minute
 
  //current event
  uint32_t lastReading;  // millisec accumative counter -- prone to overflow!
  uint32_t totalCount;              
  float    countsPerMinute;
  float    microsievertPerHour;
 
  //Exponential moving average 
  float    emaCountsPerMinute;
  float    emaMicrosievertPerHour;
} state_t;

The Arduino comes with a nice set of headers, the internal unit support is provided by the avr libs. While the LiquidCrystal, Ethernet support the most common of peripheral devices used with by hobbyists.

As you can see the take the static compile time object approach common in embedded software. One note here new and delete don't work in the Arduino compilers so don't even bother trying to use them.

#include <stdint.h>
#include <avr/eeprom.h>
#include <avr/wdt.h>
#include <limits.h>
#include <LiquidCrystal.h>
#include <SPI.h>
#include <Ethernet.h>
#include "PrivateSettings.h"

// this holds the info for the device
static settings_t settings;

// holds the control info for the device
static state_t state;

//Web Client      
Client client;

//LCD
LiquidCrystal lcd(LCD_PINS);

One large draw back with the Ardiunos is that printf is missing and the output interfaces tend to be inhomogeneous enough to be frustrating. In the end it annoyed me sufficiently well enough that I ended up writing a facade for the whole mess. It of course adds a slight overhead but in terms debug; the ability to redirect any output to network, serial port or lcd was invaluable.

//Print wrapper system.. cause it sucks without it
class PrintCore
{
public:
  virtual void endl() const = 0;
  virtual void clr() const = 0;
  virtual void p(const unsigned long val) const = 0;
  virtual void p(const unsigned int  val) const = 0;
  virtual void p(const int           val) const = 0;
  virtual void p(const char*         val) const = 0;
  virtual void p(const char          val) const = 0;
};

class PrintLCD : public PrintCore
{
private:
  mutable int line;
public:
  PrintLCD() : line(0) {}
  virtual void endl() const                     { line = (line+1)%2; lcd.setCursor(0,line); }
  virtual void clr()  const                     { lcd.clear(); line = 0; lcd.setCursor(0,0); }
  virtual void p(const unsigned long val) const { lcd.print(val,DEC); }
  virtual void p(const unsigned int  val) const { lcd.print(val); }
  virtual void p(const int           val) const { lcd.print(val); }
  virtual void p(const char*         val) const { lcd.print(val); }
  virtual void p(const char          val) const { lcd.print(val); }
};

class PrintSerial : public PrintCore
{
  virtual void endl() const                     { Serial.println(); }
  virtual void clr()  const                     { }
  virtual void p(const unsigned long val) const { Serial.print(val,DEC); }
  virtual void p(const unsigned int  val) const { Serial.print(val); }
  virtual void p(const int           val) const { Serial.print(val); }
  virtual void p(const char*         val) const { Serial.print(val); }
  virtual void p(const char          val) const { Serial.print(val); }
};

class PrintWebClient : public PrintCore
{
  virtual void endl() const                     { client.println(); }
  virtual void clr()  const                     { }
  virtual void p(const unsigned long val) const { client.print(val,DEC); }
  virtual void p(const unsigned int  val) const { client.print(val); }
  virtual void p(const int           val) const { client.print(val); }
  virtual void p(const char*         val) const { client.print(val); }
  virtual void p(const char          val) const { client.print(val); }
};

//main print wrapper..
class PrintClass : public PrintCore
{
  PrintCore*     core;
  PrintLCD       lcd_mode;
  PrintSerial    serial_mode;
  PrintWebClient client_mode;

public:
  PrintClass() { serial();  }

  //mode
  void serial() { core = &serial_mode; } 
  void lcd()    { core = &lcd_mode;    } 
  void client() { core = &client_mode;    } 
  
  //standard
  virtual void endl() const                     { core->endl(); }
  virtual void clr()  const                     { core->clr();}
  virtual void p(const unsigned long val) const { core->p(val); }
  virtual void p(const unsigned int  val) const { core->p(val); }
  virtual void p(const int           val) const { core->p(val); }
  virtual void p(const char*         val) const { core->p(val); }
  virtual void p(const char          val) const { core->p(val); }  
  
  //commons
  void pln(const char*          val) const { core->p(val); core->endl(); }
  void ulng(const unsigned long val) const { core->p(val); }
  
  //specials
  void prog(const prog_char* data) const
  {
    //IN FLASH(code) print
    //WARNING ardindo have to use special functions conserve sram use
    // this wraps up the print string to use the PSTR flash offload string
    char c=pgm_read_byte(data++);
    while(c != 0)
      {
   p(c);
   c=pgm_read_byte(data++);
      }
  }
  
  void progln(const prog_char* data) const
  {
    prog(data);
    endl();    
  }

  void pad(const int val, int padding=-1) const
  { 
    int mult = 1;
    if (padding > 0)
      {
 //not designed for -ves
 while(--padding > 0)
   mult *= 10;
 
 while((val < mult) &&
       (mult != 1))
   {
     p('0');  
     mult /= 10;
   }
      }
    p((int)val); 
  }
  
  void flt(double val, byte precision) const
  {
    if( precision <= 0) 
      return;
  
    // prints val with number of decimal places determine by precision
    // precision is a number from 0 to 6 indicating the desired decimial places
    // example: printDouble( 3.1415, 2); // prints 3.14 (two decimal places)
    //rounding
  
    unsigned long mult = 1;  //start at 10 for rounding
    byte padding = precision-1;
    while(precision--) mult *=10;
  
    val += 0.5/mult; //round
    
    p(int(val));  //prints the int part
    p('.'); // print the decimal point
    
    unsigned long frac;
    
    //get fraction and remove negative
    if(val >= 0) frac = (val - int(val)) * mult;
    else         frac = (int(val)- val ) * mult;
    
    //padding compute
    unsigned long frac1 = frac;
    while(frac1 /= 10) padding--;
    
    //print padding
    while(padding--) p('0');
    
    //print faction 
    //p(frac,DEC) ;
    p(frac) ;
  }

  //OK getting highly geiger specific now...

  // Just a utility function to nicely format an IP address.
  void ip(const uint8_t* ipAddr)
  {
    for(int i = 0;i < 4; i++)
      {
 if (i!=0) p('.');
 p((int)ipAddr[i]);
      }
  }

  void upTime(bool lng=true)
  {
    const char* comma = PSTR(":");
    pad(state.upTimeDays);      prog(PSTR(" days "));
    if(!lng) endl();
    pad(state.upTimeHours , 2); prog(comma);
    pad(state.upTimeMin   , 2); prog(comma);
    pad(state.upTimeSec   , 2); prog(comma);
    if(lng) pad(state.upTimeMillis, 3); 
  }
  
  void conversionFactor()
  {
    prog(PSTR("ConversionFactor : 1CPM is "));
    flt(settings.conversionCoefficient,4);
    prog(PSTR(" uS/Hr"));
    
    if (settings.conversionCoefficient != 0)
      {
 prog(PSTR("Hence 1 uS/h is : "));
 flt((1.0/settings.conversionCoefficient),4);
 prog(PSTR(" CPM "));
      }
  }
  
  void tubeLabel()
  {
    if (settings.tubeModel < MAX_TUBE)
      {
 prog(PSTR(" - ")); 
 const char* labelProgmemPtr = (const char*)pgm_read_word(&tubes[settings.tubeModel].label);
 prog(labelProgmemPtr); 
      }
  }
};

PrintClass print;

The main user interface to the device is the USB based serial port, this interface is how the device is initially configured and calibrated(and its software is uploaded via this also). The next part of the code will handle user input via this method.

The core routine in this was the "cmdPoll" this would simply gather input coming in from the serial port until a enter is struck. The gathered line is then dispatched for processing in the "cmdParse" function, which compares it to another array of strings and function pointers. This then dispatch's it out to the found command function or dumps the help if needed.

Note that the majority of the commands are build as getter/setters. If ran parameter-less they will dump the current setting. But if a parameter is supplied it will first update it and then dump the new setting. It reduces coding effort and cuts down the mess of commands.

/**************************************************************************/
// Set address
/**************************************************************************/
void cmdSetLanOn(char *args)
{
  if (args)
    {
      if (strtol(args, NULL, 10) != 0)
 settings.lanDhcpFails |= LAN_ON_MASK;
      else
 settings.lanDhcpFails &= ~LAN_ON_MASK;
      state.state = SOFT_RESTART;
    }
  print.prog(PSTR("Lan is set to ")); print.pad((settings.lanDhcpFails & LAN_ON_MASK) != 0); print.progln(PSTR(" Off=0"));
}

void cmdSetDhcpOn(char *args)
{
  if (args)
    {
      if (strtol(args, NULL, 10) != 0)
 settings.lanDhcpFails |= DHCP_ON_MASK;
      else
 settings.lanDhcpFails &= ~DHCP_ON_MASK;
      state.state = SOFT_RESTART;
    }
  print.prog(PSTR("DHCP set to ")); print.pad((settings.lanDhcpFails & DHCP_ON_MASK) != 0); print.progln(PSTR(" Off=0"));
}

void cmdSetFailsUntilReset(char *args)
{
  if (args)
    {
      uint8_t fails = strtol(args, NULL, 10);
      fails = fails & FAIL_COUNT_MASK;
      settings.lanDhcpFails = (settings.lanDhcpFails & ~FAIL_COUNT_MASK) | fails;
      state.state = SOFT_RESTART;
    }
  print.prog(PSTR("Max Fails til reset set to ")); print.pad(settings.lanDhcpFails & FAIL_COUNT_MASK);
}

void readIP(char *args, uint8_t* ipAddr)
{
  if (args)
    {
      char* part = strtok(args, ".");
      for(int i = 0;(i < 4) && (part != NULL); i++)
 {
   ipAddr[i] = strtol(part, NULL, 10);
   part = strtok(NULL, ".");
 }
      state.state = SOFT_RESTART;
    }
}

void cmdSetIpAddr(char *args)
{
  readIP(args, settings.ipAddr);
  print.prog(PSTR("IP set to: ")); print.ip(settings.ipAddr);
}

void cmdSetGateway(char *args)
{
  readIP(args, settings.gateway);
  print.prog(PSTR("Gateway set to: ")); print.ip(settings.gateway);
}

void cmdSetSubnet(char *args)
{
  readIP(args, settings.subnet);
  print.prog(PSTR("Subnet set to: ")); print.ip(settings.subnet);
}

void cmdSetUploadIpAddr(char *args)
{
  readIP(args, settings.uploadIPAddr);
  print.prog(PSTR("Upload IP address set to: ")); print.ip(settings.uploadIPAddr);
}

void cmdSetUploadURI(char *args)
{
  if (args)
    {
      uint32_t len = strlen(args);
      if (len > (URI_MAX-1))
 len = (URI_MAX-1);

      state.state = SOFT_RESTART;
      memset(settings.uploadURI, 0   , URI_MAX);
      memcpy(settings.uploadURI, args, strlen(args));
    }
  print.prog(PSTR("Upload URI set to: ")); Serial.println(settings.uploadURI);
}

/**************************************************************************/
// TUBE Settings
/**************************************************************************/
void cmdSetTube(char *args)
{
  if (args) 
    {
      settings.tubeModel = (uint32_t)strtol(args, NULL, 10);  
      if (settings.tubeModel > MAX_TUBE)
 settings.tubeModel = 0;
      if (settings.tubeModel != CUSTOM_TUBE)
 {
   //ugly -- but something goes wrong with floats in PROGMEM...
   uint32_t convFactMag = (pgm_read_word(&tubes[settings.tubeModel].convFactorMag));
   int8_t   convFactExp = (pgm_read_byte(&tubes[settings.tubeModel].convFactorExp));
   float convFactor = convFactMag;
   
   if (convFactExp < 0)
     while (convFactExp != 0)
       {
  convFactor /= 10.0;
  convFactExp++;
       }
   else if (convFactExp > 0)
     while (convFactExp != 0)
       {
  convFactor *= 10.0;
  convFactExp--;
       }
   settings.conversionCoefficient = convFactor;
 }
    }
  print.prog(PSTR("Tube model: ")); print.pad(settings.tubeModel); print.tubeLabel();
  print.endl();
  print.conversionFactor();  
}


void cmdSetConversionFactor(char *args)
{
  if (args) 
    settings.conversionCoefficient = strtol(args, NULL, 10);  
  print.conversionFactor();
}

void cmdSetReadingInterval(char *args)
{
  if (args) 
    settings.readingIntervalMillis = strtol(args, NULL, 10);  
  print.prog(PSTR("Reading Interval set to ")); print.ulng(settings.readingIntervalMillis); print.endl();
}

void cmdSetEmaHalfLife(char *args)
{
  if (args) 
    settings.emaHalfLife = strtol(args, NULL, 10);  
  print.prog(PSTR("EMA half life set to ")); print.pad(settings.emaHalfLife); print.endl();
}

/**************************************************************************/
// Print out the current device ID
/**************************************************************************/
void cmdReading(char *args)
{
  unsigned long deltaMillis = elapsedTime(state.lastReading);
  float guessCPM  = state.count * 60000 / deltaMillis;
  float guessDose = guessCPM * settings.conversionCoefficient;
  print.prog  (PSTR("UpTime:"));       print.upTime(); print.endl();
  print.progln(PSTR("Current:"));
  print.prog  (PSTR(" - time    : ")); print.ulng(deltaMillis); print.progln(PSTR("msec")); 
  print.prog  (PSTR(" - counts  : ")); print.pad(state.count);  print.progln(PSTR("counts")); 
  print.prog  (PSTR(" - guessed : ")); print.flt(guessCPM,4);   print.progln(PSTR("CPM"));  
  print.prog  (PSTR(" - guessed : ")); print.flt(guessDose,4);  print.progln(PSTR("uS/hr"));     
  print.progln(PSTR("Prior:"));
  print.prog  (PSTR(" - reading : ")); print.flt(state.countsPerMinute,4);     print.progln(PSTR("CPM"));
  print.prog  (PSTR(" - reading : ")); print.flt(state.microsievertPerHour,4); print.progln(PSTR("uS/hr"));
  print.progln(PSTR("Average:"));
  print.prog  (PSTR(" - reading : ")); print.flt(state.emaCountsPerMinute,4);     print.progln(PSTR("CPM"));
  print.prog  (PSTR(" - reading : ")); print.flt(state.emaMicrosievertPerHour,4); print.progln(PSTR("uS/hr"));
}

void cmdSettings(char *args)
{
  print.progln(PSTR(" GIGEIR TUBE:"));
  print.prog  (PSTR("   -  tube    : ")); print.pad(settings.tubeModel); print.tubeLabel(); print.endl();
  print.prog  (PSTR("   -  conv    : ")); print.flt(settings.conversionCoefficient,4); print.progln(PSTR(" uS/Hr = 1CPM"));  
  print.prog  (PSTR("   -  interval: ")); print.ulng(settings.readingIntervalMillis); print.endl();
  print.prog  (PSTR("   -  EMAHalf : ")); print.pad(settings.emaHalfLife); print.endl();
  print.progln(PSTR(" Lan:"));
  print.prog  (PSTR("   -  lan     : ")); print.pad((settings.lanDhcpFails & LAN_ON_MASK ) != 0); print.progln(PSTR(" (0=off)"));
  print.prog  (PSTR("   -  dhcp    : ")); print.pad((settings.lanDhcpFails & DHCP_ON_MASK) != 0); print.progln(PSTR(" (0=off)"));
  print.prog  (PSTR("   -  fail lim: ")); print.pad(settings.lanDhcpFails & FAIL_COUNT_MASK); print.endl();
  print.prog  (PSTR("   -  ip      : ")); print.ip(settings.ipAddr);       print.endl();
  print.prog  (PSTR("   -  gateway : ")); print.ip(settings.gateway);      print.endl();
  print.prog  (PSTR("   -  subnet  : ")); print.ip(settings.subnet);       print.endl();
  print.progln(PSTR(" UPLOAD:"));  
  print.prog  (PSTR("   -  upip    : ")); print.ip(settings.uploadIPAddr); print.endl();
  print.prog  (PSTR("   -  upuri   : ")); Serial.println(settings.uploadURI);

}

void cmdReset(char *args)
{  
  state.state = RESET;
  print.progln(PSTR("RESETING...\n"));
}

void cmdSave(char *args)
{  
  eeprom_write_block((byte *)&settings, 0, sizeof(settings_t));
  print.progln(PSTR("Settings saved\n"));
}

void cmdHelp(char *args)
{  
  print.progln(PSTR(" GIGEIR TUBE:"));
  print.progln(PSTR("   -  tube : set the tube type(autoset others)")); 
  print.progln(PSTR("   -  conv : set the 1CPM -> uS/Hr conversion"));  
  print.progln(PSTR("   -  inter: set the rate of reading compution/upload"));  
  print.progln(PSTR("   -  half : set the EMA half life"));  
  print.progln(PSTR(" Lan:"));
  print.progln(PSTR("   -  lan  : turn lan on/off"));
  print.progln(PSTR("   -  dhcp : turn dhcp on/off"));
  print.progln(PSTR("   -  ip   : set static ip"));
  print.progln(PSTR("   -  fail : number of upload fails before reset"));
  print.progln(PSTR("   -  gate : set static gateway"));
  print.progln(PSTR("   -  snet : set static subnet"));
  print.progln(PSTR(" UPLOAD:"));  
  print.progln(PSTR("   -  upip : set a custom server ip"));
  print.progln(PSTR("   -  upuri: set a custom server URI"));
  print.progln(PSTR(" ACTIONS:"));
  print.progln(PSTR("   -  read : display a detailed reading"));
  print.progln(PSTR("   -  sett : display the settings"));
  print.progln(PSTR("   -  reset: reset the device")); 
  print.progln(PSTR("   -  save : save any changes to settings")); 
  print.progln(PSTR(""));
}

//MAX cmds are 5 + 20 in args .. Ardiunos dont have masses of space
#define CMD_MAX_LENGTH 25
typedef struct 
{
  const char* cmd;
  void (*func)(char*);
} CmdEntry;

void cmdParse(char* msg)
{
  static CmdEntry cmdTable[] =
    {
      { "read",   cmdReading          },
      { "sett",   cmdSettings          },
      { "lan",    cmdSetLanOn            }, 
      { "dhcp",   cmdSetDhcpOn          },
      { "fail",   cmdSetFailsUntilReset  },
      { "ip",     cmdSetIpAddr          },
      { "gate",   cmdSetGateway          },
      { "snet",   cmdSetSubnet          },
      { "upip",   cmdSetUploadIpAddr     },
      { "upuri",  cmdSetUploadURI        },
      { "tube",   cmdSetTube   },
      { "conv",   cmdSetConversionFactor },
      { "inter",  cmdSetReadingInterval  },
      { "half",   cmdSetEmaHalfLife      },
      { "reset",  cmdReset   },
      { "save",   cmdSave   },
      { NULL,     NULL                   }
    };

  char* cmd = strtok(msg, " ");
  char* arg = strtok(NULL, " ");
  
  if (cmd == NULL) 
    {
      cmdHelp(NULL);
      return;
    }

  for (uint32_t i=0; cmdTable[i].cmd != NULL; i++)
    {
      if (!strcmp(cmd, cmdTable[i].cmd))
        {
   (*cmdTable[i].func)(arg);
   return;
        }
    }
  cmdHelp(NULL);
}

void cmdPoll()
{
  static char     msg[CMD_MAX_LENGTH+1];
  static uint8_t  msg_idx = 0;

  bool process = false;
  while (Serial.available())
    {
      if (msg_idx == 0)
 {
   print.endl();
   print.prog(PSTR("CMD> "));
 }
      
      char c = Serial.read();
      if (c == '\r')
 {
   process = true;
 }
      else if ( c == '\b')
 {
   Serial.print(c);
   if (msg_idx > 0)
            msg_idx--;
 }
      else if(!process)
 {
   if (msg_idx < CMD_MAX_LENGTH)
     {
       // normal character entered. add it to the buffer
       Serial.print(c);
       msg[msg_idx++] = c;
       msg[msg_idx]   = 0;
     }
 }
      else
 {
   //empty serial port... of junk after enter..
 }
    }

  if (process)
    {
      print.endl();
      Serial.println(msg);
      cmdParse(msg);
      
      msg_idx = 0;
      msg[msg_idx] = '\0';
    }
}

The Geiger is not always connected to a serial port. User input can also come in from the pull up switch added on the LCD shield and wired to the A1 pin. The button at the moment is setup to just toggle the LCD display modes. As you can see the lcd is refreshed in proxy from the button push via lcd_refresh this is so that other areas of the code can also trigger the lcd to change.

void handleButton()
{
  static bool buttonPushState = false;
  static bool prevButtonPushState = false;

  // check if the pushbutton is pressed.
  // if it is, the buttonState is HIGH:
  buttonPushState = (digitalRead(BUTTON) == HIGH);
  
  if (buttonPushState != prevButtonPushState)
    {
      prevButtonPushState = buttonPushState;
      if (buttonPushState == true)
 {
   state.lcd_mode = (state.lcd_mode+1)%LCD_MAX_MODE;
   state.lcd_refresh = true;
 }
    }

  if (state.lcd_refresh)
    {
      state.lcd_refresh = false;
      print.lcd(); 
      print.clr();
     
      switch (state.lcd_mode)
 {
 case LCD_AVE_READING:
   // LCD output
   print.prog(PSTR("Av:"));
   print.flt(state.emaCountsPerMinute,1);  
   print.endl();
   print.flt(state.emaMicrosievertPerHour,4);
   break;
 case LCD_INST_READING:
   // LCD output
   print.prog(PSTR("Rw:"));
   print.flt(state.countsPerMinute,1);  
   print.endl();
   print.flt(state.microsievertPerHour,4);
   break;
 case LCD_STATE:
   print.prog(PSTR("St:"));
   switch (state.state)
     {
     case RESET:        print.prog(PSTR("RST"));  break;
     case SOFT_RESTART: print.prog(PSTR("SRST")); break;
     case INIT_DHCP:    print.prog(PSTR("DHCP")); break;
     case OBTAIN_IP:    print.prog(PSTR("IP"));   break;
     case INIT_CLIENT:  print.prog(PSTR("CLNT")); break;
     case NORMAL:       print.prog(PSTR("NORM")); break;
     case CONNECT:      print.prog(PSTR("CNCT")); break;
     case SEND_DATA:    print.prog(PSTR("SEND")); break;
     case RESP_DATA:    print.prog(PSTR("RESP")); break;
     default:           print.prog(PSTR("????")); break;
     }
   print.endl();
   print.prog(PSTR("Fails:"));
   print.pad(state.connFailCnt,2);
   break;   
 case LCD_UPTIME:
   print.prog(PSTR("Up:"));
   print.upTime(false);
   break;
 }
      print.serial();
    }
}

Additionally for accutate computations of the radiation readings special care needs to be taken to compute the amount of time that has elapased between readings and updates of the clock. Poorly written clocking code will often drift quite dramatically, this destroying your readings accuracy. The key line is the "state.now = millis();" which samples and records the time atomically as possible, The calculations then account for timer overflows and computes the full time delta using the current and prior samples of the clock.

/**************************************************************************/
// calculate elapsed time. this takes into account rollover.
/**************************************************************************/
unsigned long elapsedTime(unsigned long startTime)
{
  unsigned long stopTime = millis();
  
  if (startTime >= stopTime)
    return startTime - stopTime;
  else
    return (ULONG_MAX - (startTime - stopTime));
}

void updateTime()
{
  unsigned long prevNow = state.now; 
  state.now = millis();
  
  unsigned long deltaMillis;
  if (state.now >= prevNow)
    deltaMillis = state.now - prevNow;
  else
    deltaMillis = ULONG_MAX - (prevNow - state.now);

  state.upTimeMillis += deltaMillis;
  if ( state.upTimeMillis >= 1000)
    {
      state.upTimeSec   += state.upTimeMillis / 1000;
      state.upTimeMillis = state.upTimeMillis % 1000;

      state.lcd_refresh |= (state.lcd_mode == LCD_UPTIME);
      
      if (state.upTimeSec >= 60)
 {
   state.upTimeMin += state.upTimeSec / 60;
   state.upTimeSec  = state.upTimeSec % 60;
   
   if (state.upTimeMin >= 60)
     {
       state.upTimeHours += state.upTimeMin / 60;
       state.upTimeMin    = state.upTimeMin % 60;
       
       if (state.upTimeHours >= 24)
  {
    state.upTimeDays += state.upTimeHours / 24;
    state.upTimeHours = state.upTimeHours % 24;
  }   
     } 
 }
    }
}

The final interface on the device is the Ethernet port. Using the port is a bit complex but luckly the Arduino has a Ethernet lib complete with a Web client. As a result all I really need to do is generate a raw HTTP get request to a waiting server with the current raw reading for it to save. And at some point later read back its response.

/**************************************************************************
    Send data to server 
**************************************************************************/
void handleClient() 
{
  if(state.state == CONNECT)
    {
      if (client.connected()) 
 {
   return;
   print.prog(PSTR("Disconnecting."));
   client.stop();
 }
      
      print.endl();
      // Try to connect to the server
      print.prog(PSTR("Connecting."));
      if (client.connect()) 
 {
   print.progln(PSTR("Connected."));
   state.lastConnectionTime = millis();
   
   // clear the connection fail count if we have at least one successful connection
   state.connFailCnt = 0;     
   state.state = SEND_DATA;
   state.lcd_refresh |= (state.lcd_mode == LCD_STATE);
 }
      else 
 {
   state.connFailCnt++;
   print.prog(PSTR("Failed count:")); print.pad(state.connFailCnt);
   print.endl();
   uint8_t fail_max = settings.lanDhcpFails & FAIL_COUNT_MASK;
   if((fail_max >0) && (state.connFailCnt > fail_max))
     state.state = SOFT_RESTART;
   else
     state.state = NORMAL;
   state.lcd_refresh |= (state.lcd_mode == LCD_STATE);
 }
    }
  else if(state.state == SEND_DATA)
    {
      print.prog(PSTR("Upload."));
      
      print.client();
      print.prog(PSTR("GET ")); 
      print.p(settings.uploadURI); 
      print.prog(PSTR("?cpm=")); 
      print.flt(state.countsPerMinute,1);
      print.prog(PSTR("&mSh=")); 
      print.flt(state.microsievertPerHour,4);
      print.progln(PSTR(" HTTP/1.0")); 
      print.endl();
      print.serial();

      state.state = RESP_DATA;
      state.lcd_refresh |= (state.lcd_mode == LCD_STATE);

      print.progln(PSTR("Uploaded."));
    }
  else if(state.state == RESP_DATA)
    { 
      //handle data transmission
      while(client.available()) 
 {
   // Echo received strings to a host PC
   char c = client.read();
   Serial.print(c);
 }

      if (!client.connected()) 
 {
   Serial.println("disconnected.");
   client.stop();
   state.state = NORMAL;
   state.lcd_refresh |= (state.lcd_mode == LCD_STATE);
 }     
    }
}

This bring us to the main body of the code. The Arduino's dont have a main per-say. They use 2 functions. The "setup" function is a run once piece of code just after reset. The "loop" function is basically an endless while(1) loop. The Chibi board is setup so that Ardiuno uses "wdt_enable(WDTO_8S)" as a hardware stop watch. If the stop watch runs out the hardware resets and starts fresh(In this case its was set to 8 secs). The later "wdt_reset()" restarts the timer freash for the next iteration of the loop. This way the Geiger can never get stuck in an unexpected state.

Also note the only interrupt that is registered in the system is the onPulse function. This is the most critical piece of the code, it counts the output from the Geiger tube. Note that its not protected from multiple interrupts, this is because the geiger tubes dead time is sufficently long enough(and the code is next to atomic anyway) for the code to complete and be ready for the next event.

The main loop can be summarized as
* handle reset
* handle serial cmds
* handle user feedback of radiative events
* compute new total uptime
* compute new reading if sampling period is over
* handle button input LCD output
* handle network setup
* handle web client communication

/**************************************************************************/
/*!
    On each falling edge of the Geiger counter's output,
    increment the counter and signal an event. The event 
    can be used to do things like pulse a buzzer or flash an LED
*/
/**************************************************************************/
void onPulse() 
{
  state.count++;
  state.eventFlag = 1;  
}

/**************************************************************************/
// main setup and loop...
/**************************************************************************/
void setup() 
{
  delay(20);

  // fill in the UART file descriptor with pointer to writer.
  //fdev_setup_stream (&uartout, uart_putchar, NULL, _FDEV_SETUP_WRITE);
  
  // The uart is the standard output device STDOUT.
  //stdout = &uartout ;

  // set up the LCD's number of columns and rows: 
  lcd.begin(8, 2);

  // Print a message to the LCD.
  lcd.print("Reset!");

  Serial.begin(57600);

  print.progln(PSTR("Reseting..")); // tick to the usb console 

  // put radio in idle state
  pinMode(RADIO_SELECT, OUTPUT);
  digitalWrite(RADIO_SELECT, HIGH);    // disable radio chip select
  
  // reset the Wiznet chip
  pinMode(WIZNET_RESET_PIN, OUTPUT);
  digitalWrite(WIZNET_RESET_PIN, HIGH);
  delay(20);
  digitalWrite(WIZNET_RESET_PIN, LOW);
  delay(20);
  digitalWrite(WIZNET_RESET_PIN, HIGH);
  
  // get the device info
  eeprom_read_block((byte*)&settings, 0, sizeof(settings_t));
  
  // init the control info
  memset(&state, 0, sizeof(state_t));
  //
  // enable watchdog to allow reset if anything goes wrong      
  wdt_enable(WDTO_8S);

  // Attach an interrupt to the digital pin and start counting
  // Note:
  // Most Arduino boards have two external interrupts:
  // numbers 0 (on digital pin 2) and 1 (on digital pin 3)
  attachInterrupt(GEIGER_INTERRUPT_PIN, onPulse, RISING);

  state.state = SOFT_RESTART;
  state.lcd_refresh |= (state.lcd_mode == LCD_STATE);
  state.count = 0;
  
  // kick the watch dog
  wdt_reset();

  print.progln(PSTR("Reset..")); // tick to the usb console 
}

void loop() 
{
  //polling loop.. very fast.. pfft
  
  // kick the dog only if we're not in RESET state. if we're in RESET
  // we will just let the device gracefully reset via the watchdag time out..
  if (state.state != RESET) wdt_reset();        
  else                      state.lcd_refresh |= (state.lcd_mode == LCD_STATE);

  if (state.state == SOFT_RESTART) 
    {
      state.state = INIT_DHCP;
      state.lcd_refresh |= (state.lcd_mode == LCD_STATE);
    }

  cmdPoll();

  // user events -- poll the command line for any input
  if (state.eventFlag)
    {
      // Advertise a geiger event
      state.eventFlag = 0;    // clear the event flag for later use
      
      print.prog(PSTR(".")); // tick to the usb console 
      
      tone(PIN_SPKR, 1000);      // beep the piezo speaker
      digitalWrite(PIN_LED, HIGH); // flash the LED
      delay(20);                  
      digitalWrite(PIN_LED, LOW); 
      noTone(PIN_SPKR);          // turn off the speaker pulse
    }
  
  // up time update -- to fast loosing to many millis
  updateTime();

  if (settings.readingIntervalMillis == 0) return;

  // uS per hour calc and dump...
  unsigned long deltaMillis = elapsedTime(state.lastReading);
  if (deltaMillis > settings.readingIntervalMillis)
    {
      state.lastReading = millis();
      // loop now cut down to about whatever the UPDATE has been changed to
      
      // count current cpm run..
      state.countsPerMinute = (float)state.count * 60000.0 / (float)deltaMillis;
      state.count = 0;
      
      // Convert from cpm to õSv/h with the pre-defined coefficient
      state.microsievertPerHour = state.countsPerMinute * settings.conversionCoefficient;
      
      //EMA compute
      float alpha = 2.0/((float)(settings.emaHalfLife+1));
      if (state.emaCountsPerMinute == 0)
 {
   state.emaCountsPerMinute     = state.countsPerMinute;
   state.emaMicrosievertPerHour = state.microsievertPerHour;
 }
      else
 {
   state.emaCountsPerMinute = alpha*state.countsPerMinute 
     + (1.0 - alpha)*state.emaCountsPerMinute;
   state.emaMicrosievertPerHour = alpha*state.microsievertPerHour
     + (1.0 - alpha)*state.emaMicrosievertPerHour;
 }

      print.endl();
      print.prog(PSTR("Up: ")); print.upTime();    print.prog(PSTR(" "));    
      print.flt(state.countsPerMinute,1);  print.prog(PSTR("CPM ")); 
      print.flt(state.microsievertPerHour,4);    print.prog(PSTR("uS/hr")); 
      print.prog(PSTR(" EMAve: "));
      print.flt(state.emaCountsPerMinute,1);     print.prog(PSTR("CPM ")); 
      print.flt(state.emaMicrosievertPerHour,4); print.prog(PSTR("uS/hr"));       

      state.lcd_refresh |= (state.lcd_mode == LCD_AVE_READING) ||
 (state.lcd_mode == LCD_INST_READING);
    }

  handleButton();

  if ((settings.lanDhcpFails & LAN_ON_MASK) != 0)
    {
      if ((settings.lanDhcpFails & DHCP_ON_MASK) != 0)
       {
         //// boot and handle dhcp
   //if (state.state == INIT_DHCP)
         //  {
         //    EthernetDHCP.begin(macAddress, 1);
         //    state.state = OBTAIN_IP;
   //    state.lcd_refresh |= (state.lcd_mode == LCD_STATE);
         //  }
         //else
   //  handleDHCP();
       }
      else 
        {
          //no dhcp.. straight to client
         if ((state.state == INIT_DHCP) ||
             (state.state == OBTAIN_IP))
     {
       state.state = INIT_CLIENT;
       state.lcd_refresh |= (state.lcd_mode == LCD_STATE);
       
       //need to fill in these
       Ethernet.begin(macAddress,
        settings.ipAddr, 
        settings.gateway,
        settings.subnet); 
       print.progln(PSTR("Static Ethernet setup.")); // tick to the usb console 
     }
       }

      if (state.state == INIT_CLIENT)
       {
   //boot and handle web client
   //client.stop();
   client.init(settings.uploadIPAddr, 80);
         state.state = NORMAL;
   state.lcd_refresh |= (state.lcd_mode == LCD_STATE);
       }
      else if (state.state >= NORMAL)  //only 1 per watch dog tick.. hence else if
       {
         if (deltaMillis > settings.readingIntervalMillis)
           {
             // send data if ready
       if (state.state == NORMAL)
  {
    state.state = CONNECT;
    state.lcd_refresh |= (state.lcd_mode == LCD_STATE);
  }
           }
   
   handleClient();
        }
    }
}

No comments:

Post a Comment