Mercurial > ~darius > hgwebdir.cgi > wh1080
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 *) ¤t_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 (¤t_data [0]); + reend_readings (¤t_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, ¤t_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, ¤t_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, ¤t_readings); + reading_time += current_readings.last_save_mins * 60; /* time this record was finished */ + current_readings.timestamp = reading_time; + insert_db_row (¤t_readings); + if (verbose) + print_readings (¤t_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 (¤t_readings); + if (verbose) + print_readings (¤t_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; +}