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();
        }
    }
}

Sunday, October 16, 2011

Homemade Geiger Counter - Part 2 -- The hardware

Homemage Geiger Counter - Part 2 -- Hardware build

The main objective here is cheap and some what accurate.

Allow me to clarify this point. Reversed biased PN junctions(avalanche photo-diode et el) are very desirable as a Geiger counters
* They are cheap to mass produce using common manufacturing processes.
* If the circuitry and PN diodes are well chosen then the counter can be more accurate and faster than a Geiger tube or even scintillation crystals.

The problem is post Fukushima Geiger counters where in such high demand that anything that ticks in the presence of radiation was acceptable. Basically all of these are PN junction based devices, that are badly slapped together devices rushed out to a sellers market. What is worst is that these cheap devices where selling up of 500 USD a pop shortly after the event. A quick check of amazon shows me that the same models are now selling at around the 200 USD mark after having peaked at around 1500 USD!

I figure if I going to get crap it may as well be crap i have created for 1/15 of the price. So the device I have is around the 100 USD mark with usb and networking capabilities.



I used a Kited setup that the Tokyo hacker space member Akiba put together.


It involves an Ardiunio base board called Chibi(bottom in the picture). And a second shield they called a NetRAD(top in the above), which is basically a board with a LAN networking chip and the Geiger counter circuitry.


The final shield is an LCD and button. As you can see it was just hacked together with a cheap a prebuilt 600yen LCD board and a pull up switch.

The NetRADs specs are here http://tokyohackerspace.org/ja/project/tokyo-hackerspace-netrad-geiger-shield. And the Dev history of the board is here. http://www.tokyohackerspace.org/en/blog/tokyo-hackerspacerdtn-geiger-shield-dev-history. It makes for a very interesting read.

A fuller set of picture are visible here

I was hoping to do a full tear-down of the device but I simply can't seem to get the time.

I have also noticed the device seems to have a wide working range, a series of readings can vary by any thing upto 100% from the average. If this is due to the nature of radioactivity or something iffy in the Geigers power supply I don't know. Besides this the device is still excellent. The readings still average out to a stable reading over a 10min period.

Wednesday, October 12, 2011

dyndns for an iphone server

Dynamic IP are common at residential properties and are quite a problem if you want your home server to be constantly reachable from outside. The solution is a dynamic IP tracking host like dyndns. Since im working from a hacked iphone a second problem is that perl doesnt seem to exist for it. So using a standard package is a bit of an issue. Here is a quick hacked up ruby daemon that can handle the dyndns updates.

Save this ruby script at /var/mobile/dyndns/update_ip.rb, (dont forget to chmod it to runable.)

UPDATE: It appears that dyndns gets peeved if you keep updating to the same IP. The code has been patched for it.
#!/usr/bin/env ruby

require 'base64'

host = "example.dyndns.tv"
auth = Base64.encode64('user:password')
timeout = 600

while 1
  output = ""
  puts "Discovering current IP.."
  
  # current ip resolve from checkip.dyndns.com
  IO.popen("telnet checkip.dyndns.com 80", "w+") do |pipe|
    output = pipe.readline
    puts output
  
    pipe.puts "GET / HTTP/1.1"
    pipe.puts "Host: checkip.dyndns.com"
    pipe.puts "Connection: close"
    pipe.puts ""
    pipe.puts ""
  
    puts "SENT ip query.."
    output = pipe.readlines.join("");
    puts "---------------RESPONSE--------------------"
    puts output
    puts "---------------  END   --------------------"
    pipe.close_write
  end
  
  if output =~ /.*Current IP Address: ([0-9]+.[0-9]+.[0-9]+.[0-9]+).*/
     ip = $1
     ip = ip.chomp
     puts ""
     puts "DETECTED At: " + ip 
  end

  oldip = ""
  File.open('currentIP.txt', 'r') do |f|
    oldip = f.gets
    oldip = oldip.chomp
  end
  puts "PRIOR At: " + oldip   

  if oldip == ip 
    puts "MATCH sleeping..."
  else
    File.open('currentIP.txt', 'w') do |f|
      f.puts ip
    end  

    exit    
    puts ""
    puts "installing new IP..."
    
    #http://login:password@members.dyndns.org/nic/update?hostname=example.dyndns.tv&myip=99.99.99.99
    IO.popen("telnet members.dyndns.org 80", "w+") do |pipe|
      output = pipe.readline
      puts output
    
      pipe.puts "GET /nic/update?hostname=" + host + "&myip=" + ip + " HTTP/1.1"
      pipe.puts "Host: members.dyndns.org"
      pipe.puts "User-Agent: codeslimjim/1.0 rubyscript/1.0"
      pipe.puts "Authorization: Basic " + auth
      pipe.puts "Connection: close"
      pipe.puts ""
      pipe.puts ""
    
      puts "SENT new ip.."
      output = pipe.readlines
      puts "---------------RESPONSE--------------------"
      puts output
      puts "---------------  END   --------------------"
      pipe.close_write
    end
  end

  #timeout for x sec..
  sleep timeout
end

Then install the plist daemon
touch /Library/LaunchDaemons/com.dyndns.plist
chmod go-wrx /Library/LaunchDaemons/com.dyndns.plist
nano /Library/LaunchDaemons/com.dyndns.plist

Paste in the code
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Label</key>
    <string>com.dyndns</string>
    <key>ProgramArguments</key>
    <array>
        <string>/var/mobile/dyndns/update_ip.rb</string>
    </array>
    <key>RunAtLoad</key>
    <true/>
    <key>KeepAlive</key>
    <true/>
    <key>UserName</key>
    <string>mobile</string>
    <key>WorkingDirectory</key>
    <string>/var/mobile/dyndns</string>
</dict>
</plist>

And boot/load up the daemon and test
launchctl load -w /Library/LaunchDaemons/com.dyndns.plist
ps aux | grep dyndns 

Remmber if u need to shut it down use
launchctl unload -w /Library/LaunchDaemons/com.dyndns.plist

Saturday, October 1, 2011

Homemade Geiger Counter - Part 1 -- The basics

My Blog has been kind of quiet lately. the Reason is that I have been focused on building a Geiger counter. So here is what I have learnt.

Geiger basics. A Geiger counter is basically a simple event counting device. A Geiger tube connects to an amp, noise filter, comparator circuit and some form of counting system.

The Geiger tube is basically a outer housing, containing gas and a cathode and anode. The housing can be metal or even ultra thin foils, The gas is normally some inert gas and the cathode/anode can be made using anything from glass to metals.

The Geiger tube is charged to high voltage, this excites the gas inside the tube. In this state no current is yet flowing and the tube appears as an open circuit. When radiation is incident on the tube it must first travel through the outer housing, and strike one of the excited gases particles. This collision will tear free one or more electrons and these with then bump into other atoms and tear free more electrons and thus creating an avalanche effect.

Note carefully the limitations of the device,
1) the radiation must travel through the tubes outer housing and strike a gas atom. So the outer housing of the tube will absorb some particles without causing a detectable effect. Also particles can travel right thought the tube without causing an event. The size and thickness of the tube also effects how sensitive it is to radiation. But designers of the tube need to consider mechanical damage from shocks and contact with other items. Most directional tubes have a very thin wall made of mica that is very delicate. In summary there are a large range of tubes with different sensitivity to the various forms of radiations.

2) The main incident particle transfers energy to a single electron knocking it off the atom, this electron then hits another, these 2 electrons then knock more free and so on until there are no more electrons to be knocked free, this is the avalanche effect. This sea of freed electrons flows out the cathode, and eventually the gas recharges to high energy state. Now if the radiation was to knock a second electron free or second unconnected event was to hit the gases in the tube at the same time the tube is discharging then there is no way to tell the difference. This period is the tubes dead time. A Geiger counter that is taken into a high radiation area will suddenly go silent this is not broken it is because there are so many events hitting the tube that avalanche will never stop and the comparator will never detect the switch between Geiger's tubes quiescent state and the avalanche discharge state

3) This is a very simple device all it does is count events. Using it for anything other than the number counts requires some level of interpretation.

What these limitations mean is that what caused the event that was counted cannot be determined. If we assume your measuring air and it was an alpha particle then it is unlikely to penetrate skin and is therefore a lower dose. If it was gamma it will likely pass through your body and the wall behind u, thus giving it a bit more potential to cause some real damage to you. (Its not that simple but sufficient to highlight my point). So the conversion of counts to dosage is highly dependent on the type of tube your using, and the procedure that you used to measure it, what is causing the count (IE the spectrum of radioactive substances your measuring), and what the interaction of the substance is with the living being being dosed. In summary the devices dose conversion calibration needs to understood rather carefully, and the device used correctly.

Now Geiger counters don't necessarily need to be made with a a Geiger tube. A cheaper an less accurate design is to apply a similar circuit to a standard solar panel or LDR that is covered to prevent the entry of normal light but allow gamma radiation to still be incident on the panel or junction. These designs will measure radiation however, solar panel's and LDRs are not designed for radiation readings and so these designs have even more drawbacks than a standard Geiger counter.

Unfortunately post fukushima, Geiger counter prices sky rocketed and supplies became rarefied. These cheaper devices where affordable and available in a market with buyers who were desperate to get their hands on something/anything that can read the radiation. As a result these cheaper devices have been sold en-mass in japan and few customers realize the difference between what they have purchased and what the real item is. Keep in mind that the device does work so its technically not illegal to sell it as a radiation measurement instrument... Its just that if the customer is expecting a real device then they are getting hoodwinked by their own limited understanding. I have tried explaining this to Joe average before only it never seems to work. The discussion seems to always work out like explaining the difference between 24 Carat Gold and 12 Carat Gold to a person who only wants to understand that Gold is yellow.

Also note that Geiger tubes are far from the ultimate device to measure radiation a scintillator crystal is capable of producing varying colors and intensity of light as radiation strikes it, devices using these crystals are a level(and cost) far beyond the humble Geiger counter.