diff wh1080.c @ 0:9dab44dcb331

Initial commit of Greg's code from http://www.lemis.com/grog/tmp/wh1080.tar.gz
author Daniel O'Connor <darius@dons.net.au>
date Tue, 09 Feb 2010 13:44:25 +1030
parents
children 9da35e705144
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/wh1080.c	Tue Feb 09 13:44:25 2010 +1030
@@ -0,0 +1,630 @@
+/*
+ * Data input utility for Fine Offset WH-1080 weather station.
+ * This bases on code supplied to me by Steve Woodford.
+ *
+ * Greg Lehey, 12 November 2009
+ *
+ * $Id: wh1080.c,v 1.18 2010/02/07 03:40:37 grog Exp $
+ */
+
+#include "wh1080.h"
+struct usb_device *station_device;
+usb_dev_handle *station;
+
+/* Data from the device */
+struct wh1080_page0 page0;
+struct wh1080_page1 page1;
+struct wh1080_readings current_data [2];        /* current data readings, from station */
+
+struct readings current_readings;               /* current data readings, our version */
+struct readings previous_readings;              /* copy of previous set of readings */
+
+char previous_wind_direction_text [4];          /* reuse previous direction if we have no wind */
+
+int previous_rain;                              /* previous rainfall reading: we need a difference */
+float db_previous_rain;                         /* previous rainfall reading for database */
+
+struct libusb_context **usb_context;
+
+#if BYTE_ORDER == BIG_ENDIAN
+/*
+ * The station sends data in big-endian format.  If we're running on a
+ * big-endian machine, we need to turn the 16 bit fields around.
+ */
+
+void reend_page0 ()
+{
+  page0.current_page = le16toh (page0.current_page);
+}
+
+void reend_page1 ()
+{
+  page1.rel_pressure = le16toh (page1.rel_pressure);
+  page1.abs_pressure = le16toh (page1.abs_pressure);
+}
+
+void reend_readings (struct wh1080_readings *page)
+{
+  page->inside_temp = le16toh (page->inside_temp); /* inside temperature */
+  page->outside_temp = le16toh (page->outside_temp); /* outside temperature */
+  page->pressure = le16toh (page->pressure);    /* absolute pressure, hPa */
+  page->rain = le16toh (page->rain);            /* rainfall in 0.3 mm units */
+}
+#endif
+
+/*
+ * Scan USB busses for our device.
+ * Return 1 and device information to devp if found.
+ */
+int find_usb_device (int vendor, int product, struct usb_device **devp)
+{
+  struct usb_bus *bus;
+  int count;
+
+  count = usb_find_busses ();
+  count = usb_find_devices ();
+
+  for (bus = usb_get_busses (); bus; bus = bus->next)
+  {
+    for (*devp = bus->devices; *devp; *devp = (*devp)->next)
+    {
+      if ((*devp)->descriptor.idVendor == vendor
+          && (*devp)->descriptor.idProduct == product )
+        return 1;
+    }
+  }
+  return 0;                                     /* not found */
+}
+
+/*
+ * Set up communications with the weather station.
+ * On error, print message and die.  Return means success.
+ */
+void device_setup ()
+{
+  usb_init ();                                  /* initialize libusb */
+
+  if (! find_usb_device (WH1080_USB_VENDOR, WH1080_USB_PRODUCT, &station_device))
+  {
+    fprintf (stderr, "Can't find WH-1080 device\n");
+    exit (1);
+  }
+
+  /* Open station */
+  if (! (station = usb_open (station_device)))
+  {
+    fprintf (stderr,
+             "Can't open weather station: %s (%d)\n",
+             usb_strerror (),
+             errno);
+    exit (1);
+  }
+
+#ifdef linux
+  /*
+   * For some reason, Linux gives a device to the kernel, so we need
+   * to prise it away again.
+   */
+#if 0
+  http://blemings.org/hugh/blog/blosxom.cgi/2008/01/15#20080115a
+      struct usb_bus          *bus_list;
+    struct usb_device       *dev = NULL;
+    struct usb_dev_handle   *handle;
+
+
+        /* Look for the first u4xx device we can find then try and open */
+    if((dev = find_u4xx(bus_list)) == NULL) {
+        return NULL;
+    }
+
+        /* Try and get a handle to the device */
+    if((handle = usb_open(dev)) == NULL) {
+        return NULL;
+    }
+
+        /* The kernel's HID driver will seize the USBMicro device as it
+           says it's a HID device - we need to tell the kernel to
+           let go of it */
+    if (usb_detach_kernel_driver_np(handle, 0) < 0) {
+          /* If this fails, usually just means that no kernel driver
+             had attached itself to the device so just ignore/warn */
+    }
+
+        /* Set the configuration */
+    if(usb_set_configuration(handle, 1) != 0) {
+        usb_close(handle);
+        return NULL;
+    }
+
+        /* Clain interface - gather would need to this for each
+           interface if the device has more than one */
+    if (usb_claim_interface(handle, 0) != 0) {
+        usb_close(handle);
+        return NULL;
+    }
+
+    /* etc. etc. */
+
+#endif
+  if (usb_detach_kernel_driver_np (station, 0) < 0)
+  {
+    fprintf (stderr, "%s (%d)\n", usb_strerror (), errno);
+/*    exit (1); */
+  }
+#endif
+
+  /* And grab the interface */
+  if (usb_claim_interface (station, 0) < 0)
+  {
+    fprintf (stderr, "%s (%d)\n", usb_strerror (), errno);
+    exit (1);
+  }
+}
+
+/*
+ * Try to recover from USB breakage.
+ * XXX This doesn't work in the current form.
+ */
+void device_reset ()
+{
+  if (usb_release_interface (station, 0) < 0)
+    fprintf (stderr,
+             "Can't release interface: %s (%d)\n",
+             usb_strerror (),
+             errno);
+#if 0
+  if (usb_close (station) < 0)
+    fprintf (stderr,
+             "Can't close interface: %s (%d)\n",
+             usb_strerror (),
+             errno);
+  device_setup ();
+#endif
+}
+
+
+/*
+ * Write control data to device.  We don't write real data.
+ */
+int write_station_control (char *buf)
+{
+  int written;
+  char textdate [64];
+
+  /* XXX Find out what all this means, and be cleverer about retries */
+  do
+  {
+    if (written = (usb_control_msg (station,
+                                    USB_TYPE_CLASS + USB_RECIP_INTERFACE,
+                                    0x9,
+                                    0x200,
+                                    0,
+                                    buf,
+                                    8,          /* we always write 8 bytes */
+                                    WH1080_WRITE_TIMEOUT) > 0))
+      return written;
+#if 0
+    if (errno == ENOTTY)
+    {
+      fprintf (stderr, "USB bus stuck, reinitializing\n");
+      device_reset ();
+    }
+#endif
+  }
+  while (errno == EINTR);                       /*  || (errno == ENOTTY)); */
+
+  datetext (time (NULL), textdate, "%e %B %Y  %T");
+  fprintf (stderr,
+           "%s: PID %d: can't write to device: %s (%d)\n",
+           textdate,
+           getpid (),
+           usb_strerror (),
+           errno );
+  return 0;
+}
+
+/*
+ * Read 8 bytes from the device.
+ */
+int read_station (char *buf)
+{
+  int bytes_read;
+
+  /* XXX Find out what all this means, and be cleverer about retries */
+  do
+  {
+    if ((bytes_read = usb_interrupt_read (station,
+                                          USB_ENDPOINT_IN | USB_RECIP_INTERFACE,
+                                          buf,
+                                          8,
+                                          10 )) == 8 )
+      return bytes_read;
+    if (errno == EAGAIN)
+      usleep (10000);
+  }
+  while (errno != EINTR);
+  fprintf (stderr,
+           "Can't read device: %s (%d)\n",
+           usb_strerror (),
+           errno );
+  return 0;
+}
+
+/* Read a page (32 bytes) from the device. */
+int read_station_page (uint16_t page, char *buf)
+{
+  int bytes_read = 0;                           /* keep track of input */
+  struct read_page_request
+  {
+    char command;                               /* XXX I'm guessing at this */
+    uint16_t address;
+    char length;
+  } __attribute__ ((packed)) request [2];
+
+#if BYTE_ORDER == LITTLE_ENDIAN
+  page = htobe16 (page);                        /* in big-endian for the command */
+#endif
+  request [0] = (struct read_page_request) {0xa1, page, 32};
+  request [1] = request [0];
+  if (write_station_control ((char *) request) == 0)
+    exit (1);                                   /* XXX we've already complained */
+  while (bytes_read < 32)
+    bytes_read += read_station ((char *) &buf [bytes_read]);
+  return bytes_read;
+}
+
+/*
+ * Read and compare page from station.  Keep trying until we get two that are
+ * the same.
+ *
+ * XXX don't loop for ever.
+ */
+int read_valid_station_page (uint16_t page, char *buf)
+{
+  char duplicate [WH1080_PAGE_SIZE];
+
+  read_station_page (page, duplicate);          /* read once for comparison */
+  while (1)
+  {
+    read_station_page (page, buf);              /* and once where we want it */
+    if (! memcmp (buf, duplicate, WH1080_PAGE_SIZE)) /* bingo! */
+      return WH1080_PAGE_SIZE;
+    memcpy (duplicate, buf, WH1080_PAGE_SIZE);
+  }
+}
+
+/* Read observations from specified page.
+ *
+ * We have to read 32 bytes from the device, but the data we want is only 16
+ * bytes.  If it's the last 16 bytes in memory, who knows what will happen?  To
+ * be on the safe side, always read from an even-numbered page and then return 0
+ * or 1 to point to the correct entry in (global) current_data.
+ */
+void read_readings (int page, struct readings *readings)
+{
+  int pagehalf = (page & 0x10) >> 4;            /* index in our two entries */
+
+#if 0
+  /* Page 1: pressure readings, currently unused */
+  read_valid_station_page (WH1080_PAGE1, (char *) &page1);
+#if BYTE_ORDER == BIG_ENDIAN
+/*  reend_page0 (); */
+  reend_page1 ();
+#endif
+#endif
+
+  /* Read the data itself */
+  read_valid_station_page (page & WH1080_PAGE_MASK, (char *) &current_data);
+
+#if 0
+  printf ("\n");
+  hexdump ((unsigned char *) current_data, 2 * sizeof (struct wh1080_readings));
+#endif
+#if BYTE_ORDER == BIG_ENDIAN
+  /* Turn both around to avoid surprises */
+  reend_readings (&current_data [0]);
+  reend_readings (&current_data [1]);
+#endif
+
+  /*
+   * Now copy the info back to readings.  Note that we don't set the timestamp
+   * here; only the caller knows when this reading was made.
+   */
+  readings->page = page;                        /* save page number too */
+  readings->last_save_mins
+    = current_data [pagehalf].last_save_mins;   /* last save minutes (?) */
+  readings->inside_humidity
+    = current_data [pagehalf].inside_humidity;  /* humidity in percent */
+  /* This appears to be an "out of range" situation. */
+  if (readings->inside_humidity == 255)
+    readings->inside_humidity = previous_readings.inside_humidity;
+  readings->inside_temp
+    = ((float) current_data [pagehalf].inside_temp) / 10; /* inside temperature */
+  readings->outside_humidity
+    = current_data [pagehalf].outside_humidity; /* humidity in percent */
+  if (readings->outside_humidity == 255)
+    readings->outside_humidity = previous_readings.outside_humidity;
+  readings->outside_temp
+    = ((float) current_data [pagehalf].outside_temp) / 10; /* outside temperature */
+  readings->pressure
+    = ((float) current_data [pagehalf].pressure) / 10 /* absolute pressure, hPa */
+      - config.pressure_error;                  /* adjust for inaccuracies */
+  readings->wind_speed
+    = ((float) current_data [pagehalf].wind_speed) / 3.6; /* wind speed in km/h */
+  readings->wind_gust
+    = ((float) current_data [pagehalf].wind_gust) / 3.6; /* wind gust speed in km/h */
+  /*
+   * Wind direction is confusing.  It's normally a value between 0 and 15, but
+   * it's 0x80 if there's no wind at all.  Following an idea by Steve Woodford,
+   * reuse the previous value if it's 0x80.
+   *
+   * Other values shouldn't happen.  If they do, report them in numeric form.
+   *
+   * In addition to the text, we save the original value in degrees for
+   * Wunderground and friends.  If it's invalid, we simply don't send it.
+   */
+  if (current_data [pagehalf].wind_direction == 0x80) /* no wind */
+  {
+    strcpy (readings->wind_direction_text, previous_wind_direction_text);
+    readings->wind_direction = INVALID_DIRECTION;
+  }
+  else if (current_data [pagehalf].wind_direction < 16) /* valid direction */
+  {
+    strcpy (previous_wind_direction_text, wind_directions [current_data [pagehalf].wind_direction]);
+    strcpy (readings->wind_direction_text, wind_directions [current_data [pagehalf].wind_direction]);
+    readings->wind_direction = ((float) current_data [pagehalf].wind_direction) * 22.5;
+  }
+  else                                          /* invalid value */
+  {
+    sprintf (readings->wind_direction_text,
+             "%3d",
+             current_data [pagehalf].wind_direction); /* just put in the number */
+    readings->wind_direction = INVALID_DIRECTION;
+  }
+
+  /* Count incremental rainfall in readings->rain */
+  if (current_data [pagehalf].rain != previous_rain)
+  {
+    int temprain = current_data [pagehalf].rain - previous_rain;
+
+    if (temprain < 0)                           /* wraparound */
+      temprain += USHRT_MAX;                    /* add 65536 */
+    readings->rain += ((float) temprain) * 0.3; /* rainfall in mm */
+    previous_rain = current_data [pagehalf].rain;
+  }
+
+  set_dewpoints (readings);
+  set_sea_level_pressure (readings);
+
+  /* Stuff from page 1 */
+  readings->page1_abs_pressure = ((float) page1.abs_pressure) / 10; /* absolute pressure, hPa */
+}
+
+/*
+ * Read page 0.
+ */
+
+void read_page0 ()
+{
+  /*
+   * Page 0: magic and offset of current page
+   */
+  do
+    read_valid_station_page (WH1080_PAGE0, (char *) &page0);
+  while (((page0.magic != WH1080_PAGE0_MAGIC1A)
+          && (page0.magic != WH1080_PAGE0_MAGIC1B) )
+         || (page0.magic2 != WH1080_PAGE0_MAGIC2) );
+}
+
+/*
+ * Get a set of readings from the station.
+ */
+void read_station_data ()
+{
+  read_page0 ();                                /* get current page number from page0 */
+  read_readings (page0.current_page, &current_readings);
+  current_readings.timestamp = time (NULL);
+}
+
+void dump_memory ()
+{
+  int page;
+  struct readings readings;
+  time_t reading_time;                          /* calculate time of the reading */
+  struct tm *reading_tm;                        /* for trimming off seconds */
+
+  /*
+   * Calculate the time of the most recent archive reading, which is
+   * current_readings.last_save_mins old.  This is confused by the fact that the
+   * times don't include seconds, so for some semblence of uniformity, assume
+   * that we're in the middle of the minute.
+   */
+  reading_time = time (NULL);                   /* now */
+  reading_tm = localtime (&reading_time);       /* and in struct tm format */
+  reading_time -= reading_tm->tm_sec;           /* adjust to beginning of the minute */
+  /* XXX decide how to round */
+
+  /*
+   * Note that there's a race condition in the end condition.
+   * page0.current_page could increment during dumping.  That's perfectly
+   * acceptable.
+   */
+  for (page = page0.current_page + WH1080_ARCHIVE_RECORD_SIZE;
+       page != page0.current_page;
+       page += WH1080_ARCHIVE_RECORD_SIZE)
+  {
+    if (page >= 0xfff0)                         /* wrap around */
+      page = WH1080_FIRST_ARCHIVE;              /* back to last one in memory */
+    read_readings (page, &readings);
+#if 0
+    if (readings.last_save_mins != 30)          /* this seems to always be the value in archive readings  */
+      return;                                   /* this would be the beginning of time? */
+#endif
+    readings.timestamp = reading_time;
+    print_readings (&readings);
+    reading_time += WH1080_ARCHIVE_INTERVAL;
+  }
+}
+
+void insert_db_row (struct readings *readings)
+{
+  char reading_date [STAMPSIZE];                /* formatted date */
+  char reading_time [STAMPSIZE];                /* and time */
+
+  strftime (reading_date, STAMPSIZE, "%F", localtime (&readings->timestamp)); /* format time and date */
+  strftime (reading_time, STAMPSIZE, "%T", localtime (&readings->timestamp)); /* format time and date */
+
+  sprintf (mysql_querytext,
+           "INSERT INTO %s\n"
+           "  (station_id, date, time, inside_humidity, inside_temp, inside_dewpoint,\n"
+           "  outside_humidity, outside_temp, outside_dewpoint, pressure_abs, pressure_msl,\n"
+           "  wind_speed, wind_gust, wind_direction, wind_direction_text, rain)\n"
+           "VALUES\n"
+           "  (\"%s\", \"%s\", \"%s\", %d, %6.1f, %6.1f, "
+           "  %d, %6.1f, %6.1f, %6.1f, %6.1f, "
+           "  %6.1f, %6.1f, %6.1f, \"%s\", %6.1f);",
+           config.db_table,
+           config.station_id,
+           reading_date,
+           reading_time,
+           readings->inside_humidity,
+           readings->inside_temp,
+           readings->inside_dewpoint,
+           readings->outside_humidity,
+           readings->outside_temp,
+           readings->outside_dewpoint,
+           readings->pressure,
+           readings->pressure_sea_level,
+           readings->wind_speed,
+           readings->wind_gust,
+           readings->wind_direction,
+           readings->wind_direction_text,
+           readings->rain - db_previous_rain );
+  db_previous_rain = readings->rain;            /* update our current rainfall XXX fix this */
+  if (update)                                   /* only if updates set */
+  {
+    if (mysql_query (mysql, mysql_querytext))
+    {
+      fprintf (stderr,
+               "Can't insert database record: %s (%d)\n",
+               mysql_error (mysql),
+               mysql_errno (mysql) );
+    }
+  }
+  else if (verbose)
+    puts (mysql_querytext);
+}
+
+/*
+ * Check if we have missed any updates since we last ran.
+ *
+ * Not so affectionately named after Powercor (http://www.powercor.com.au/),
+ * whose continual power outages are the main reason that this function is
+ * needed.
+ *
+ * XXX check this for relationship to share file.
+ */
+void recover_powercor_breakage ()
+{
+  int page;
+  time_t reading_time;
+
+  read_page0 ();
+  if (current_readings.page != page0.current_page) /* we've moved on */
+  {
+    /* Go back to the previous record to read rain */
+    if ((page = current_readings.page - WH1080_ARCHIVE_RECORD_SIZE)
+        < WH1080_FIRST_ARCHIVE)                 /* wrap around */
+      page = WH1080_LAST_ARCHIVE;
+    reading_time = current_readings.timestamp   /* time of this reading */
+      - (current_readings.last_save_mins * 60);
+    read_readings (page, &current_readings);
+    do
+    {
+      /* Next page, with wraparound */
+      page += WH1080_ARCHIVE_RECORD_SIZE;
+      if (page > WH1080_LAST_ARCHIVE)
+        page = WH1080_FIRST_ARCHIVE;
+      /*
+       * The archive records appear to be every 30 minutes, but since the
+       * duration is stored in them, there's no reason not to calculate the
+       * time the record was completed.
+       */
+      read_readings (page, &current_readings);
+      reading_time += current_readings.last_save_mins * 60; /* time this record was finished */
+      current_readings.timestamp = reading_time;
+      insert_db_row (&current_readings);
+      if (verbose)
+        print_readings (&current_readings);
+    }
+    while (page != page0.current_page);
+  }
+}
+
+void usage (char *me)
+{
+  fprintf (stderr,
+           "Usage: %s [-d] [-n] [-v] [station ID] [db user] [password] [db host] [database]\n",
+           me);
+  exit (1);
+}
+
+
+int main (int argc, char *argv [])
+{
+  read_config (argc, argv);
+
+  device_setup ();                              /* initialize the device */
+  if (recover)
+    recover_powercor_breakage ();               /* check for missed updates */
+  read_station_data ();                         /* at least to set the rainfall counter */
+
+  /*
+   * Rainfall is a pain.  We have several ways of reporting it:
+   *
+   * Print out from this program.
+   * Print out in various forms from report.
+   * Insert into database.
+   * Inform Wunderground.
+   *
+   * Each of these can be done asynchronously, so we maintain cumulative values of
+   * current rainfall and a rainfall value for each of these reporting methods.
+   * They are:
+   *
+   * Print out from this program (really util.c)
+   *    current_readings.rain - print_previous_rain
+   * Inform Wunderground.
+   *    current_readings.rain - current_readings.previous_rain
+   * Print out from report
+   *     This is done from util.c as well, so we need to set print_previous_rain
+   *     to current_readings.previous_rain
+   * Insert into database.
+   *     Done from wh1080,
+   *    current_readings.rain - db_previous_rain
+   */
+  current_readings.rain = 0.0;                  /* And current rainfall */
+  current_readings.previous_rain = 0.0;         /* Initialize previous rainfall for report */
+  print_previous_rain = 0.0;
+  db_previous_rain = 0.0;
+
+  if (debug)
+  {
+    dump_memory ();                             /* show old data */
+    exit (0);
+  }
+
+  while (1)
+  {
+    read_station_data ();
+    insert_db_row (&current_readings);
+    if (verbose)
+      print_readings (&current_readings);
+
+    /* XXX calculate the exact time to wait, which will be marginally smaller */
+    sleep (config.poll_interval);
+    previous_readings = current_readings;       /* make a copy of the current readings */
+  }
+  /* If we ever get here, this is what we should do */
+  usb_release_interface (station, 0);
+  return 0;
+}