# HG changeset patch # User darius@Inchoate # Date 1231811053 -37800 # Node ID 75f785a09e2ebd43e927c610e9f684787590ea93 # Parent 4c91fdfc862e97d2f8700c65ff12170b5d88b452 Add license. Split test stuff into a separate file. Fix some bugs.. - Default frame ID to 1 so we get status replies by default. - Correct address generation routine for TX packets. Add doc strings and epydoc variable comments. diff -r 4c91fdfc862e -r 75f785a09e2e zb.py --- a/zb.py Wed Nov 07 17:10:08 2007 +1030 +++ b/zb.py Tue Jan 13 12:14:13 2009 +1030 @@ -1,25 +1,77 @@ -import serial, inspect, time +# +# Code to talk to MaxStream ZigBee modules in API (no escaped +# characters) +# +# Copyright (c) 2009 +# Daniel O'Connor . All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY AUTHOR AND CONTRIBUTORS ``AS IS'' AND +# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL AUTHOR OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +# SUCH DAMAGE. +# +"""MaxStream ZigBee module API interface + +This code expects the module to be in API mode 1 (no escape +characters), ie you have done something like.. ++++ +ATAP=1 +ATWR +ATCN + +See here for details +http://www.digi.com/products/wireless/point-multipoint/xbee-series1-moduledocs.jsp +""" + +import inspect def easyord(i): + """Helper function to return the ordinal of a string, or just the +passed in value""" if (type(i) != type(str())): return i else: return ord(i) class PktBase(object): + """Base class for all packet types""" PKT_MAXLEN = 2 ** 16 + PKT_TYPE = None def __init__(self): - print "Constructing " + self.__class__.__name__ + #print "Constructing " + self.__class__.__name__ + pass + + def Encapsulate(self): + """Encapsulate the packet""" + return Packets.Encapsulate([self.PKT_TYPE] + self.data) - def Encapsulate(self): - return Packets.Encapsulate([self.PKT_TYPE] + self.data) + def Pack(self): + """Return string version of encapsulated packet""" + return reduce(lambda x, y: str(x) + str(y), self.Encapsulate()) def resize(self, dlen): """Ensure the data list can hold at least len elements (0 fill)""" if (len(self._data) < dlen): self._data = (self._data + [0] * dlen)[0:dlen] + @staticmethod def _checklist(list, min = 0, max = 255, maxlen = None): if (maxlen != None and len(list) > maxlen): raise ValueError("must have %d elements" % (maxlen)) @@ -28,31 +80,35 @@ if (easyord(list[i]) < min or easyord(list[i]) > max): raise ValueError("element %d (= %d) out of range must be between %d and %d inclusive" % (i, ord(list[i]), min, max)) - _checklist = staticmethod(_checklist) class TXPkts(PktBase): """Base class for all packets that go to the module""" + def __init__(self): + # Frame ID of 0 will prevent TX status pakets being sent + self._frameid = 1 + def setframeid(self, value): if (value < 0 or value > 255): raise ValueError("FrameID must be 0-255") self._frameid = value frameid = property(lambda s: s._frameid, setframeid) +class AT_Cmd(TXPkts): + """AT command packet""" -class AT_Cmd(TXPkts): PKT_TYPE = 0x08 PKT_DESC = "AT Command" def __init__(self, cmd = None, cmdarg = None): + self.frameid = 1 # XXX: why do I need to dupe this? + self.cmdarg = [] + super(AT_Cmd, self).__init__() if (cmd != None): self.cmd = cmd if (cmdarg != None): self.cmdarg = cmdarg - - self.frameid = 0 - self.cmdarg = [] def setcmd(self, value): if (len(value) != 2): @@ -71,10 +127,14 @@ data = property(getdata) class AT_Cmd_Queue(AT_Cmd): + """Queued AT command packet""" + PKT_TYPE = 0x09 PKT_DESC = "AT Command (queued)" class AT_Response(PktBase): + """Response from an AT command packet""" + PKT_TYPE = 0x88 PKT_DESC = "AT Command response" frameid = property(lambda s: s._data[0], None) @@ -82,42 +142,71 @@ statusOK = property(lambda s: s._data[3] == 0, None) payload = property(lambda s: s._data[4:], None) + def __init__(self, data = []): + super(AT_Response, self).__init__() + self._data = data + class Modem_Status(PktBase): PKT_TYPE = 0x8a PKT_DESC = "Modem Status" class RX_16_Bit(PktBase): + """RX packet from a remote module (16 bit)""" PKT_TYPE = 0x81 PKT_DESC = "RX Packet: 16 bit address" ADDR_SIZE = 2 + ADRBCASTMSK = 0x01 + PANBCASTMSK = 0x02 def __init__(self, data = []): super(RX_16_Bit, self).__init__() self._data = data - + def __str__(self): - return "0x%0*x (%ddBm) -> %s" % (self.ADDR_SIZE * 2, self.sender, - self.rssi, str(self.payload)) - + return "RX_%d_Bit 0x%0*x (%ddBm) -> %s" % (self.ADDR_SIZE * 8, self.ADDR_SIZE * 2, + self.sender, self.rssi, self.payloadstr) def getsender(self): value = 0 + # Done this way so we can reuse the code for the 64 bit version for i, j in zip(reversed(range(self.ADDR_SIZE)), range(0, self.ADDR_SIZE * 8, 8)): value |= self._data[i] << j return value + #: Source module address sender = property(getsender, None) + def isAdrBcast(self): + """Is this an address broadcast packet?""" + return self.flags & self.ADRBCASTMSK + + def isPanBcast(self): + """Is this an PAN broadcast packet?""" + return self.flags & self.PANBCASTMSK + + #: RX signal strength (dBm) rssi = property(lambda s: -1 * s._data[s.ADDR_SIZE], None) + #: Return flag byte flags = property(lambda s: s._data[s.ADDR_SIZE + 1], None) + #: Payload (list of ords) payload = property(lambda s: s._data[s.ADDR_SIZE + 2:], None) - + + #: String version of payload + def payloadstr(self): + return reduce(lambda a, b: a + chr(b), self.payload, "") + payloadstr = property(payloadstr, None) + + class RX_64_Bit(RX_16_Bit): PKT_TYPE = 0x80 PKT_DESC = "RX Packet: 64 bit address" ADDR_SIZE = 8 class RXIO_16_Bit(RX_16_Bit): + """RX I/O packet from remote module (16 bit). + +This is sent when a remote module is configured to send data based on its IO or DAC pins +""" PKT_TYPE = 0x83 PKT_DESC = "RXIO Packet: 16 bit address" @@ -151,33 +240,45 @@ ADDR_SIZE = 8 class TX_16_Bit(TXPkts): + """Transmit to a 16 bit destination""" PKT_TYPE = 0x01 PKT_DESC = "TX Packet: 16 bit address" ADDR_SIZE = 2 + #: Flag to disable ACK FLG_DISABLE_ACK = 0x01 + #: Send to broadcast PAN ID FLG_BCAST_PANID = 0x04 + #: Maximum size payload we can send PKT_MAX_PAYLOAD = 100 def __init__(self, *args): - if (len(args) == 1): + """Takes 0 to 2 arguments. First is the recipient, the second is the payload (string)""" + self._flags = 0 + self.payload = [] + + if len(args) == 0: + pass + if len(args) == 1: super(TX_16_Bit, self).__init__() self.recipient = args[0] - elif (len(args) == 2): - super(TX_16_Bit, self).__init__([]) + elif len(args) == 2: + super(TX_16_Bit, self).__init__() self.recipient = args[0] self.payload = args[1] else: - raise TypeError("__init__ takes 1 list of ordinals or 2 strings") + raise TypeError("incorrect number of arguments"); - self.frameid = 0 - self.flags = 0 - self.payload = [] - + def __str__(self): + return "TX_%d_Bit 0x%0*x <- %s" % (self.ADDR_SIZE * 8, self.ADDR_SIZE * 2, self.recipient, + self.payload) + def setrecipient(self, value): if (value < 0 or value > 2 ** (self.ADDR_SIZE * 8)): raise ValueError("value out of range must be between 0 and %d" % (2 ** self.ADDR_SIZE)) self._recipient = value + + """Destination address of the packet""" recipient = property(lambda s: s._recipient, setrecipient) def setflags(self, value): @@ -192,9 +293,13 @@ self._payload = value payload = property(lambda s: s._payload, setpayload) + def payloadstr(self): + return reduce(lambda a, b: a + chr(b), self.payload, "") + payloadstr = property(payloadstr, None) + def getdata(self): data = [self.frameid] - for i, j in zip(reversed(range(self.ADDR_SIZE)), range(0, self.ADDR_SIZE * 8, 8)): + for i, j in zip(reversed(range(self.ADDR_SIZE)), reversed(range(0, self.ADDR_SIZE * 8, 8))): data.append((self.recipient & (0xff << j)) >> j) data.append(self.flags) data.extend(map(easyord, self.payload)) @@ -213,11 +318,18 @@ frameid = property(lambda s: s._data[0], None) status = property(lambda s: s._data[1], None) statusMsg = property(lambda s: s.statusTxt[s._data[1]], None) - + + def __init__(self, data = []): + super(TX_Status, self).__init__() + self._data = data + class Packets(object): + """Packet parsing class (misnamed)""" PKT_CLASSES = None - + + @classmethod def Build(self, data): + """Build a packet from data""" if (self.PKT_CLASSES == None): m = inspect.getmodule(self) # Generate list of objects from their names @@ -230,20 +342,21 @@ for p in self.PKT_CLASSES: if (p.PKT_TYPE == data[0]): + #print "Matched " + str(p.PKT_TYPE) return(p(data[1:])) raise ValueError("Unknown packet type 0x%02x" % (data[0])) - Build = classmethod(Build) + @staticmethod def Encapsulate(data): + """Encapsulate a packet so it can be sent to the module. Calculates checksum etc..""" pktsum = reduce(lambda x, y: x + y, data) & 0xff pkt = [0x7e] + [len(data) >> 8] + [len(data) & 0xff] + data + [0xff - pktsum] return(map(chr, pkt)) - Encapsulate = staticmethod(Encapsulate) - def __init__(self, s = None): - print str(inspect.getmodule(self)) + """Init class, if s is passed in it is used for reading & writing data""" + #print str(inspect.getmodule(self)) self.buffer = [] self.state = 'init' self.packets = [] @@ -256,27 +369,38 @@ self.rx_cnt = 0 # Packet count self.pktq = [] - self.s = s + self.s = s # Output handle for convenience methods def writedata(self, data): + """Convenience method to write data""" self.s.write("".join(map(str, data))) def getdata(self): + """Read data until nothing is available (assumes non-blocking) and process it""" l = [] - while (1): + while 1: a = self.s.read() - if (a == ''): + if a == '': break l.append(ord(a)) return self.process(l) + def processstr(self, data): + """Process a string of data""" + return self.process(map(ord, data)) + def process(self, data): + """Process (ordinal) data through the state machine. + +Returns the number of packets in the queue when finished. Updates +various internal counters too. +""" pktcount = 0 for d in data: if (self.state == 'init'): if (d != 0x7e): - print "Framing error, got 0x%02x, expected 0x7e" % (d) + print "Framing error, got 0x%02x, expected 0x7e" % d self.fr_err += 1 continue @@ -302,7 +426,7 @@ print "Checksum error, got 0x%02x, expected 0x%02x" % \ (rxcksum, 0xff - pktsum) else: - print "Got a packet - " + str(self.buffer) + #print "Got a packet - " + str(self.buffer) p = Packets.Build(self.buffer) self.pktq.append(p) self.buffer = [] @@ -314,49 +438,3 @@ return pktcount -def polldev(): - while (1): - foo = up.getdata() - for p in up.pktq: - print p - - if (foo == 0): - time.sleep(0.1) - -#for c in dir(): -# if (issubclass(c, PktBase)): -# print .. - -try: - s = serial.Serial(port='/dev/cuaU0', baudrate=9600, bytesize=8, parity='N', \ - stopbits=1, rtscts=0) - # Non-blocking - s.timeout = 0 - #s.write('+++') - #s.readline(eol='\r') -except serial.serialutil.SerialException, e: - print "Can't open serial port - " + str(e) - s = None - -# 0x0001 (-36dBm) -> 1 samples, mask 0x000f, DIO - 0x00f -goodtest = [126, 0, 10, 131, 0, 1, 36, 0, 1, 0, 15, 0, 15, 56] - -# Checksum error -badtest = [126, 0, 10, 131, 0, 1, 36, 0, 1, 0, 15, 0, 14, 56] - -#0x0005 (-36dBm) -> 1 samples, mask 0x020e, DIO - 0x00e, ADC0 - 0x3ff -adctest = [126, 0, 12, 131, 0, 5, 36, 0, 1, 2, 14, 0, 14, 3, 255, 50] - -# Exception -badpkttypetest = [126, 0, 3, 10, 86, 76, 83] - -# Frame ID = 0, Cmd = 'VL', Status = OK, Value = 'VL Result' -atreply = [126, 0, 14, 136, 0, 86, 76, 0, 86, 76, 32, 82, 101, 115, 117, 108, 116, 148] - -up = Packets(s) -up.process(goodtest) -up.process(badtest) -up.process(adctest) -print up.pktq.pop(0) -print up.pktq.pop(0) - diff -r 4c91fdfc862e -r 75f785a09e2e zbtest.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/zbtest.py Tue Jan 13 12:14:13 2009 +1030 @@ -0,0 +1,86 @@ +# +# Test code for ZigBee code +# +# Copyright (c) 2009 +# Daniel O'Connor . All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY AUTHOR AND CONTRIBUTORS ``AS IS'' AND +# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL AUTHOR OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +# SUCH DAMAGE. +# + +import zb, serial, time + +def polldev(): + while (1): + foo = up.getdata() + for p in up.pktq: + print p + + if (foo == 0): + time.sleep(0.1) + +#for c in dir(): +# if (issubclass(c, PktBase)): +# print .. + +try: + s = serial.Serial(port='/dev/cuaU0', baudrate=38400, bytesize=8, parity='N', \ + stopbits=1, rtscts=0) + # Non-blocking + s.timeout = 0 + #s.write('+++') + #s.readline(eol='\r') +except serial.serialutil.SerialException, e: + print "Can't open serial port - " + str(e) + s = None + +# 0x0001 (-36dBm) -> 1 samples, mask 0x000f, DIO - 0x00f +goodtest = [126, 0, 10, 131, 0, 1, 36, 0, 1, 0, 15, 0, 15, 56] + +# Checksum error +badtest = [126, 0, 10, 131, 0, 1, 36, 0, 1, 0, 15, 0, 14, 56] + +#0x0005 (-36dBm) -> 1 samples, mask 0x020e, DIO - 0x00e, ADC0 - 0x3ff +adctest = [126, 0, 12, 131, 0, 5, 36, 0, 1, 2, 14, 0, 14, 3, 255, 50] + +# Exception +badpkttypetest = [126, 0, 3, 10, 86, 76, 83] + +# Frame ID = 0, Cmd = 'VL', Status = OK, Value = 'VL Result' +atreply = [126, 0, 14, 136, 0, 86, 76, 0, 86, 76, 32, 82, 101, 115, 117, 108, 116, 148] + +# Do some basic tests +up = zb.Packets(s) +up.process(goodtest) +up.process(badtest) +up.process(adctest) +p = up.pktq.pop(0) +assert(p.sender == 0x1 and p.rssi == -36 and p.nsamples == 1 and p.mask == 0xf) + +p = up.pktq.pop(0) +assert(p.sender == 0x5 and p.rssi == -36 and p.nsamples == 1 and p.mask == 0x20e) + +assert len(up.pktq) == 0 + +assert(up.rx_cnt == 2) +assert(up.ck_err == 1) +assert(up.fr_err == 0) +