--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/thermometers/thermometers.c Sun May 25 22:06:56 2014 +0200 @@ -0,0 +1,553 @@ +/***************************************************************************** + * Copyright (C) 2014 + * + * Michiel Broek <mbroek at mbse dot eu> + * + * This file is part of the mbsePi-apps + * + * This is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation; either version 2, or (at your option) any + * later version. + * + * mbsePi-apps is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with EC-65K; see the file COPYING. If not, write to the Free + * Software Foundation, 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. + *****************************************************************************/ + +#include "thermometers.h" + + +#define STATUS_CONNECTING 0 +#define STATUS_CONNACK_RECVD 1 +#define STATUS_WAITING 2 + +/* Global variables for use in callbacks. */ +static int qos = 0; +static int status = STATUS_CONNECTING; +static int mid_sent = 0; +static int last_mid = -1; +static int last_mid_sent = -1; +static bool connected = true; +static bool disconnect_sent = false; +static bool connect_lost = false; +static bool my_shutdown = false; +static pid_t pgrp, mypid; + +extern bool debug; +extern sys_config Config; +#ifdef HAVE_WIRINGPI_H +extern int lcdHandle; +#endif + +int server(void); +void help(void); +void die(int); + + +void help(void) +{ + fprintf(stdout, "mbsePi-apps thermometers v%s starting\n\n", VERSION); + fprintf(stdout, "Usage: thermomeneters [-d] [-h]\n"); + fprintf(stdout, " -d --debug Debug and run in foreground\n"); + fprintf(stdout, " -h --help Display this help\n"); +} + + + +void die(int onsig) +{ + switch (onsig) { + case SIGHUP: syslog(LOG_NOTICE, "Got SIGHUP, shutting down"); + break; + case SIGINT: syslog(LOG_NOTICE, "Keyboard interrupt, shutting down"); + break; + case SIGTERM: syslog(LOG_NOTICE, "Got SIGTERM, shutting down"); + break; + default: syslog(LOG_NOTICE, "die() on signal %d", onsig); + } + + my_shutdown = true; +} + + + +void my_connect_callback(struct mosquitto *mosq, void *obj, int result) +{ + if (connect_lost) { + connect_lost = false; + syslog(LOG_NOTICE, "Reconnect: %s", mosquitto_connack_string(result)); + } + + if (!result) { + status = STATUS_CONNACK_RECVD; + } else { + syslog(LOG_NOTICE, "my_connect_callback: %s\n", mosquitto_connack_string(result)); + } +} + + + +void my_disconnect_callback(struct mosquitto *mosq, void *obj, int rc) +{ + if (my_shutdown) { + syslog(LOG_NOTICE, "Acknowledged DISCONNECT from %s", Config.mosq_host); + connected = false; + } else { + /* + * The remove server was brought down. We must keep running + */ + syslog(LOG_NOTICE, "Received DISCONNECT from %s, connection lost", Config.mosq_host); + connect_lost = true; + } +} + + + +void my_publish_callback(struct mosquitto *mosq, void *obj, int mid) +{ + last_mid_sent = mid; +} + + + +void my_log_callback(struct mosquitto *mosq, void *obj, int level, const char *str) +{ + syslog(LOG_NOTICE, "MQTT: %s", str); + printf("MQTT: %s\n", str); +} + + + +#ifdef HAVE_WIRINGPI_H +void stopLCD(void) +{ + lcdClear(lcdHandle); + setBacklight(0); +} +#endif + + + +int main(int argc, char *argv[]) +{ + int rc, c, i; + pid_t frk; +#ifdef HAVE_WIRINGPI_H + char buf[80]; +#endif + + while (1) { + int option_index = 0; + static struct option long_options[] = { + {"debug", 0, 0, 'c'}, + {"help", 0, 0, 'h'}, + {0, 0, 0, 0} + }; + + c = getopt_long(argc, argv, "dh", long_options, &option_index); + if (c == -1) + break; + + switch (c) { + case 'd': debug = true; + break; + case 'h': help(); + return 1; + } + } + + openlog("thermometers", LOG_PID|LOG_CONS|LOG_NOWAIT, LOG_USER); + syslog(LOG_NOTICE, "mbsePi-apps thermometers v%s starting", VERSION); + if (debug) + fprintf(stdout, "mbsePi-apps thermometers v%s starting\n", VERSION); + + if (rdconfig((char *)"thermometers.conf")) { + fprintf(stderr, "Error reading configuration\n"); + syslog(LOG_NOTICE, "halted"); + return 1; + } + + /* + * Catch all the signals we can, and ignore the rest. Note that SIGKILL can't be ignored + * but that's live. This daemon should only be stopped by SIGTERM. + * Don't catch SIGCHLD. + */ + for (i = 0; i < NSIG; i++) { + if ((i != SIGCHLD) && (i != SIGKILL) && (i != SIGSTOP)) + signal(i, (void (*))die); + } + +#ifdef HAVE_WIRINGPI_H + + if (wiringPiSetup () ) + return 1; + + if ((rc = initLCD (16, 2))) { + fprintf(stderr, "Cannot initialize LCD display, rc=%d\n", rc); + return 1; + } + + lcdPosition(lcdHandle, 0, 0); + lcdPuts(lcdHandle, "Thermometers"); + lcdPosition(lcdHandle, 0, 1); + sprintf(buf, "Version %s", VERSION); + lcdPuts(lcdHandle, buf); +#endif + + if (debug) { + /* + * For debugging run in foreground. + */ + rc = server(); + } else { + /* + * Server initialization is complete. Now we can fork the + * daemon and return to the user. We need to do a setpgrp + * so that the daemon will no longer be assosiated with the + * users control terminal. This is done before the fork, so + * that the child will not be a process group leader. Otherwise, + * if the child were to open a terminal, it would become + * associated with that terminal as its control terminal. + */ + if ((pgrp = setpgid(0, 0)) == -1) { + syslog(LOG_NOTICE, "setpgpid failed"); + } + + frk = fork(); + switch (frk) { + case -1: + syslog(LOG_NOTICE, "Daemon fork failed: %s", strerror(errno)); + syslog(LOG_NOTICE, "Finished, rc=1"); +#ifdef HAVE_WIRINGPI_H + stopLCD(); +#endif + exit(1); + case 0: /* + * Run the daemon + */ + fclose(stdin); + if (open("/dev/null", O_RDONLY) != 0) { + syslog(LOG_NOTICE, "Reopen of stdin to /dev/null failed"); + _exit(2); + } + fclose(stdout); + if (open("/dev/null", O_WRONLY | O_APPEND | O_CREAT,0600) != 1) { + syslog(LOG_NOTICE, "Reopen of stdout to /dev/null failed"); + _exit(2); + } + fclose(stderr); + if (open("/dev/null", O_WRONLY | O_APPEND | O_CREAT,0600) != 2) { + syslog(LOG_NOTICE, "Reopen of stderr to /dev/null failed"); + _exit(2); + } + mypid = getpid(); + rc = server(); + break; + /* Not reached */ + default: + /* + * Here we detach this process and let the child + * run the deamon process. + */ + syslog(LOG_NOTICE, "Starting daemon with pid %d", frk); + exit(0); + } + } + + syslog(LOG_NOTICE, "Finished, rc=%d", rc); + return rc; +} + + + +int server(void) +{ + char *id = NULL, *state = NULL; + struct mosquitto *mosq = NULL; + char hostname[256], buf[1024]; + int temp, rc, deviation, keepalive = 60; +#ifdef HAVE_WIRINGPI_H + int lcdupdate; +#endif + unsigned int max_inflight = 20; + char err[1024]; + w1_therm *tmp1, *old1; + char *device, *alias, line[60], *p = NULL; + FILE *fp; + + /* + * Initialize mosquitto communication + */ + mosquitto_lib_init(); + + /* + * Build MQTT id + */ + hostname[0] = '\0'; + gethostname(hostname, 256); + hostname[255] = '\0'; + + id = xstrcpy((char *)"thermometers/"); + id = xstrcat(id, hostname); + if(strlen(id) > MOSQ_MQTT_ID_MAX_LENGTH) { + /* + * Enforce maximum client id length of 23 characters + */ + id[MOSQ_MQTT_ID_MAX_LENGTH] = '\0'; + } + + mosq = mosquitto_new(id, true, NULL); + if(!mosq) { + switch(errno) { + case ENOMEM: + syslog(LOG_NOTICE, "mosquitto_new: Out of memory"); + break; + case EINVAL: + syslog(LOG_NOTICE, "mosquitto_new: Invalid id"); + break; + } + mosquitto_lib_cleanup(); + return 1; + } + + if (debug) { + mosquitto_log_callback_set(mosq, my_log_callback); + } + + /* + * Set our will + */ + state = xstrcpy((char *)"clients/"); + state = xstrcat(state, hostname); + state = xstrcat(state, (char *)"/thermometers/state"); + sprintf(buf, "0"); + if ((rc = mosquitto_will_set(mosq, state, strlen(buf), buf, qos, true))) { + if (rc == MOSQ_ERR_INVAL) { + syslog(LOG_NOTICE, "mosquitto_will_set: input parameters invalid"); + } else if (rc == MOSQ_ERR_NOMEM) { + syslog(LOG_NOTICE, "mosquitto_will_set: Out of Memory"); + } else if (rc == MOSQ_ERR_PAYLOAD_SIZE) { + syslog(LOG_NOTICE, "mosquitto_will_set: invalid payload size"); + } + mosquitto_lib_cleanup(); + return rc; + } + + mosquitto_max_inflight_messages_set(mosq, max_inflight); + mosquitto_connect_callback_set(mosq, my_connect_callback); + mosquitto_disconnect_callback_set(mosq, my_disconnect_callback); + mosquitto_publish_callback_set(mosq, my_publish_callback); + + if ((rc = mosquitto_connect(mosq, Config.mosq_host, Config.mosq_port, keepalive))) { + if (rc == MOSQ_ERR_ERRNO) { + strerror_r(errno, err, 1024); + syslog(LOG_NOTICE, "mosquitto_connect: error: %s", err); + } else { + syslog(LOG_NOTICE, "mosquitto_connect: unable to connect (%d)", rc); + } + mosquitto_lib_cleanup(); + return rc; + } + syslog(LOG_NOTICE, "Connected with %s:%d", Config.mosq_host, Config.mosq_port); + + /* + * Initialise is complete, report our presence state + */ + mosquitto_loop_start(mosq); + sprintf(buf, "1"); + rc = mosquitto_publish(mosq, &mid_sent, state, strlen(buf), buf, qos, 1); +#ifdef HAVE_WIRINGPI_H +// setBacklight(0); +#endif + + /* + * Report alias names + */ + for (tmp1 = Config.w1therms; tmp1; tmp1 = old1) { + old1 = tmp1->next; + + alias = xstrcpy((char *)"/raw/"); + alias = xstrcat(alias, hostname); + alias = xstrcat(alias, (char *)"/thermometers/w1/"); + alias = xstrcat(alias, tmp1->master); + alias = xstrcat(alias, (char *)"/"); + alias = xstrcat(alias, tmp1->name); + alias = xstrcat(alias, (char *)"/alias"); + + sprintf(buf, "%s", tmp1->alias); + if ((rc = mosquitto_publish(mosq, &mid_sent, alias, strlen(buf), buf, qos, 1))) { + if (rc == MOSQ_ERR_NO_CONN) + mosquitto_reconnect(mosq); + else + syslog(LOG_NOTICE, "mainloop: error %d from mosquitto_publish", rc); + } + + free(alias); + alias = NULL; + } + + if (debug) + fprintf(stdout, (char *)"Enter loop, connected %d\n", connected); + + do { + if (status == STATUS_CONNACK_RECVD) { +#ifdef HAVE_WIRINGPI_H + lcdupdate = FALSE; +#endif + + /* + * Here send our 1-wire sensors values + */ + for (tmp1 = Config.w1therms; tmp1; tmp1 = old1) { + old1 = tmp1->next; + + /* + * Build path and alias topic + */ + device = xstrcpy((char *)"/sys/bus/w1/devices/"); + device = xstrcat(device, tmp1->master); + device = xstrcat(device, (char *)"/"); + device = xstrcat(device, tmp1->name); + device = xstrcat(device, (char *)"/w1_slave"); + alias = xstrcpy((char *)"/raw/"); + alias = xstrcat(alias, hostname); + alias = xstrcat(alias, (char *)"/thermometers/w1/"); + alias = xstrcat(alias, tmp1->master); + alias = xstrcat(alias, (char *)"/"); + alias = xstrcat(alias, tmp1->name); + alias = xstrcat(alias, (char *)"/temperature"); + + /* + * Read sensor data + */ + if ((fp = fopen(device, "r"))) { + /* + * The output looks like: + * 72 01 4b 46 7f ff 0e 10 57 : crc=57 YES + * 72 01 4b 46 7f ff 0e 10 57 t=23125 + */ + fgets(line, 50, fp); + line[strlen(line)-1] = '\0'; + if ((line[36] == 'Y') && (line[37] == 'E')) { + /* + * CRC is Ok, continue + */ + fgets(line, 50, fp); + line[strlen(line)-1] = '\0'; + strtok(line, (char *)"="); + p = strtok(NULL, (char *)"="); + rc = sscanf(p, "%d", &temp); + if ((rc == 1) && (tmp1->lastval != temp)) { + /* + * It is possible to have read errors or extreme values. + * This can happen with bad connections so we compare the + * value with the previous one. If the difference is too + * much, we don't send that value. That also means that if + * the next value is ok again, it will be marked invalid too. + * Maximum error is 20 degrees for now. + */ + deviation = 20000; + if ((tmp1->lastval == 0) || + (tmp1->lastval && (temp > (tmp1->lastval - deviation)) && (temp < (tmp1->lastval + deviation)))) { + /* + * Temperature is changed and valid, update and publish this. + */ + sprintf(buf, "%.1f", temp / 1000.0); + if ((rc = mosquitto_publish(mosq, &mid_sent, alias, strlen(buf), buf, qos, 1))) { + if (rc == MOSQ_ERR_NO_CONN) + mosquitto_reconnect(mosq); + else + syslog(LOG_NOTICE, "mainloop: error %d from mosquitto_publish", rc); + } + } else { + syslog(LOG_NOTICE, "deviation error deviation=%d, old=%d new=%d", deviation, tmp1->lastval, temp); + if (debug) { + fprintf(stdout, "deviation error deviation=%d, old=%d new=%d\n", deviation, tmp1->lastval, temp); + } + } + tmp1->lastval = temp; +#ifdef HAVE_WIRINGPI_H + lcdupdate = TRUE; +#endif + } + } else { + syslog(LOG_NOTICE, "sensor %s/%s CRC error", tmp1->master, tmp1->name); + } + fclose(fp); + tmp1->present = 1; + } else { + tmp1->present = 0; + if (debug) + printf("sensor %s is missing\n", tmp1->name); + } + + free(device); + device = NULL; + free(alias); + alias = NULL; + } + +#ifdef HAVE_WIRINGPI_H + if (lcdupdate) { + lcdPosition(lcdHandle, 0, 0); + tmp1 = Config.w1therms; + snprintf(buf, 16, "%5.1f %cC %s ", tmp1->lastval / 1000.0, 0xdf, tmp1->alias); + lcdPuts(lcdHandle, buf); + old1 = tmp1->next; + tmp1 = old1; + lcdPosition(lcdHandle, 0, 1); + snprintf(buf, 16, "%5.1f %cC %s ", tmp1->lastval / 1000.0, 0xdf, tmp1->alias); + lcdPuts(lcdHandle, buf); + } +#endif + + if (my_shutdown) { + /* + * Final publish 0 to clients/<hostname>/thermometers/state + */ + sprintf(buf, "0"); + mosquitto_publish(mosq, &mid_sent, state, strlen(buf), buf, qos, true); + last_mid = mid_sent; + status = STATUS_WAITING; +#ifdef HAVE_WIRINGPI_H + lcdClear(lcdHandle); + lcdPosition(lcdHandle, 0, 0); + lcdPuts(lcdHandle, "Shuting down ..."); +#endif + } + + usleep(100000); + + } else if (status == STATUS_WAITING) { + if (debug) + fprintf(stdout, (char *)"Waiting\n"); + if (last_mid_sent == last_mid && disconnect_sent == false) { + mosquitto_disconnect(mosq); + disconnect_sent = true; + } + usleep(100000); + } + rc = MOSQ_ERR_SUCCESS; + + } while (rc == MOSQ_ERR_SUCCESS && connected); + + if (debug) + fprintf(stdout, (char *)"Out of loop\n"); + + mosquitto_loop_stop(mosq, false); + mosquitto_destroy(mosq); + mosquitto_lib_cleanup(); + +#ifdef HAVE_WIRINGPI_H + stopLCD(); +#endif + + return rc; +} +