From ac2f8dcdcc708830a47b3fd69e8d49ba0231c6c6 Mon Sep 17 00:00:00 2001 From: Matthew Wall Date: Sat, 9 Feb 2013 00:07:26 +0000 Subject: [PATCH] added adaptive polling mode and pressure check --- bin/wee_config_fousb.py | 32 ++++- bin/weewx/fousb.py | 257 ++++++++++++++++++++++++++++++++++------ 2 files changed, 246 insertions(+), 43 deletions(-) diff --git a/bin/wee_config_fousb.py b/bin/wee_config_fousb.py index 2aa1e5f2..9cdcd2fa 100755 --- a/bin/wee_config_fousb.py +++ b/bin/wee_config_fousb.py @@ -1,5 +1,5 @@ #!/usr/bin/python -# $Id: config_fousb.py 451 2013-02-07 22:39:43Z mwall $ +# $Id: config_fousb.py 352 2013-01-04 15:48:17Z mwall $ # # Copyright 2012 Matthew Wall # @@ -45,7 +45,7 @@ Output from a 308x-series station would be particularly helpful. # TODO: # set station archive interval -usage="""%prog: config_file [--help] [--info] [--debug]""" +usage="""%prog: config_file [--help | --info | --check-pressures] [--debug]""" epilog = """Mutating actions will request confirmation before proceeding.""" @@ -57,8 +57,10 @@ def main(): # Add the various options: parser.add_option("--info", action="store_true", dest="info", help="display weather station configuration") + parser.add_option("--check-pressures", action="store_true", dest="chkpres", + help="query station for pressure sensor data") parser.add_option("--debug", action="store_true", dest="debug", - help="display all bits, not just known bits") + help="display additional information while running") # Now we are ready to parse the command line: (options, args) = parser.parse_args() @@ -92,7 +94,9 @@ def main(): station = weewx.fousb.FineOffsetUSB(altitude=altitude_m, **config_dict['FineOffsetUSB']) - if options.info or len(args) == 1: + if options.chkpres: + checkpressures(station) + elif options.info or len(args) == 1: info(station) def info(station): @@ -122,6 +126,26 @@ def info(station): for s in slist[k]: print s +def checkpressures(station): + """Query the station then display sensor readings.""" + print "Querying the station..." + val = getvalues(station, '', weewx.fousb.fixed_format) + station.closePort() + + for packet in station.genLoopPackets(): + print packet + rp = station.get_fixed_block(['rel_pressure']) + sp = packet['pressure'] + ap1 = weewx.wxformulas.altimeter_pressure_Metric(sp, station.altitude) + ap2 = weewx.fousb.sp2ap(sp, station.altitude) + bp2 = weewx.fousb.sp2bp(sp, station.altitude, packet['outTemp']) + print 'relative pressure: %s' % rp + print 'station pressure: %s' % sp + print 'altimeter pressure (davis): %s' % ap1 + print 'altimeter pressure (noaa): %s' % ap2 + print 'barometer pressure (weewx): ?' + print 'barometer pressure (wview): %s' % bp2 + def stash(slist, s): if s.find('settings') != -1: slist['settings'].append(s) diff --git a/bin/weewx/fousb.py b/bin/weewx/fousb.py index 1c5a6e9f..3acec056 100644 --- a/bin/weewx/fousb.py +++ b/bin/weewx/fousb.py @@ -1,5 +1,5 @@ # FineOffset module for weewx -# $Id: fousb.py 449 2013-02-07 17:15:41Z mwall $ +# $Id: fousb.py 456 2013-02-09 00:04:50Z mwall $ # # Copyright 2012 Matthew Wall # @@ -147,6 +147,7 @@ It is used as: A200 1A20 A2AA 0020 to indicate a data refresh. The WH1080 acknowledges the write with an 8 byte chunk: A5A5 A5A5. """ +from datetime import datetime import math import time import syslog @@ -335,15 +336,19 @@ def logdbg(msg): def loginf(msg): syslog.syslog(syslog.LOG_INFO, 'fousb: %s' % msg) +def logerr(msg): + syslog.syslog(syslog.LOG_ERR, 'fousb: %s' % msg) + def logcrt(msg): syslog.syslog(syslog.LOG_CRIT, 'fousb: %s' % msg) -def logerr(msg): - syslog.syslog(syslog.LOG_ERR, 'fousb: %s' % msg) +# noaa definitions for station pressure, altimeter setting, and sea level +# http://www.crh.noaa.gov/bou/awebphp/definitions_pressure.php # implementation copied from wview def sp2ap(sp_mbar, elev_meter): """Convert station pressure to sea level pressure. + http://www.wrh.noaa.gov/slc/projects/wxcalc/formulas/altimeterSetting.pdf sp_mbar - station pressure in millibars @@ -399,6 +404,10 @@ class BlockLengthError(Exception): def __str__(self): return self.msg +# mechanisms for polling the station +REGULAR_POLLING = 'REGULAR' +ADAPTIVE_POLLING = 'ADAPTIVE' + class FineOffsetUSB(weewx.abstractstation.AbstractStation): """Driver for FineOffset USB stations.""" @@ -406,11 +415,14 @@ class FineOffsetUSB(weewx.abstractstation.AbstractStation): """Initialize the station object. altitude: Altitude of the station - [Required. Obtained from weewx configuration.] + [Required. No Default.] model: Which station model is this? [Optional. Default is 'WH1080 (USB)'] + polling_mode: The mechanism to use when polling the station. + [Optional. Default is 'REGULAR'] + rain_max_sane: Maximum sane value for rain in a single sampling period, measured in mm [Optional. Default is 2] @@ -450,6 +462,7 @@ class FineOffsetUSB(weewx.abstractstation.AbstractStation): self.altitude = stn_dict['altitude'] self.record_generation = stn_dict.get('record_generation', 'software') self.model = stn_dict.get('model', 'WH1080 (USB)') + self.polling_mode = stn_dict.get('polling_mode', REGULAR_POLLING) self.rain_max_sane = int(stn_dict.get('rain_max_sane', 2)) self.timeout = float(stn_dict.get('timeout', 15.0)) self.wait_before_retry = float(stn_dict.get('wait_before_retry', 5.0)) @@ -462,16 +475,26 @@ class FineOffsetUSB(weewx.abstractstation.AbstractStation): self.usb_read_size = int(stn_dict.get('usb_read_size', '0x20'), 0) self.data_format = stn_dict.get('data_format', '1080') + # avoid USB activity this many seconds each side of the time when + # console is believed to be writing to memory. + self.avoid = 3.0 + # minimum interval between polling for data change + self.min_pause = 0.5 + self._rain_period_ts = None self._last_rain = None self._fixed_block = None self._data_block = None self._data_pos = None self._current_ptr = None + self._station_clock = None + self._sensor_clock = None self.openPort() + loginf('using %s polling mode' % self.polling_mode) - # Unfortunately there is no provision to obtain the model number. + # Unfortunately there is no provision to obtain the model from the station + # itself, so use what we were given. @property def hardware_name(self): return self.model @@ -479,7 +502,7 @@ class FineOffsetUSB(weewx.abstractstation.AbstractStation): def openPort(self): dev = self._findDevice() if not dev: - logerr("Cannot find USB device with Vendor=0x%04x ProdID=0x%04x" % (self.vendor_id, self.product_id)) + logcrt("Cannot find USB device with Vendor=0x%04x ProdID=0x%04x" % (self.vendor_id, self.product_id)) raise weewx.WeeWxIOError("Unable to find USB device") self.devh = dev.open() # Detach any old claimed interfaces @@ -503,21 +526,23 @@ class FineOffsetUSB(weewx.abstractstation.AbstractStation): self.devh.detachKernelDriver(self.interface) except: pass + + def _findDevice(self): + """Find the vendor and product ID on the USB bus.""" + for bus in usb.busses(): + for dev in bus.devices: + if dev.idVendor == self.vendor_id and dev.idProduct == self.product_id: + return dev def genLoopPackets(self): """Generator function that continuously returns decoded packets""" - - genBytes = self._genBytes_raw() - for ibyte in genBytes: + for p in self.get_observations(): packet = {} # required elements packet['usUnits'] = weewx.METRIC packet['dateTime'] = int(time.time() + 0.5) - # decode the bytes into a pywws dictionary - p = _decode(ibyte, reading_format[self.data_format]) - # map the pywws dictionary to something weewx understands for k in keymap.keys(): if keymap[k][0] in p and p[keymap[k][0]] is not None: @@ -578,14 +603,9 @@ class FineOffsetUSB(weewx.abstractstation.AbstractStation): logdbg('got rainfall of %f cm' % packet['rain']) yield packet - - def _findDevice(self): - """Find the vendor and product ID on the USB bus.""" - for bus in usb.busses(): - for dev in bus.devices: - if dev.idVendor == self.vendor_id and dev.idProduct == self.product_id: - return dev + # get data from the station. + # # there are a few types of non-fatal failures we might encounter while # reading. when we encounter one, report the failure to log then retry. # @@ -593,29 +613,32 @@ class FineOffsetUSB(weewx.abstractstation.AbstractStation): # us, so keep querying until we get a valid pointer. # # if we get USB read failures, retry until we get something valid. - def _genBytes_raw(self): - """Get a sequence of bytes from the USB interface.""" + def get_observations(self): + """Get data from the station at regular intervals.""" nusberr = 0 nptrerr = 0 - old_ptr = None while True: try: - new_ptr = self.current_pos() - if new_ptr is None: - raise CurrentPositionError('current_pos returned None') - if weewx.debug and old_ptr is not None and old_ptr != new_ptr: - logdbg('ptr changed: old=0x%06x new=0x%06x' % (old_ptr, new_ptr)) - nptrerr = 0 - old_ptr = new_ptr + if self.polling_mode == ADAPTIVE_POLLING: + for data in self.live_data(): + nusberr = 0 + yield data + elif self.polling_mode == REGULAR_POLLING: + new_ptr = self.current_pos() + if new_ptr is None: + raise CurrentPositionError('current_pos returned None') + nptrerr = 0 - block = self.get_raw_data(new_ptr, unbuffered=True) - if not len(block) == reading_len[self.data_format]: - raise BlockLengthError(len(block), reading_len[self.data_format]) - - nusberr = 0 - yield block - time.sleep(self.sample_period) + block = self.get_raw_data(new_ptr, unbuffered=True) + if not len(block) == reading_len[self.data_format]: + raise BlockLengthError(len(block), reading_len[self.data_format]) + nusberr = 0 + data = _decode(block, reading_format[self.data_format]) + yield data + time.sleep(self.sample_period) + else: + raise Exception("unknown polling mode '%s'" % self.polling_mode) except (IndexError, usb.USBError), e: logdbg('read data from USB failed: %s' % e) @@ -641,7 +664,7 @@ class FineOffsetUSB(weewx.abstractstation.AbstractStation): #============================================================================== # methods for reading from and writing to usb -# FIXME: these should be abstracted to a class to support multiple usb drivers +# FIXME: to support multiple usb drivers, these should be abstracted to a class #============================================================================== def _read_usb(self, address): @@ -664,9 +687,140 @@ class FineOffsetUSB(weewx.abstractstation.AbstractStation): #============================================================================== # methods for reading data from the weather station -# the following were adapted from pywws 12.10_r547 +# the following were adapted from pywws +# +# commit d422883ee05a8dddcedefd3f1366d54d46037668 +# Author: Jim Easterbrook +# Date: Fri Feb 8 11:54:48 2013 +0000 #============================================================================== + def live_data(self, logged_only=False): + # There are two things we want to synchronise to - the data is + # updated every 48 seconds and the address is incremented + # every 5 minutes (or 10, 15, ..., 30). Rather than getting + # data every second or two, we sleep until one of the above is + # due. (During initialisation we get data every two seconds + # anyway.) + read_period = self.get_fixed_block(['read_period']) + log_interval = float(read_period * 60) + live_interval = 48.0 + old_ptr = self.current_pos() + old_data = self.get_data(old_ptr, unbuffered=True) + now = time.time() + if self._sensor_clock: + next_live = now + next_live -= (next_live - self._sensor_clock) % live_interval + next_live += live_interval + else: + next_live = None + if self._station_clock and next_live: + # set next_log + next_log = next_live - live_interval + next_log -= (next_log - self._station_clock) % 60 + next_log -= old_data['delay'] * 60 + next_log += log_interval + else: + next_log = None + self._station_clock = None + while True: + last_time = now + if not self._station_clock: + next_log = None + if not self._sensor_clock: + next_live = None + # wake up just before next reading is due + now = time.time() + advance = now + max(self.avoid, self.min_pause) + self.min_pause + pause = 600.0 + if next_live: + if not logged_only: + pause = min(pause, next_live - advance) + else: + pause = self.min_pause + if next_log: + pause = min(pause, next_log - advance) + elif old_data['delay'] < read_period - 1: + pause = min( + pause, ((read_period - old_data['delay']) * 60.0) - 110.0) + else: + pause = self.min_pause + pause = max(pause, self.min_pause) +# logdbg('delay %s, pause %g' % (str(old_data['delay']), pause)) + time.sleep(pause) + # first look for data changes + new_data = self.get_data(old_ptr, unbuffered=True) + now = time.time() + # 'good' time stamp if we haven't just woken up from long + # pause and data read wasn't delayed + valid_now = now - last_time < (self.min_pause * 2.0) - 0.1 + # make sure changes because of logging interval aren't + # mistaken for new live data + old_data['delay'] = new_data['delay'] + if self.data_format == '3080': + # ignore solar data which changes every 60 seconds + old_data['illuminance'] = new_data['illuminance'] + old_data['uv'] = new_data['uv'] + if next_live and not logged_only: + while now > next_live + live_interval: + loginf('live_data missed') + next_live += live_interval + if new_data != old_data: + logdbg('live_data new data') + result = dict(new_data) + if valid_now: + # data has just changed, so definitely at a 48s update time + self._sensor_clock = now + loginf('setting sensor clock %g' % (now % live_interval)) + if not next_live: + loginf('live_data live synchronised') + next_live = now + elif next_live and now < next_live - self.min_pause: + loginf('live_data lost sync %g' % (now - next_live)) + next_live = None + self._sensor_clock = None + if next_live and not logged_only: + result['idx'] = datetime.utcfromtimestamp(int(next_live)) + next_live += live_interval + yield result +# yield result, old_ptr, False + old_data = new_data + # now look for pointer changes + if new_data['delay'] < read_period: + # pointer won't have changed + continue + new_ptr = self.current_pos() + now2 = time.time() + valid_now = now2 - last_time < (self.min_pause * 2.0) - 0.1 + while valid_now and next_log and now2 > next_log + 12.0: + loginf('live_data log extended') + next_log += 60.0 + if new_ptr != old_ptr: + logdbg('live_data new ptr: %06x' % new_ptr) + # re-read data, to be absolutely sure it's the last + # logged data before the pointer was updated + new_data = self.get_data(old_ptr, unbuffered=True) + result = dict(new_data) + if valid_now: + # pointer has just changed, so definitely at a logging time + self._station_clock = now2 + loginf('setting station clock %g' % (now2 % 60.0)) + if not next_log: + loginf('live_data log synchronised') + next_log = now2 + elif next_log and now2 < next_log - self.min_pause: + loginf('live_data lost log sync %g' % (now2 - next_log)) + next_log = None + self._station_clock = None + if next_log: + result['idx'] = datetime.utcfromtimestamp(int(next_log)) + next_log += log_interval + yield result +# yield result, old_ptr, True + if new_ptr != self.inc_ptr(old_ptr): + logerr('live_data unexpected ptr change %06x -> %06x' % + (old_ptr, new_ptr)) + old_ptr = new_ptr + def inc_ptr(self, ptr): """Get next circular buffer data pointer.""" result = ptr + reading_len[self.data_format] @@ -756,11 +910,35 @@ class FineOffsetUSB(weewx.abstractstation.AbstractStation): fmt = fmt[key] return _decode(self._fixed_block, fmt) + def _wait_for_station(self): + # avoid times when station is writing to memory + while True: + pause = 60.0 + if self._station_clock: + phase = time.time() - self._station_clock + if phase > 24 * 3600: + # station clock was last measured a day ago, so reset it + self._station_clock = None + else: + pause = min(pause, (self.avoid - phase) % 60) + if self._sensor_clock: + phase = time.time() - self._sensor_clock + if phase > 24 * 3600: + # sensor clock was last measured 6 hrs ago, so reset it + self._sensor_clock = None + else: + pause = min(pause, (self.avoid - phase) % 48) + if pause > self.avoid * 2.0: + return + logdbg('avoid %s' % str(pause)) + time.sleep(pause) + def _read_block(self, ptr, retry=True): # Read block repeatedly until it's stable. This avoids getting corrupt # data when the block is read as the station is updating it. old_block = None while True: + self._wait_for_station() new_block = self._read_usb(ptr) if new_block: if (new_block == old_block) or not retry: @@ -777,10 +955,11 @@ class FineOffsetUSB(weewx.abstractstation.AbstractStation): # check 'magic number' if result[:2] not in ([0x55, 0xAA], [0xFF, 0xFF], [0x55, 0x55], [0xC4, 0x00]): - logerr('unrecognised magic number %02x %02x' % (result[0], result[1])) + logcrt('unrecognised magic number %02x %02x' % (result[0], result[1])) return result def _write_byte(self, ptr, value): + self._wait_for_station() if not self._write_usb(ptr, value): raise weewx.WeeWxIOError('Write to USB failed')