Files
weewx/experimental/test_ip.py
2012-03-29 16:45:17 +00:00

658 lines
25 KiB
Python

''' Test a VantagePro IP by doing a DMP command
'''
tcp_port = 22222
tcp_send_delay = 1
timeout = 5
import socket
import struct
import time
import sys
try:
hostname = sys.argv[1]
except:
print "usage: test_ip ip-address"
exit(1)
print "Using host ", hostname
# These are used for serial ports
#port = '/dev/ttyUSB0'
#baudrate=19200
#import serial
class WeeWxIOError(IOError):
"""Base class of exceptions thrown when encountering an I/O error with the console."""
class WakeupError(WeeWxIOError):
"""Exception thrown when unable to wake up the console"""
class CRCError(WeeWxIOError):
"""Exception thrown when unable to pass a CRC check."""
class RetriesExceeded(WeeWxIOError):
"""Exception thrown when max retries exceeded."""
class UnknownArchiveType(StandardError):
"""Exception thrown after reading an unrecognized archive type."""
# A few handy constants:
_ack = chr(0x06)
_resend = chr(0x15) # NB: The Davis documentation gives this code as 0x21, but it's actually decimal 21
class EthernetWrapper(object):
"""Ethernet connection"""
def __init__(self, host, port, timeout, tcp_send_delay):
self.host = host
self.port = port
self.timeout = timeout
self.tcp_send_delay = tcp_send_delay
def wakeup_console(self, max_tries=3, wait_before_retry=1.2):
"""Wake up a Davis VantagePro console.
If unsuccessful, an exception of type weewx.WakeupError is thrown"""
# Wake up the console. Try up to max_tries times
for unused_count in xrange(max_tries) :
try:
# Clear out any pending input or output characters:
self.flush_output()
self.flush_input()
# It can be hard to get the console's attention, particularly
# when in the middle of a LOOP command. Send a whole bunch of line feeds,
# then flush everything, then look for the \n\r acknowledgment
self.write('\n\n\n')
time.sleep(0.5)
self.flush_input()
self.write('\n')
_resp = self.read(2)
if _resp == '\n\r':
print "successfully woke up console"
return
print "Unable to wake up console... sleeping"
time.sleep(wait_before_retry)
print "Unable to wake up console... retrying"
except WeeWxIOError:
pass
print "Unable to wake up console"
raise WakeupError("Unable to wake up VantagePro console")
def send_data(self, data):
"""Send data to the Davis console, waiting for an acknowledging <ACK>
If the <ACK> is not received, no retry is attempted. Instead, an exception
of type weewx.WeeWxIOError is raised
data: The data to send, as a string"""
self.write(data)
# Look for the acknowledging ACK character
_resp = self.read()
if _resp != _ack:
print "No <ACK> received from console"
raise WeeWxIOError("No <ACK> received from VantagePro console")
def send_data_with_crc16(self, data, max_tries=3) :
"""Send data to the Davis console along with a CRC check, waiting for an acknowledging <ack>.
If none received, resend up to max_tries times.
data: The data to send, as a string"""
#Calculate the crc for the data:
_crc = crc16(data)
# ...and pack that on to the end of the data in big-endian order:
_data_with_crc = data + struct.pack(">H", _crc)
# Retry up to max_tries times:
for unused_count in xrange(max_tries):
try:
self.write(_data_with_crc)
# Look for the acknowledgment.
_resp = self.read()
if _resp == _ack:
return
except WeeWxIOError:
pass
print "Unable to pass CRC16 check while sending data"
raise CRCError("Unable to pass CRC16 check while sending data to VantagePro console")
def send_command(self, command, max_tries=3, wait_before_retry=1.2):
"""Send a command to the console, then look for the string 'OK' in the response.
Any response from the console is split on \n\r characters and returned as a list."""
for unused_count in xrange(max_tries):
try :
self.wakeup_console(max_tries=max_tries, wait_before_retry=wait_before_retry)
self.write(command)
# Takes a bit for the VP to react and fill up the buffer. Sleep for a half sec:
time.sleep(0.5)
# Can't use function serial.readline() because the VP responds with \n\r, not just \n.
# So, instead find how many bytes are waiting and fetch them all
nc = self.queued_bytes()
_buffer = self.read(nc)
# Split the buffer on the newlines
_buffer_list = _buffer.strip().split('\n\r')
# The first member should be the 'OK' in the VP response
if _buffer_list[0] == 'OK' :
# Return the rest:
return _buffer_list[1:]
except WeeWxIOError:
# Caught an error. Keep trying...
continue
print "Max retries exceeded while sending command %s" % command
raise RetriesExceeded("Max retries exceeded while sending command %s" % command)
def get_data_with_crc16(self, nbytes, prompt=None, max_tries=3) :
"""Get a packet of data and do a CRC16 check on it, asking for retransmit if necessary.
It is guaranteed that the length of the returned data will be of the requested length.
An exception of type CRCError will be thrown if the data cannot pass the CRC test
in the requested number of retries.
nbytes: The number of bytes (including the 2 byte CRC) to get.
prompt: Any string to be sent before requesting the data. Default=None
max_tries: Number of tries before giving up. Default=3
returns: the packet data as a string. The last 2 bytes will be the CRC"""
if prompt:
self.write(prompt)
first_time = True
for unused_count in xrange(max_tries):
try:
if not first_time:
self.write(_resend)
_buffer = self.read(nbytes)
if crc16(_buffer) == 0 :
return _buffer
except WeeWxIOError:
pass
first_time = False
print "Unable to pass CRC16 check while getting data"
raise CRCError("Unable to pass CRC16 check while getting data")
def openPort(self):
try:
self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.socket.settimeout(self.timeout)
self.socket.connect((self.host, self.port))
except:
print "Unable to connect to ethernet host %s on port %d." % (self.host, self.port)
raise
def closePort(self):
try:
# This will cancel any pending loop:
self.wakeup_console(max_tries=1)
except:
pass
self.socket.shutdown(socket.SHUT_RDWR)
self.socket.close()
def flush_input(self):
"""Flush the input buffer from WeatherLinkIP"""
try:
# This is a bit of a hack, but there is no analogue to pyserial's flushInput()
self.socket.settimeout(0)
self.read(4096)
except:
pass
finally:
self.socket.settimeout(self.timeout)
def flush_output(self):
"""Flush the output buffer to WeatherLinkIP
This function does nothing as there should never be anything left in
the buffer when using socket.sendall()"""
pass
def queued_bytes(self):
"""Determine how many bytes are in the buffer"""
length = 0
try:
self.socket.settimeout(0)
length = len(self.socket.recv(8192, socket.MSG_PEEK))
except socket.error:
pass
finally:
self.socket.settimeout(self.timeout)
return length
def read(self, chars=1):
"""Read bytes from WeatherLinkIP"""
try:
_buffer = self.socket.recv(chars)
N = len(_buffer)
if N != chars:
raise WeeWxIOError("Expected to read %d chars; got %d instead" % (chars, N))
return _buffer
except:
raise WeeWxIOError("Socket read error")
def write(self, data):
"""Write to a WeatherLinkIP"""
try:
self.socket.sendall(data)
time.sleep(self.tcp_send_delay)
except:
raise WeeWxIOError("Socket write error")
#===============================================================================
# VantagePro class
#===============================================================================
class VantagePro(object):
"""Class that represents a connection to a VantagePro console.
The connection will be opened after initialization"""
def __init__(self, **vp_dict) :
"""Initialize an object of type VantagePro. """
self.wait_before_retry= float(vp_dict.get('wait_before_retry', 1.2))
self.max_tries = int(vp_dict.get('max_tries' , 4))
# Get the port:
self.port = EthernetWrapper(hostname, tcp_port, timeout, tcp_send_delay)
#self.port = SerialWrapper(port, baudrate, timeout)
# Open it up:
self.port.openPort()
def closePort(self):
"""Close the connection to the console. """
self.port.closePort()
def genArchivePackets(self):
"""A generator function to return all archive packets on a VP2.
yields: a sequence of dictionaries containing the data
"""
# Wake up the console...
self.port.wakeup_console()
# ... request a dump...
self.port.send_data('DMP\n')
# Cycle through the pages...
for ipage in range(512):
# ... get a page of archive data
_page = self.port.get_data_with_crc16(267, prompt=_ack, max_tries=self.max_tries)
# Now extract each record from the page
for _index in xrange(5) :
# If the console has been recently initialized, there will
# be unused records, which are filled with 0xff. Detect this
# by looking at the first 4 bytes (the date and time):
if _page[1+52*_index:5+52*_index] == 4*chr(0xff) :
yield None
# Unpack the raw archive packet:
_packet = unpackArchivePacket(_page[1+52*_index:53+52*_index])
timestamp = _archive_datetime(_packet)
yield timestamp
#===============================================================================
# UTILITIES
#===============================================================================
def timestamp_to_string(ts):
"""Return a string formatted from the timestamp """
if ts:
return "%s (%d)" % (time.strftime("%Y-%m-%d %H:%M:%S %Z", time.localtime(ts)), ts)
else:
return "****** N/A ******** ( N/A )"
# Tuples of all the types held in a VantagePro2 Rev A or Rev B archive packet in their native order.
vp2archA =('date_stamp', 'time_stamp', 'outTemp', 'highOutTemp', 'lowOutTemp',
'rain', 'rainRate', 'barometer', 'radiation', 'number_of_wind_samples',
'inTemp', 'inHumidity', 'outHumidity', 'windSpeed', 'windGust', 'windGustDir', 'windDir',
'UV', 'ET', 'soilMoist1', 'soilMoist2', 'soilMoist3', 'soilMoist4',
'soilTemp1', 'soilTemp2', 'soilTemp3','soilTemp4',
'leafWet1', 'leafWet2', 'leafWet3', 'leafWet4',
'extraTemp1', 'extraTemp2',
'extraHumid1', 'extraHumid2',
'readClosed', 'readOpened')
vp2archB =('date_stamp', 'time_stamp', 'outTemp', 'highOutTemp', 'lowOutTemp',
'rain', 'rainRate', 'barometer', 'radiation', 'number_of_wind_samples',
'inTemp', 'inHumidity', 'outHumidity', 'windSpeed', 'windGust', 'windGustDir', 'windDir',
'UV', 'ET', 'highRadiation', 'highUV', 'forecastRule',
'leafTemp1', 'leafTemp2', 'leafWet1', 'leafWet2',
'soilTemp1', 'soilTemp2', 'soilTemp3','soilTemp4', 'download_record_type',
'extraHumid1', 'extraHumid2', 'extraTemp1', 'extraTemp2', 'extraTemp3',
'soilMoist1', 'soilMoist2', 'soilMoist3', 'soilMoist4')
archive_format_revA = struct.Struct("<HHhhhHHHHHhBBBBBBBBx4B4B4B2B2BHHx")
archive_format_revB = struct.Struct("<HHhhhHHHHHhBBBBBBBBHBB2B2B4BB2B3B4B")
def unpackArchivePacket(raw_packet):
"""Decode a Davis archive packet, returning the results as a dictionary.
raw_packet: The archive packet data buffer, passed in as a string. This will be unpacked and
the results placed a dictionary"""
# Figure out the packet type:
packet_type = ord(raw_packet[42])
if packet_type == 0x00 :
# Rev B packet type:
archive_format = archive_format_revB
dataTypes = vp2archB
elif packet_type == 0xff:
# Rev A packet type:
archive_format = archive_format_revA
dataTypes = vp2archA
else:
raise UnknownArchiveType("Unknown archive type = 0x%x" % (packet_type,))
data_tuple = archive_format.unpack(raw_packet)
packet = dict(zip(dataTypes, data_tuple))
return packet
def _archive_datetime(packet) :
"""Returns the epoch time of the archive packet."""
datestamp = packet['date_stamp']
timestamp = packet['time_stamp']
# Decode the Davis time, constructing a time-tuple from it:
time_tuple = ((0xfe00 & datestamp) >> 9, # year
(0x01e0 & datestamp) >> 5, # month
(0x001f & datestamp), # day
timestamp // 100, # hour
timestamp % 100, # minute
0, # second
0, 0, -1)
# Convert to epoch time:
try:
ts = int(time.mktime(time_tuple))
except (OverflowError, ValueError):
ts = None
return ts
import array
_table=(
0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50a5, 0x60c6, 0x70e7, # 0x00
0x8108, 0x9129, 0xa14a, 0xb16b, 0xc18c, 0xd1ad, 0xe1ce, 0xf1ef, # 0x08
0x1231, 0x0210, 0x3273, 0x2252, 0x52b5, 0x4294, 0x72f7, 0x62d6, # 0x10
0x9339, 0x8318, 0xb37b, 0xa35a, 0xd3bd, 0xc39c, 0xf3ff, 0xe3de, # 0x18
0x2462, 0x3443, 0x0420, 0x1401, 0x64e6, 0x74c7, 0x44a4, 0x5485, # 0x20
0xa56a, 0xb54b, 0x8528, 0x9509, 0xe5ee, 0xf5cf, 0xc5ac, 0xd58d, # 0x28
0x3653, 0x2672, 0x1611, 0x0630, 0x76d7, 0x66f6, 0x5695, 0x46b4, # 0x30
0xb75b, 0xa77a, 0x9719, 0x8738, 0xf7df, 0xe7fe, 0xd79d, 0xc7bc, # 0x38
0x48c4, 0x58e5, 0x6886, 0x78a7, 0x0840, 0x1861, 0x2802, 0x3823, # 0x40
0xc9cc, 0xd9ed, 0xe98e, 0xf9af, 0x8948, 0x9969, 0xa90a, 0xb92b, # 0x48
0x5af5, 0x4ad4, 0x7ab7, 0x6a96, 0x1a71, 0x0a50, 0x3a33, 0x2a12, # 0x50
0xdbfd, 0xcbdc, 0xfbbf, 0xeb9e, 0x9b79, 0x8b58, 0xbb3b, 0xab1a, # 0x58
0x6ca6, 0x7c87, 0x4ce4, 0x5cc5, 0x2c22, 0x3c03, 0x0c60, 0x1c41, # 0x60
0xedae, 0xfd8f, 0xcdec, 0xddcd, 0xad2a, 0xbd0b, 0x8d68, 0x9d49, # 0x68
0x7e97, 0x6eb6, 0x5ed5, 0x4ef4, 0x3e13, 0x2e32, 0x1e51, 0x0e70, # 0x70
0xff9f, 0xefbe, 0xdfdd, 0xcffc, 0xbf1b, 0xaf3a, 0x9f59, 0x8f78, # 0x78
0x9188, 0x81a9, 0xb1ca, 0xa1eb, 0xd10c, 0xc12d, 0xf14e, 0xe16f, # 0x80
0x1080, 0x00a1, 0x30c2, 0x20e3, 0x5004, 0x4025, 0x7046, 0x6067, # 0x88
0x83b9, 0x9398, 0xa3fb, 0xb3da, 0xc33d, 0xd31c, 0xe37f, 0xf35e, # 0x90
0x02b1, 0x1290, 0x22f3, 0x32d2, 0x4235, 0x5214, 0x6277, 0x7256, # 0x98
0xb5ea, 0xa5cb, 0x95a8, 0x8589, 0xf56e, 0xe54f, 0xd52c, 0xc50d, # 0xA0
0x34e2, 0x24c3, 0x14a0, 0x0481, 0x7466, 0x6447, 0x5424, 0x4405, # 0xA8
0xa7db, 0xb7fa, 0x8799, 0x97b8, 0xe75f, 0xf77e, 0xc71d, 0xd73c, # 0xB0
0x26d3, 0x36f2, 0x0691, 0x16b0, 0x6657, 0x7676, 0x4615, 0x5634, # 0xB8
0xd94c, 0xc96d, 0xf90e, 0xe92f, 0x99c8, 0x89e9, 0xb98a, 0xa9ab, # 0xC0
0x5844, 0x4865, 0x7806, 0x6827, 0x18c0, 0x08e1, 0x3882, 0x28a3, # 0xC8
0xcb7d, 0xdb5c, 0xeb3f, 0xfb1e, 0x8bf9, 0x9bd8, 0xabbb, 0xbb9a, # 0xD0
0x4a75, 0x5a54, 0x6a37, 0x7a16, 0x0af1, 0x1ad0, 0x2ab3, 0x3a92, # 0xD8
0xfd2e, 0xed0f, 0xdd6c, 0xcd4d, 0xbdaa, 0xad8b, 0x9de8, 0x8dc9, # 0xE0
0x7c26, 0x6c07, 0x5c64, 0x4c45, 0x3ca2, 0x2c83, 0x1ce0, 0x0cc1, # 0xE8
0xef1f, 0xff3e, 0xcf5d, 0xdf7c, 0xaf9b, 0xbfba, 0x8fd9, 0x9ff8, # 0xF0
0x6e17, 0x7e36, 0x4e55, 0x5e74, 0x2e93, 0x3eb2, 0x0ed1, 0x1ef0, # 0xF8
)
table = array.array('H',_table)
def crc16(string, crc=0):
""" Calculate CRC16 sum"""
for ch in string:
crc = (table[((crc>>8)^ord(ch)) & 0xff] ^ (crc<<8)) & 0xffff
return crc
#===============================================================================
# SerialWrapper
#===============================================================================
#class SerialWrapper(object):
# """Wraps a serial connection returned from package serial"""
#
# def __init__(self, port, baudrate, timeout):
# self.port = port
# self.baudrate = baudrate
# self.timeout = timeout
#
#
# #===============================================================================
# # Primitives for working with the Davis Console
# #===============================================================================
#
# def wakeup_console(self, max_tries=3, wait_before_retry=1.2):
# """Wake up a Davis VantagePro console.
#
# If unsuccessful, an exception of type weewx.WakeupError is thrown"""
#
# # Wake up the console. Try up to max_tries times
# for unused_count in xrange(max_tries) :
# try:
# # Clear out any pending input or output characters:
# self.flush_output()
# self.flush_input()
# # It can be hard to get the console's attention, particularly
# # when in the middle of a LOOP command. Send a whole bunch of line feeds,
# # then flush everything, then look for the \n\r acknowledgment
# self.write('\n\n\n')
# time.sleep(0.5)
# self.flush_input()
# self.write('\n')
# _resp = self.read(2)
# if _resp == '\n\r':
# print "successfully woke up console"
# return
# print "Unable to wake up console... sleeping"
# time.sleep(wait_before_retry)
# print "Unable to wake up console... retrying"
# except WeeWxIOError:
# pass
#
# print "Unable to wake up console"
# raise WakeupError("Unable to wake up VantagePro console")
#
# def send_data(self, data):
# """Send data to the Davis console, waiting for an acknowledging <ACK>
#
# If the <ACK> is not received, no retry is attempted. Instead, an exception
# of type weewx.WeeWxIOError is raised
#
# data: The data to send, as a string"""
#
# self.write(data)
#
# # Look for the acknowledging ACK character
# _resp = self.read()
# if _resp != _ack:
# print "No <ACK> received from console"
# raise WeeWxIOError("No <ACK> received from VantagePro console")
#
# def send_data_with_crc16(self, data, max_tries=3) :
# """Send data to the Davis console along with a CRC check, waiting for an acknowledging <ack>.
# If none received, resend up to max_tries times.
#
# data: The data to send, as a string"""
#
# #Calculate the crc for the data:
# _crc = crc16(data)
#
# # ...and pack that on to the end of the data in big-endian order:
# _data_with_crc = data + struct.pack(">H", _crc)
#
# # Retry up to max_tries times:
# for unused_count in xrange(max_tries):
# try:
# self.write(_data_with_crc)
# # Look for the acknowledgment.
# _resp = self.read()
# if _resp == _ack:
# return
# except WeeWxIOError:
# pass
#
# print "VantagePro: Unable to pass CRC16 check while sending data"
# raise CRCError("Unable to pass CRC16 check while sending data to VantagePro console")
#
# def send_command(self, command, max_tries=3, wait_before_retry=1.2):
# """Send a command to the console, then look for the string 'OK' in the response.
#
# Any response from the console is split on \n\r characters and returned as a list."""
#
# for unused_count in xrange(max_tries):
# try :
# self.wakeup_console(max_tries=max_tries, wait_before_retry=wait_before_retry)
#
# self.write(command)
# # Takes a bit for the VP to react and fill up the buffer. Sleep for a half sec:
# time.sleep(0.5)
# # Can't use function serial.readline() because the VP responds with \n\r, not just \n.
# # So, instead find how many bytes are waiting and fetch them all
# nc = self.queued_bytes()
# _buffer = self.read(nc)
# # Split the buffer on the newlines
# _buffer_list = _buffer.strip().split('\n\r')
# # The first member should be the 'OK' in the VP response
# if _buffer_list[0] == 'OK' :
# # Return the rest:
# return _buffer_list[1:]
#
# except WeeWxIOError:
# # Caught an error. Keep trying...
# continue
#
# print "Max retries exceeded while sending command %s" % command
# raise RetriesExceeded("Max retries exceeded while sending command %s" % command)
#
#
# def get_data_with_crc16(self, nbytes, prompt=None, max_tries=3) :
# """Get a packet of data and do a CRC16 check on it, asking for retransmit if necessary.
#
# It is guaranteed that the length of the returned data will be of the requested length.
# An exception of type CRCError will be thrown if the data cannot pass the CRC test
# in the requested number of retries.
#
# nbytes: The number of bytes (including the 2 byte CRC) to get.
#
# prompt: Any string to be sent before requesting the data. Default=None
#
# max_tries: Number of tries before giving up. Default=3
#
# returns: the packet data as a string. The last 2 bytes will be the CRC"""
# if prompt:
# self.write(prompt)
#
# first_time = True
#
# for unused_count in xrange(max_tries):
# try:
# if not first_time:
# self.write(_resend)
# _buffer = self.read(nbytes)
# if crc16(_buffer) == 0 :
# return _buffer
# except WeeWxIOError:
# pass
# first_time = False
#
# print "Unable to pass CRC16 check while getting data"
# raise CRCError("Unable to pass CRC16 check while getting data")
#
# def flush_input(self):
# self.serial_port.flushInput()
#
# def flush_output(self):
# self.serial_port.flushOutput()
#
# def queued_bytes(self):
# return self.serial_port.inWaiting()
#
# def read(self, chars=1):
# _buffer = self.serial_port.read(chars)
# N = len(_buffer)
# if N != chars:
# raise WeeWxIOError("Expected to read %d chars; got %d instead" % (chars, N))
# return _buffer
#
# def write(self, data):
# N = self.serial_port.write(data)
# # Python version 2.5 and earlier returns 'None', so it cannot be used to test for completion.
# if N is not None and N != len(data):
# raise WeeWxIOError("Expected to write %d chars; sent %d instead" % (len(data), N))
#
# def openPort(self):
# # Open up the port and store it
# self.serial_port = serial.Serial(self.port, self.baudrate, timeout=self.timeout)
#
# def closePort(self):
# try:
# # This will cancel any pending loop:
# self.wakeup_console(max_tries=1)
# except:
# pass
# self.serial_port.close()
#===============================================================================
# Main test
#===============================================================================
vp = VantagePro()
N=N_none=0
t1=time.time()
try:
for ts in vp.genArchivePackets():
print timestamp_to_string(ts)
if ts:
N += 1
else:
N_none += 1
except Exception, e:
print "Exception:", e
else:
print "Loop terminated normally"
T = time.time() - t1
print "Timestamps received: ", N
print "Nulls received: ", N_none
print "Elapsed time (secs): ", T