view wh1080.c @ 7:9a14029b3782

- Wait longer (100msec) for a read, and reduce the number of retries to match. - Print out if we read OK after more than 1 attempt for diagnostic purposes.
author Daniel O'Connor <darius@dons.net.au>
date Thu, 11 Feb 2010 12:47:59 +1030
parents b22a888eb975
children e3d2b5500b53
line wrap: on
line source

/*
 * 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 <stdlib.h>

#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)
{
#define MAXTRIES 10
  int bytes_read, i = 0;

  /* 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,
                                          100 )) == 8 ){
      if (i > 0)
        fprintf(stderr, "Read OK after %d tries\n", i + 1);
      return bytes_read;
    }
    fprintf(stderr, "Retrying..\n");

    usleep (10000);
    i++;
  }
  while (errno != EINTR && i < MAXTRIES);
  if (i == MAXTRIES) {
    fprintf(stderr,
	    "Can't read from device after %d attempts\n", MAXTRIES);
  } else
    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 i, bytes_read, tries = 0;
  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
  rerequest:
  bytes_read = 0;
  tries++;
  if (tries > 10) {
    fprintf(stderr, "Unable to read page after 10 tries\n");
    exit(1);
  }
  
  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) {
    if ((i = read_station ((char *) &buf [bytes_read])) == 0) {
      fprintf(stderr, "Couldn't read from the device, rerequesting\n");
      goto rerequest;
    }
    
    bytes_read += i;
  }
  
  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);
}

/*
 * 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 0
  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)
#endif
}

/*
 * 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;
}


/*
 *;;; Local Variables: ***
 *;;; c-basic-offset:2 ***
 *;;; End: ***
 */