Monday, October 8, 2012

Source Code - Installment One

Edit 15-Oct-2012:   Posted corrected code - missed a few typos. GpioPoller.c is now the multiplexed version. Edit to make HTML behave nicely.

I have been looking at this source code issue from the wrong perspective.  I knew that I would be posting my source code here eventually, but I didn't think that it would be useful to that many people.  That was when tunnel vision had me thinking of just this alarm system project.

The example code that I present here is really much more widely applicable.  This is my main function, which implements a daemon process in C.  Also included are my data structures and my method for using worker threads.  Copious comments have been added to help clarify things.

RPiHouse.h    data structures, function prototypes, and global variables

/*--------------------------------------------------------------------------- RPiHouse.h - include file for the Raspberry Pi re-write of controld 08-Aug-2012 Ted Hale add enums and new dev struct 08-Oct-2012 Cleanup and comments for release of source ---------------------------------------------------------------------------*/ #define PIDFILE "/var/run/RPiHouse.pid" #define CONFIGFILE "/pihome/RPiHouse.conf" #define DEVICEFILE "/pihome/devices.conf" #define MAXDEVICES 100 #define MYPORT 17100 #define MAXCONNECTIONS 10 #define BYTE unsigned char //#include "mysql.h" //#include "mysqld_error.h" // causes Global variables to be defined in the main // and referenced as extern in all the other source files #ifndef EXTERN #define EXTERN extern #endif // device types typedef enum { X10, Gout, Gin } DevType; // device categories typedef enum { Light, OutdoorLight, MotionSensor, DoorSensor, Other } DevCategory; // the device structure typedef struct { char *name; DevType type; BYTE addr; BYTE house; DevCategory category; char *oncmd; char *offcmd; int stat; time_t tOn; time_t tOff; } Device; // "at" commands structure. this is used in a linked list typedef struct { char *cmd; // command to perform time_t time; // when to perform it int period; // -1: one time, else seconds to add for next time void *next; // next in queue } At_qEntry; // prototype definitions for the worker threads void *ListenerThread(void *param); void *GpioPoller(void *param); void *X10Thread(void *param); void *LogicThread(void *param); // some other prototype definitions int DoCommands(char *Cmd); int LogToClients(char *format, ... ); int AtCmd(char *cmd); void X10TurnOn(char *dev); void X10TurnOff(char *dev); void DoTurnOnOff(char *name, int onoff); char *CatName(int n); char *TypeName(int n); // GLOBAL variables. A lock needs to be used to prevent any // simultaneous access from multiple threads EXTERN int kicked; // flag for shutdown or restart EXTERN int nDevices; // number of devices defined EXTERN Device dev[MAXDEVICES]; // the array of devices ///EXTERN MYSQL *conn; // the DB connection EXTERN int logsock[MAXCONNECTIONS]; // log listeners EXTERN time_t Sunrise; // time of sunrise for today EXTERN time_t Sunset; // time of sunset for today

main.c    The entry point for the program

/*--------------------------------------------------------------------------- main.c By Ted B. Hale part of the home alarm and automation system previously known as "control" renamed to RPiHouse for this rebuild on the Raspberry Pi This file implements the main for a daemon process 01-Nov-2009 Starting over from scratch (mostly) on Linux 23-Jul-2010 on VersaLogic Jaguar embedded system now disabled weather thread 03-Aug-2012 re-write for Raspberry Pi 08-Oct-2012 Cleanup and comments for release of source ---------------------------------------------------------------------------*/ #include <errno.h> #include <stdio.h> #include <stdarg.h> #include <string.h> #include <stdlib.h> #include <signal.h> #include <sys/timeb.h> #include <pthread.h> // database not used yet //#define dbhost "localhost" //#define dbuser "control" //#define dbpass "secret" //#define dbdatabase "control" // this defines the pre-processor variable EXTERN to be nothing // it results in the variables in RPiHouse.h being defined only here #define EXTERN #include "RPiHouse.h" //************************************************************************ // reads the device file defined in RpiHouse.h andsets up the dev table int ReadDevices() { FILE *f; char line[200]; char *p; int i, n; // open the device config file f = fopen(DEVICEFILE,"r"); if (!f) { Log("Failed to open device file [%s]\n",CONFIGFILE); return 0; } // read lines from the file. It is structured like a windows ini file // where the device names are the section names ( enclosed in [] ) nDevices = 0; n = -1; while (read_line(f,line)>=0) { //Log("READCONFIG: %s",line); // these are all lines to ignore - comments and lines that are blank if ((line[0]==';')||(line[0]=='#')||(line[0]==' ')||(line[0]==0)) continue; // is this a device name if (line[0]=='[') { n++; nDevices = n+1; // is the table full if (nDevices>=MAXDEVICES) { Log(" ***** Out of devices *****"); break; } // add a new device to the table dev[n].name = strdup(line+1); p = strchr(dev[n].name,']'); if (p) *p = 0; dev[n].stat = 0; dev[n].tOn = 0; dev[n].tOff = 0; continue; } // ignore everything until a device is defined if (nDevices==0) continue; // parse out "variable=value" // get pointer to = p = strchr(line,'='); // if no =, then skip if (!p) continue; // this will put a null terminator after the variable name *p=0; // bump p by 1 to have it point at the value p++; // the line may have a newline or other character on the end // this will fix that if (p[strlen(p)-1]<' ') p[strlen(p)-1] = 0; // set specified variable // device type if (!strcmp(line,"type")) { if (!strcasecmp (p,"X10")) { dev[n].type = X10; } else if (!strcasecmp (p,"Gout")) { dev[n].type = Gout; } else if (!strcasecmp (p,"Gin")) { dev[n].type = Gin; } } // device address if (!strcmp(line,"addr")) { dev[n].addr = atoi(p); } // house code part of address for X10 devices if (!strcmp(line,"house")) { dev[n].house = *p-'A'; } // device category if (!strcmp(line,"category")) { if (!strcasecmp (p,"Light")) { dev[n].category = Light; } else if (!strcasecmp (p,"MotionSensor")) { dev[n].category = MotionSensor; } else if (!strcasecmp (p,"DoorSensor")) { dev[n].category = DoorSensor; } else { dev[n].category = Other; } } // execute this command when the device turns on // mostly useful for GPIO inputs but can be used for X10 too if (!strcmp(line,"on")) { dev[n].oncmd = strdup(p); } // same for when it turns off if (!strcmp(line,"off")) { dev[n].offcmd = strdup(p); } } ///Log("Done reading devices"); // close the config file fclose(f); // output a table of the devices to the log file for (i=0; i<nDevices; i++) { Log(" %-20s %-15s %-5s address: %2d %2d", dev[i].name, CatName(dev[i].category), TypeName(dev[i].type), dev[i].addr, dev[i].house); } return 0; } //************************************************************************ // handles signals to restart or shutdown void sig_handler(int signo) { switch (signo) { case SIGPWR: break; case SIGHUP: // do a restart Log("SIG restart\n"); LogToClients("SIG restart"); kicked = 1; break; case SIGINT: case SIGTERM: // do a clean exit Log("SIG exit\n"); LogToClients("SIG exit"); kicked = 2; break; } } //************************************************************************ // and finally, the main program // a cmd line parameter of "f" will cause it to run in the foreground // instead of as a daemon int main(int argc, char *argv[]) { pid_t pid; FILE *f; pthread_t tid1,tid2,tid3,tid4,tid5; // thread IDs struct tm *today; // check cmd line param if ((argc==1) || strncmp(argv[1],"f",1)) { //printf("going to daemon mode\n"); // Spawn off a child, then kill the parent. // child will then have no controlling terminals, // and will become adopted by the init proccess. if ((pid = fork()) < 0) { perror("Error forking process "); exit (-1); } else if (pid != 0) { exit (0); // parent process goes bye bye } // The child process continues from here setsid(); // Become session leader; } // trap some signals signal(SIGTERM, sig_handler); signal(SIGINT, sig_handler); signal(SIGPWR, sig_handler); signal(SIGHUP, sig_handler); // save the pid in a file pid = getpid(); f = fopen(PIDFILE,"w"); if (f) { fprintf(f,"%d",pid); fclose(f); } // open the debug log LogOpen("/pihome/logs/RPiHouse"); // database not added back yet // init MySQL interface /* conn = mysql_init(NULL); if (conn == NULL) { Log("mysql_init Error %u: %s\n", mysql_errno(conn), mysql_error(conn)); } else { if (mysql_real_connect(conn, dbhost, dbuser, dbpass, dbdatabase, 0, NULL, 0) == NULL) { Log("Error %u: %s\n", mysql_errno(conn), mysql_error(conn)); mysql_close(conn); conn = NULL; } }*/ // start the main loop do { LogToClients("STARTING"); // read config info ReadDevices(); // start the various threads tid1 = tid2 = tid3 = tid4 = tid5 = 0; pthread_create(&tid1, NULL, GpioPoller, NULL); pthread_create(&tid2, NULL, X10Thread, NULL); pthread_create(&tid3, NULL, LogicThread, NULL); pthread_create(&tid4, NULL, ListenerThread, NULL); ///pthread_create(&tid5, NULL, MiscThread, NULL); // wait for signal to restart or exit do { sleep(1); } while (!kicked); // wait for running threads to stop if (tid1!=0) pthread_join(tid1, NULL); if (tid2!=0) pthread_join(tid2, NULL); if (tid3!=0) pthread_join(tid3, NULL); if (tid4!=0) pthread_join(tid4, NULL); if (tid5!=0) pthread_join(tid5, NULL); // exit? if (kicked==2) break; // else restart, set flag back to 0 kicked = 0; } while (1); // forever // delete the PID file unlink(PIDFILE); return 0; }


GpioPoller.c    The GPIO polling thread

/*--------------------------------------------------------------------------- GpioPoller.c Poll the GPIO devices via the wiringPi interface Ted Hale 08-Aug-2012 initial version for Raspberry Pi re-write of controld 08-Oct-2012 Cleanup and comments for release of source 14-Oct-2012 modify for muxed input ---------------------------------------------------------------------------*/ #include <errno.h> #include <stdio.h> #include <stdarg.h> #include <string.h> #include <stdlib.h> #include <signal.h> #include <sys/timeb.h> #include <pthread.h> #include <wiringPi.h> #include "RPiHouse.h" // Thread entry point, param is not used void *GpioPoller(void *param) { int pin, i, x, a0, a1, a2; // initialize the WireingPi interface Log("GpioPoller: init wiringPi"); if (wiringPiSetup () == -1) { Log("Error on wiringPiSetup. GpioPoller thread quitting."); return; } Log("GpioPoller: init devices"); // initialize input circuit // muxed input - 0 is input 1-3 are address bits pinMode (0, INPUT); pullUpDnControl(0,PUD_UP); for (i=1; i<4; i++) { pinMode (i, OUTPUT); digitalWrite(i, 0); } // relay outputs are 4-7 for (i=4; i<8; i++) { pinMode (i, OUTPUT); digitalWrite(i, 0); } // start polling loop do { for (i = 0 ; i < nDevices ; i++) { switch (dev[i].type) { case Gin: // set mux address a0 = ((dev[i].addr & 1)==1)?1:0; a1 = ((dev[i].addr & 2)==2)?1:0; a2 = ((dev[i].addr & 4)==4)?1:0; digitalWrite(1, a0); digitalWrite(2, a1); digitalWrite(3, a2); // mux needs a tiny amount of time for the value to settle Sleep(0); // input is pulled high, so 1 is off and 0 (shorted to ground) is on x = digitalRead(0); // if on if (x==0) { // and it wasn't already on if (!dev[i].stat) { Log("GpioPoller> %s ON\n",dev[i].name); LogToClients("%s ON",dev[i].name); /////DoCommands(dev[i].oncmd); } time(&dev[i].tOn); dev[i].stat=1; } else { // if Off // and it wasn't already off if (dev[i].stat) { Log("micropoller> %s OFF\n",dev[i].name); LogToClients("%s OFF",dev[i].name); /////DoCommands(dev[i].offcmd); } time(&dev[i].tOff); dev[i].stat=0; } // Save to DB // ??? this should be only when the value changes !!! /*if (conn!=NULL) { sprintf(sql,"insert into data (var,val) VALUES ('%s',%d)",dev[i].name,dev[i].stat); if (mysql_query(conn, sql)) { Log("mysql_query Error sql: %s\n errno = %u: %s", sql, mysql_errno(conn), mysql_error(conn)); } }*/ break; case Gout: // do nothing break; default: // other modes not yet supported break; } } // let other thread run, sleep 10ms Sleep(10); } while (kicked==0); // exit loop if flag set }

4 comments:

  1. Hi Ted!

    Can you say us how did you do to download/install/import mysql.h into your raspberry pi. I'm unable to realize that and I don't want to make it in Python...
    Thank you for your help!

    ReplyDelete
    Replies
    1. I actually had not tried MySQL yet from the RaspberryPi, but it worked as expected.

      You need to have the MySQL development libraries installed:
      sudo apt-get install libmysqlclient-dev

      Add to your makefile option for gcc
      -I/usr/include/mysql

      add to the link step
      -L/usr/local/lib/mysql -lmysqlclient


      Here is a snippet of C code to get you started.
      plenty of resources can be found via google.


      // change these to suit your needs
      #define dbhost "localhost"
      #define dbuser "control"
      #define dbpass "secret"
      #define dbdatabase "control"

      MYSQL *conn;

      conn = mysql_init(NULL);
      if (conn == NULL)
      {
      printf("mysql_init Error %u: %s\n", mysql_errno(conn), mysql_error(conn));
      }
      else
      {
      if (mysql_real_connect(conn, dbhost, dbuser, dbpass, dbdatabase, 0, NULL, 0) == NULL)
      {
      printf("Error %u: %s\n", mysql_errno(conn), mysql_error(conn));
      mysql_close(conn);
      conn = NULL;
      }
      }

      Delete
  2. Hi Ted,

    Just wondering if you have updated the RPiHouse code since this last post.. Its been a while so I thought I would ask. I'm planning on using it for my garage/shop. Its overkill for what I need the the framework is there so that makes it easy for me ;-)

    John - jrsphoto@gmail.com

    ReplyDelete
    Replies
    1. It's been over two years. I'm sure I have made many changes to the software in that time. However, it would all be things that likely wouldn't matter to you - things specific to my setup.
      Good luck with your project.

      Delete