mirror of
https://github.com/weewx/weewx.git
synced 2026-05-24 09:46:19 -04:00
added adaptive polling mode and pressure check
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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 <jim@jim-easterbrook.me.uk>
|
||||
# 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')
|
||||
|
||||
|
||||
Reference in New Issue
Block a user