From e036444cf364d5189d33f914a0ee5a08ad2bc145 Mon Sep 17 00:00:00 2001 From: jaseg Date: Thu, 21 Mar 2013 00:21:27 +0100 Subject: [PATCH] Implemented multi-master support This is not yet tested on the actual hardware, though I got all the unit tests working. --- avr/Makefile | 2 +- common/Makefile | 9 -- common/comm.h | 4 +- common/comm_handle.h | 33 +++-- common/main.c | 1 + generator.py | 38 +++--- pylibcerebrum/ganglion.py | 107 ++++++---------- pylibcerebrum/serial_mux.py | 71 +++++++++++ pylibcerebrum/test.py | 255 +++++++++++++++++++++---------------- pylibcerebrum/timeout_exception.py | 4 + test/Makefile | 2 +- test/uart.c | 4 + 12 files changed, 308 insertions(+), 222 deletions(-) delete mode 100644 common/Makefile create mode 100644 pylibcerebrum/serial_mux.py rewrite pylibcerebrum/test.py (97%) create mode 100644 pylibcerebrum/timeout_exception.py diff --git a/avr/Makefile b/avr/Makefile index 9f59913..fdf3bd2 100644 --- a/avr/Makefile +++ b/avr/Makefile @@ -3,7 +3,7 @@ all: objects objects: autocode.c config.c uart.c ../common/main.c ../common/comm.c - avr-gcc -Wall -fshort-enums -fno-inline-small-functions -fpack-struct -Wall -fno-strict-aliasing -funsigned-char -funsigned-bitfields -ffunction-sections -mmcu=${MCU} -DFDEV_SETUP_STREAM -DF_CPU=${CLOCK} -DCEREBRUM_BAUDRATE=${CEREBRUM_BAUDRATE} -std=gnu99 -Os -o main.elf -Wl,--gc-sections,--relax -I . -I../common $^ + avr-gcc -Wall -fshort-enums -fno-inline-small-functions -fpack-struct -Wall -fno-strict-aliasing -funsigned-char -funsigned-bitfields -ffunction-sections -mmcu=${MCU} -DFDEV_SETUP_STREAM -DF_CPU=${CLOCK} -DCEREBRUM_BAUDRATE=${CEREBRUM_BAUDRATE} -DCONFIG_ADDRESS=${CONFIG_ADDRESS} -std=gnu99 -Os -o main.elf -Wl,--gc-sections,--relax -I . -I../common $^ avr-objcopy -O ihex main.elf main.hex avr-size main.elf diff --git a/common/Makefile b/common/Makefile deleted file mode 100644 index 482eae3..0000000 --- a/common/Makefile +++ /dev/null @@ -1,9 +0,0 @@ - -all: comm-test - -comm-test: - @gcc -std=gnu99 -D__TEST__ -o comm-test comm.c main.c uart.c autocode.c config.c - -clean: - rm comm-test - diff --git a/common/comm.h b/common/comm.h index b8457f3..dcf3e9a 100644 --- a/common/comm.h +++ b/common/comm.h @@ -20,7 +20,7 @@ struct comm_callback_descriptor { uint16_t argbuf_len; }; typedef struct { - comm_callback_descriptor* descriptor; + comm_callback_descriptor const * descriptor; void* argbuf_end; } callback_stack_t; @@ -39,6 +39,8 @@ extern const volatile uint8_t global_argbuf[]; #endif #endif +#define ADDRESS_DISCOVERY 0xFFFF + void comm_loop(void); #endif//__COMM_H__ diff --git a/common/comm_handle.h b/common/comm_handle.h index f66f303..7abaae9 100644 --- a/common/comm_handle.h +++ b/common/comm_handle.h @@ -8,7 +8,8 @@ #define comm_debug_print(...) //fprintf(stderr, __VA_ARGS__) #define comm_debug_print2(...) //fprintf(stderr, __VA_ARGS__) #else//__TEST__ -#define htobe16(...) (__VA_ARGS__) +//AVR/MSP430 targets +#define be16toh(i) ((i>>8)|(i<<8)) #define comm_debug_print(...) #define comm_debug_print2(...) #endif//__TEST__ @@ -19,13 +20,14 @@ static inline void comm_handle(uint8_t c){ uint8_t escaped:1; } state_t; typedef struct { + uint16_t node_id; uint16_t funcid; uint16_t arglen; } args_t; static state_t state; static void* argbuf; static void* argbuf_end; - static comm_callback_descriptor* current_callback; + static comm_callback_descriptor const * current_callback; args_t* args = (args_t*)global_argbuf; #define ARGS_END (((uint8_t*)(args))+sizeof(args_t)) if(state.escaped){ @@ -53,17 +55,32 @@ static inline void comm_handle(uint8_t c){ if(argbuf == argbuf_end){ if(argbuf_end == ARGS_END){ comm_debug_print("[DEBUG] received the header\n"); - if(htobe16(args->funcid) >= callback_count){ //only jump to valid callbacks. - comm_debug_print("[DEBUG] invalid callback: funcid=0x%x given, callback_count=0x%x\n", htobe16(args->funcid), callback_count); + uint16_t addr = be16toh(args->node_id); + if(addr != CONFIG_ADDRESS){ + if(addr == ADDRESS_DISCOVERY){ + //With a packet addressed to the discovery address a master may discover the nodes on the bus. + //Here the funcid and arglen fields are abused as a "selector" to describe the nodes answering + //to this request. The selector works just like a network address/netmask pair in IP, + //the numeric (e.g. /8) netmask being in arglen and the network address in funcid. + if((CONFIG_ADDRESS & (0xFFFF>>be16toh(args->arglen))) == be16toh(args->funcid)){ + //Send a "I'm here!"-response. + uart_putc_nonblocking(0xFF); + } + } + state.receiving = 0; + return; + } + if(be16toh(args->funcid) >= callback_count){ //only jump to valid callbacks. + comm_debug_print("[DEBUG] invalid callback: funcid=0x%x given, callback_count=0x%x\n", be16toh(args->funcid), callback_count); state.receiving = 0; //return to idle state return; } comm_debug_print("[DEBUG] getting the callback\n"); - argbuf = comm_callbacks[htobe16(args->funcid)].argbuf; - current_callback = comm_callbacks + htobe16(args->funcid); + argbuf = comm_callbacks[be16toh(args->funcid)].argbuf; + current_callback = comm_callbacks + be16toh(args->funcid); uint16_t len = current_callback->argbuf_len; - if(htobe16(args->arglen) <= len){ - len = htobe16(args->arglen); + if(be16toh(args->arglen) <= len){ + len = be16toh(args->arglen); } argbuf_end = argbuf+len; if(argbuf != argbuf_end){ diff --git a/common/main.c b/common/main.c index 98fad59..ff05bc3 100644 --- a/common/main.c +++ b/common/main.c @@ -29,6 +29,7 @@ int main(void){ int16_t v; while((v = getchar()) >= 0){ comm_handle(v); + loop_auto(); } #else for(;;) loop_auto(); diff --git a/generator.py b/generator.py index ddd6e95..5ba604a 100644 --- a/generator.py +++ b/generator.py @@ -7,6 +7,7 @@ import subprocess import os.path import time +import random from threading import Thread import struct from inspect import isfunction @@ -113,16 +114,17 @@ config_c_template = """\ #endif unsigned int auto_config_descriptor_length = ${desc_len}; -const char auto_config_descriptor[] PROGMEM = {${desc}}; +char const auto_config_descriptor[] PROGMEM = {${desc}}; """ #FIXME possibly make a class out of this one #FIXME I think the target parameter is not used anywhere. Remove? -def generate(desc, device, build_path, builddate, target = 'all'): +def generate(desc, device, build_path, builddate, target = 'all', config_address=None): members = desc["members"] seqnum = 23 #module number (only used during build time to generate unique names) current_id = 0 desc["builddate"] = str(builddate) + config_address = config_address or random.randint(0, 65534) autocode = Template(autocode_header).render_unicode(version=desc["version"], builddate=builddate) init_functions = [] loop_functions = [] @@ -281,6 +283,7 @@ def generate(desc, device, build_path, builddate, target = 'all'): make_env['MCU'] = device.get('mcu') make_env['CLOCK'] = str(device.get('clock')) make_env['CEREBRUM_BAUDRATE'] = str(device.get('cerebrum_baudrate')) + make_env['CONFIG_ADDRESS'] = str(config_address) #65535 is reserved as discovery address subprocess.check_call(['/usr/bin/env', 'make', '--no-print-directory', '-C', build_path, 'clean', target], env=make_env) return desc @@ -300,19 +303,12 @@ class TestBuild(unittest.TestCase): pass def test_basic_build(self): - generate({'members': {}, 'version': 0.17}, {'mcu': 'test'}, 'test', '2012-05-23 23:42:17') - -def compareJSON(bytesa, bytesb): - jsona = json.JSONDecoder().decode(str(bytesa, "ASCII")) - normstra = bytes(json.JSONEncoder(separators=(',',':')).encode(jsona), 'ASCII') - jsonb = json.JSONDecoder().decode(str(bytesb, "ASCII")) - normstrb = bytes(json.JSONEncoder(separators=(',',':')).encode(jsonb), 'ASCII') - return normstra == normstrb + generate({'members': {}, 'version': 0.17}, {'mcu': 'test'}, 'test', '2012-05-23 23:42:17', config_address=0x2342) class TestCommStuff(unittest.TestCase): def setUp(self): - generate({'members': {'test': {'type': 'test'}}, 'version': 0.17}, {'mcu': 'test'}, 'test', '2012-05-23 23:42:17') + generate({'members': {'test': {'type': 'test'}}, 'version': 0.17}, {'mcu': 'test'}, 'test', '2012-05-23 23:42:17', config_address=0x2342) def new_test_process(self): #spawn a new communication test process @@ -333,7 +329,7 @@ class TestCommStuff(unittest.TestCase): def test_config_descriptor(self): (p, stdin, stdout, t) = self.new_test_process(); - stdin.write(b'\\#\x00\x00\x00\x00') + stdin.write(b'\\#\x23\x42\x00\x00\x00\x00') stdin.flush() stdin.close() @@ -349,7 +345,7 @@ class TestCommStuff(unittest.TestCase): def test_multipart_call(self): (p, stdin, stdout, t) = self.new_test_process(); - stdin.write(b'\\#\x00\x01\x00\x41AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA') + stdin.write(b'\\#\x23\x42\x00\x01\x00\x41AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA') stdin.flush() stdin.close() @@ -361,7 +357,7 @@ class TestCommStuff(unittest.TestCase): """Test whether the test function actually fails when given invalid data.""" (p, stdin, stdout, t) = self.new_test_process(); - stdin.write(b'\\#\x00\x01\x00\x41AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAA') + stdin.write(b'\\#\x23\x42\x00\x01\x00\x41AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAA') stdin.flush() stdin.close() @@ -372,8 +368,8 @@ class TestCommStuff(unittest.TestCase): def test_multipart_call_long_args(self): (p, stdin, stdout, t) = self.new_test_process(); - stdin.write(b'\\#\x00\x05\x01\x01'+b'A'*257) - stdin.write(b'\\#\x00\x06\x00\x00') + stdin.write(b'\\#\x23\x42\x00\x05\x01\x01'+b'A'*257) + stdin.write(b'\\#\x23\x42\x00\x06\x00\x00') stdin.flush() stdin.close() @@ -386,7 +382,7 @@ class TestCommStuff(unittest.TestCase): """Test whether the test function actually fails when given invalid data.""" (p, stdin, stdout, t) = self.new_test_process(); - stdin.write(b'\\#\x00\x05\x01\x01'+b'A'*128+b'B'+b'A'*128) + stdin.write(b'\\#\x23\x42\x00\x05\x01\x01'+b'A'*128+b'B'+b'A'*128) stdin.flush() stdin.close() @@ -397,8 +393,8 @@ class TestCommStuff(unittest.TestCase): def test_attribute_accessors_multipart(self): (p, stdin, stdout, t) = self.new_test_process(); - stdin.write(b'\\#\x00\x03\x01\x01'+b'A'*32+b'B'*32+b'C'*32+b'D'*32+b'E'*32+b'F'*32+b'G'*32+b'H'*32+b'I') # write some characters to test_buffer - stdin.write(b'\\#\x00\x04\x00\x00') # call check_test_buffer + stdin.write(b'\\#\x23\x42\x00\x03\x01\x01'+b'A'*32+b'B'*32+b'C'*32+b'D'*32+b'E'*32+b'F'*32+b'G'*32+b'H'*32+b'I') # write some characters to test_buffer + stdin.write(b'\\#\x23\x42\x00\x04\x00\x00') # call check_test_buffer stdin.flush() stdin.close() @@ -409,8 +405,8 @@ class TestCommStuff(unittest.TestCase): def test_meta_attribute_accessors_multipart(self): (p, stdin, stdout, t) = self.new_test_process(); - stdin.write(b'\\#\x00\x03\x01\x01'+b'A'*33+b'B'*31+b'C'*32+b'D'*32+b'E'*32+b'F'*32+b'G'*32+b'H'*32+b'I') # write some characters to test_buffer - stdin.write(b'\\#\x00\x04\x00\x00') # call check_test_buffer + stdin.write(b'\\#\x23\x42\x00\x03\x01\x01'+b'A'*33+b'B'*31+b'C'*32+b'D'*32+b'E'*32+b'F'*32+b'G'*32+b'H'*32+b'I') # write some characters to test_buffer + stdin.write(b'\\#\x23\x42\x00\x04\x00\x00') # call check_test_buffer stdin.flush() stdin.close() diff --git a/pylibcerebrum/ganglion.py b/pylibcerebrum/ganglion.py index 3994498..ac5a3b1 100644 --- a/pylibcerebrum/ganglion.py +++ b/pylibcerebrum/ganglion.py @@ -6,7 +6,6 @@ #modify it under the terms of the GNU General Public License #version 3 as published by the Free Software Foundation. -import serial import json import struct try: @@ -14,20 +13,19 @@ try: except: import pylzma as lzma import time +import serial from pylibcerebrum.NotifyList import NotifyList +from pylibcerebrum.timeout_exception import TimeoutException """Call RPC functions on serially connected devices over the Cerebrum protocol.""" -class TimeoutException(Exception): - pass - class Ganglion(object): """Proxy class for calling remote methods on hardware connected through a serial port using the Cerebrum protocol""" # NOTE: the device config is *not* the stuff from the config "dev" section but #read from the device. It can also be found in that [devicename].config.json #file created by the code generator - def __init__(self, device=None, baudrate=115200, jsonconfig=None, ser=None): + def __init__(self, node_id, jsonconfig=None, ser=None): """Ganglion constructor Keyword arguments: @@ -36,18 +34,10 @@ class Ganglion(object): The other keyword arguments are for internal use only. """ - # get a config - object.__setattr__(self, '_opened_ser', None) #This must be set here so in case of an error __del__ does not end in infinite recursion - if ser is None: - assert(jsonconfig is None) - s = serial.Serial(port=device, baudrate=baudrate, timeout=1) - #Trust me, without the following two lines it *wont* *work*. Fuck serial ports. - s.setXonXoff(True) - s.setXonXoff(False) - s.setDTR(True) - s.setDTR(False) - object.__setattr__(self, '_opened_ser', s) - object.__setattr__(self, '_ser', s) + object.__setattr__(self, '_ser', ser) + object.__setattr__(self, 'node_id', node_id) + if not jsonconfig: + # get a config i=0 while True: try: @@ -61,81 +51,62 @@ class Ganglion(object): i += 1 if i > 20: raise serial.serialutil.SerialException('Could not connect, giving up after 20 tries') - else: - assert(device is None) - object.__setattr__(self, '_ser', ser) # populate the object object.__setattr__(self, 'members', {}) for name, member in jsonconfig.get('members', {}).items(): - self.members[name] = Ganglion(jsonconfig=member, ser=self._ser) + self.members[name] = Ganglion(node_id, jsonconfig=member, ser=self._ser) object.__setattr__(self, 'properties', {}) for name, prop in jsonconfig.get('properties', {}).items(): self.properties[name] = (prop['id'], prop['fmt'], prop.get('access', 'rw')) object.__setattr__(self, 'functions', {}) for name, func in jsonconfig.get('functions', {}).items(): def proxy_method(*args): - return self._callfunc(func["id"], fun.get("args", ""), args, func.get("returns", "")) - self.functions[name] = func + return self._callfunc(func["id"], func.get("args", ""), args, func.get("returns", "")) + self.functions[name] = proxy_method object.__setattr__(self, 'type', jsonconfig.get('type', None)) object.__setattr__(self, 'config', { k: v for k,v in jsonconfig.items() if not k in ['members', 'properties', 'functions'] }) - def __del__(self): - self.close() - - def __exit__(self, exception_type, exception_val, trace): - self.close() - - def close(self): - """Close the serial port.""" - if self._opened_ser: - self._opened_ser.close() - def __iter__(self): """Construct an iterator to iterate over *all* (direct or not) child nodes of this node.""" return GanglionIter(self) - def _my_ser_read(self, n): - """Read n bytes from the serial device and raise a TimeoutException in case of a timeout.""" - data = self._ser.read(n) - if len(data) != n: - raise TimeoutException('Read {} bytes trying to read {}'.format(len(data), n)) - return data - def _read_config(self): """Fetch the device configuration descriptor from the device.""" - self._ser.write(b'\\#\x00\x00\x00\x00') - (clen,) = struct.unpack(">H", self._my_ser_read(2)) - cbytes = self._my_ser_read(clen) - #decide whether cbytes contains lzma or json depending on the first byte (which is used as a magic here) - if cbytes[0] is ord('#'): - return json.JSONDecoder().decode(str(lzma.decompress(cbytes[1:]), "utf-8")) - else: - return json.JSONDecoder().decode(str(cbytes, "utf-8")) + with self._ser as s: + s.write(b'\\#' + struct.pack(">H", self.node_id) + b'\x00\x00\x00\x00') + (clen,) = struct.unpack(">H", s.read(2)) + cbytes = s.read(clen) + #decide whether cbytes contains lzma or json depending on the first byte (which is used as a magic here) + if cbytes[0] is ord('#'): + return json.JSONDecoder().decode(str(lzma.decompress(cbytes[1:]), "utf-8")) + else: + return json.JSONDecoder().decode(str(cbytes, "utf-8")) def _callfunc(self, fid, argsfmt, args, retfmt): """Call a function on the device by id, directly passing argument/return format parameters.""" # Make a list out of the arguments if they are none if not (isinstance(args, tuple) or isinstance(args, list)): args = [args] - # Send the encoded data - cmd = b'\\#' + struct.pack("H", self._my_ser_read(2)) - # payload data - cbytes = self._my_ser_read(clen) - if clen != struct.calcsize(retfmt): - # CAUTION! This error is thrown not because the user supplied a wrong value but because the device answered in an unexpected manner. - # FIXME raise an error here or let the whole operation just fail in the following struct.unpack? - raise AttributeError("Device response format problem: Length mismatch: {} != {}".format(clen, struct.calcsize(retfmt))) - rv = struct.unpack(retfmt, cbytes) - # Try to interpret the return value in a useful manner - if len(rv) == 0: - return None - elif len(rv) == 1: - return rv[0] - else: - return list(rv) + with self._ser as s: + # Send the encoded data + cmd = b'\\#' + struct.pack(">HHH", self.node_id, fid, struct.calcsize(argsfmt)) + struct.pack(argsfmt, *args) + s.write(cmd) + # payload length + (clen,) = struct.unpack(">H", s.read(2)) + # payload data + cbytes = s.read(clen) + if clen != struct.calcsize(retfmt): + # CAUTION! This error is thrown not because the user supplied a wrong value but because the device answered in an unexpected manner. + # FIXME raise an error here or let the whole operation just fail in the following struct.unpack? + raise AttributeError("Device response format problem: Length mismatch: {} != {}".format(clen, struct.calcsize(retfmt))) + rv = struct.unpack(retfmt, cbytes) + # Try to interpret the return value in a useful manner + if len(rv) == 0: + return None + elif len(rv) == 1: + return rv[0] + else: + return list(rv) def __dir__(self): """Get a list of all attributes of this object. This includes virtual Cerebrum stuff like members, properties and functions.""" diff --git a/pylibcerebrum/serial_mux.py b/pylibcerebrum/serial_mux.py new file mode 100644 index 0000000..f8bd06f --- /dev/null +++ b/pylibcerebrum/serial_mux.py @@ -0,0 +1,71 @@ + +import serial +import threading +import struct +from pylibcerebrum.ganglion import Ganglion +from pylibcerebrum.timeout_exception import TimeoutException + +class SerialMux(object): + + def __init__(self, device=None, baudrate=115200, ser=None): + s = ser or LockableSerial(port=device, baudrate=baudrate, timeout=1) + #Trust me, without the following two lines it *wont* *work*. Fuck serial ports. + s.setXonXoff(True) + s.setXonXoff(False) + s.setDTR(True) + s.setDTR(False) + self.ser = s + + def open(self, node_id): + """ Open a Ganglion by node ID """ + return Ganglion(node_id, ser=self._ser) + + def discover(self, mask=0, address=0, found=[]): + """ Discover all node IDs connected to the bus + + Note: You do not need to set any of the arguments. These are used for the recursive discovery process. + """ + for a in [address, 1<HH', address, mask)) + timeout_tmp = s.timeout + s.timeout = 0.0005 + try: + s.read(1) + s.timeout = timeout_tmp + return True + except TimeoutException: + s.timeout = timeout_tmp + return False + + def __del__(self): + self.ser.close() + +class LockableSerial(serial.Serial): + def __init__(self, *args, **kwargs): + super(serial.Serial, self).__init__(*args, **kwargs) + self.lock = threading.RLock() + + def __enter__(self): + self.lock.__enter__() + return self + + def __exit__(self, *args): + self.lock.__exit__(*args) + + def read(self, n): + """Read n bytes from the serial device and raise a TimeoutException in case of a timeout.""" + data = serial.Serial.read(self, n) + if len(data) != n: + raise TimeoutException('Read {} bytes trying to read {}'.format(len(data), n)) + return data + diff --git a/pylibcerebrum/test.py b/pylibcerebrum/test.py dissimilarity index 97% index 7c5ecd6..77778df 100644 --- a/pylibcerebrum/test.py +++ b/pylibcerebrum/test.py @@ -1,113 +1,142 @@ -from pylibcerebrum.pylibcerebrum import Ganglion -import unittest -import serial -import generator - -class TestGanglion(generator.TestCommStuff): - - def setUp(self): - super(TestGanglion, self).setUp() - - def test_connect(self): - fs = FakeSerial() - #fs.inp += b'\x00K]\x00\x00\x80\x00\x00=\x88\x8a\xc6\x94S\x90\x86\xa6c}%:\xbbAj\x14L\xd9\x1a\xae\x93n\r\x10\x83E1\xba]j\xdeG\xb1\xba\xa6[:\xa2\xb9\x8eR~#\xb9\x84%\xa0#q\x87\x17[\xd6\xcdA)J{\xab*\xf7\x96%\xff\xfa\x12g\x00' - fs.inp += b'\x00\x3F{"version":0.17,"builddate":"2012-05-23 23:42:17","members":{}}' - - g = Ganglion(ser=fs) - g.config = g._read_config() - self.assertEqual(fs.out, b'\\#\x00\x00\x00\x00', 'The ganglion sent garbage trying to read the device config.') - self.assert_('version' in g.config, 'The ganglion has an invalid config without a version attribute') - self.assertEqual(g.config['version'], 0.17, 'The ganglion\'s config\'s version attribute is wrong') - self.assert_('members' in g.config, 'The ganglion has an invalid config without a \'members\' attribute') - self.assertEqual(g.config['members'], {}, 'The ganglion\'s config\'s memers attribute is not an empty hash') - - def test_simple_callback_invocation(self): - fs = FakeSerial() - g = Ganglion(ser=fs) - g._config = {'version': 0.17, 'builddate': '2012-05-23 23:42:17', 'members': {"foo": {"type": "test", "functions": {"callback": {"id": 1}}}}} - #put the device's response into the input of the ganglion - fs.inp += b'\x00\x00\x00\x00' - #access the attribute - g.foo.callback() - self.assertEqual(fs.out, b'\\#\x00\x01\x00\x00', 'Somehow pylibcerebrum sent a wrong command to the device.') - - def test_complex_callback_invocation(self): - fs = FakeSerial() - g = Ganglion(ser=fs) - g._config = {'version': 0.17, 'builddate': '2012-05-23 23:42:17', 'members': {"foo": {"type": "test", "functions": {"callback": {"id": 1, "args": "3B", "returns": "3B"}}}}} - #put the device's response into the input of the ganglion - fs.inp += b'\x00\x03ABC' - #access the attribute - foo = g.foo.callback(0x44, 0x45, 0x46) - self.assertEqual(fs.out, b'\\#\x00\x01\x00\x03DEF', 'Somehow pylibcerebrum sent a wrong command to the device.') - self.assertEqual(foo, (0x41, 0x42, 0x43), 'Somehow a device response was decoded wrong.') - - def test_attribute_read(self): - fs = FakeSerial() - g = Ganglion(ser=fs) - g._config = {'version': 0.17, 'builddate': '2012-05-23 23:42:17', 'members': {"foo": {"type": "test", "properties": {"prop": {"fmt": "B", "id": 1, "size": 1}}}}} - #put the device's response into the input of the ganglion - fs.inp += b'\x00\x01\x41' - #access the attribute - (foo,) = g.foo.prop - self.assertEqual(fs.out, b'\\#\x00\x01\x00\x00', 'Somehow pylibcerebrum sent a wrong command to the device.') - self.assertEqual(foo, 0x41, 'Somehow a device response was decoded wrong.') - - def test_attribute_read_long(self): - #This reads a rather large attribute (>64 bytes) in order to catch problems with multipart callbacks. - fs = FakeSerial() - g = Ganglion(ser=fs) - g._config = {'version': 0.17, 'builddate': '2012-05-23 23:42:17', 'members': {"foo": {"type": "test", "properties": {"prop": {"fmt": "65B", "id": 1, "size": 65}}}}} - #put the device's response into the input of the ganglion - fs.inp += b'\x00\x41AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' - #access the attribute - foo = g.foo.prop - self.assertEqual(fs.out, b'\\#\x00\x01\x00\x00', 'Somehow pylibcerebrum sent a wrong command to the device.') - self.assertEqual(foo, (0x41,)*65, 'Somehow a device response was decoded wrong.') - - def test_attribute_write(self): - fs = FakeSerial() - g = Ganglion(ser=fs) - g._config = {'version': 0.17, 'builddate': '2012-05-23 23:42:17', 'members': {"foo": {"type": "test", "properties": {"prop": {"fmt": "B", "id": 1, "size": 1}}}}} - #put the device's response into the input of the ganglion - fs.inp += b'\x00\x00' - #access the attribute - g.foo.prop = 0x41 - self.assertEqual(fs.out, b'\\#\x00\x02\x00\x01\x41', 'Somehow pylibcerebrum sent a wrong command to the device.') - - def test_attribute_write_long(self): - #This writes a rather large attribute (>64 bytes) in order to catch problems with multipart callbacks. - fs = FakeSerial() - g = Ganglion(ser=fs) - g._config = {'version': 0.17, 'builddate': '2012-05-23 23:42:17', 'members': {"foo": {"type": "test", "properties": {"prop": {"fmt": "257B", "id": 1, "size": 0x101}}}}} - #put the device's response into the input of the ganglion - fs.inp += b'\x00\x00' - #access the attribute - g.foo.prop = (0x41,)*0x101 - self.assertEqual(fs.out, b'\\#\x00\x02\x01\x01'+b'A'*0x101, 'Somehow pylibcerebrum sent a wrong command to the device.') - pass - - def test_attribute_forbidden_write(self): - fs = FakeSerial() - g = Ganglion(ser=fs) - g._config = {'version': 0.17, 'builddate': '2012-05-23 23:42:17', 'members': {"foo": {"type": "test", "properties": {"prop": {"fmt": "B", "id": 1, "size": 1, "access": "r"}}}}} - #access the attribute - with self.assertRaises(TypeError, msg="prop is a read-only property"): - g.foo.prop = 0x41 - -class FakeSerial: - - def __init__(self): - self.out = b'' - self.inp = b'' - - def read(self, n): - r = self.inp[0:n] - self.inp = self.inp[n:] - return r - - def write(self, bs): - if not isinstance(bs, bytes): - raise ArgumentError('FakeSerial.write only accepts -bytes-') - self.out += bs - +from pylibcerebrum.ganglion import Ganglion +from pylibcerebrum.serial_mux import SerialMux +from pylibcerebrum.timeout_exception import TimeoutException +import unittest +import serial +import generator + +class TestGanglion(generator.TestCommStuff): + + def setUp(self): + super(TestGanglion, self).setUp() + + def test_connect(self): + fs = FakeSerial() + #fs.inp += b'\x00K]\x00\x00\x80\x00\x00=\x88\x8a\xc6\x94S\x90\x86\xa6c}%:\xbbAj\x14L\xd9\x1a\xae\x93n\r\x10\x83E1\xba]j\xdeG\xb1\xba\xa6[:\xa2\xb9\x8eR~#\xb9\x84%\xa0#q\x87\x17[\xd6\xcdA)J{\xab*\xf7\x96%\xff\xfa\x12g\x00' + fs.inp += b'\x00\x3F{"version":0.17,"builddate":"2012-05-23 23:42:17","members":{}}' + + g = Ganglion(0x2342, ser=fs) + self.assertEqual(fs.out, b'\\#\x23\x42\x00\x00\x00\x00', 'The ganglion sent garbage trying to read the device config.') + self.assert_('version' in g.config, 'The ganglion has an invalid config without a version attribute') + self.assertEqual(g.config['version'], 0.17, 'The ganglion\'s config\'s version attribute is wrong') + + def test_simple_callback_invocation(self): + fs = FakeSerial() + g = Ganglion(0x2342, ser=fs, jsonconfig = {'version': 0.17, 'builddate': '2012-05-23 23:42:17', 'members': {"foo": {"type": "test", "functions": {"callback": {"id": 1}}}}}) + #put the device's response into the input of the ganglion + fs.inp += b'\x00\x00\x00\x00' + #access the attribute + g.foo.callback() + self.assertEqual(fs.out, b'\\#\x23\x42\x00\x01\x00\x00', 'Somehow pylibcerebrum sent a wrong command to the device.') + + def test_complex_callback_invocation(self): + fs = FakeSerial() + g = Ganglion(0x2342, ser=fs, jsonconfig = {'version': 0.17, 'builddate': '2012-05-23 23:42:17', 'members': {"foo": {"type": "test", "functions": {"callback": {"id": 1, "args": "3B", "returns": "3B"}}}}}) + #put the device's response into the input of the ganglion + fs.inp += b'\x00\x03ABC' + #access the attribute + foo = g.foo.callback(0x44, 0x45, 0x46) + self.assertEqual(fs.out, b'\\#\x23\x42\x00\x01\x00\x03DEF', 'Somehow pylibcerebrum sent a wrong command to the device.') + self.assertEqual(foo, [0x41, 0x42, 0x43], 'Somehow a device response was decoded wrong.') + + def test_attribute_read(self): + fs = FakeSerial() + g = Ganglion(0x2342, ser=fs, jsonconfig = {'version': 0.17, 'builddate': '2012-05-23 23:42:17', 'members': {"foo": {"type": "test", "properties": {"prop": {"fmt": "B", "id": 1, "size": 1}}}}}) + #put the device's response into the input of the ganglion + fs.inp += b'\x00\x01\x41' + #access the attribute + foo = g.foo.prop + self.assertEqual(fs.out, b'\\#\x23\x42\x00\x01\x00\x00', 'Somehow pylibcerebrum sent a wrong command to the device.') + self.assertEqual(foo, 0x41, 'Somehow a device response was decoded wrong.') + + def test_attribute_read_long(self): + #This reads a rather large attribute (>64 bytes) in order to catch problems with multipart callbacks. + fs = FakeSerial() + g = Ganglion(0x2342, ser=fs, jsonconfig = {'version': 0.17, 'builddate': '2012-05-23 23:42:17', 'members': {"foo": {"type": "test", "properties": {"prop": {"fmt": "65B", "id": 1, "size": 65}}}}}) + #put the device's response into the input of the ganglion + fs.inp += b'\x00\x41AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + #access the attribute + foo = g.foo.prop + self.assertEqual(fs.out, b'\\#\x23\x42\x00\x01\x00\x00', 'Somehow pylibcerebrum sent a wrong command to the device.') + self.assertEqual(foo, [0x41]*65, 'Somehow a device response was decoded wrong.') + + def test_attribute_write(self): + fs = FakeSerial() + g = Ganglion(0x2342, ser=fs, jsonconfig = {'version': 0.17, 'builddate': '2012-05-23 23:42:17', 'members': {"foo": {"type": "test", "properties": {"prop": {"fmt": "B", "id": 1, "size": 1}}}}}) + #put the device's response into the input of the ganglion + fs.inp += b'\x00\x00' + #access the attribute + g.foo.prop = 0x41 + self.assertEqual(fs.out, b'\\#\x23\x42\x00\x02\x00\x01\x41', 'Somehow pylibcerebrum sent a wrong command to the device.') + + def test_attribute_write_long(self): + #This writes a rather large attribute (>64 bytes) in order to catch problems with multipart callbacks. + fs = FakeSerial() + g = Ganglion(0x2342, ser=fs, jsonconfig = {'version': 0.17, 'builddate': '2012-05-23 23:42:17', 'members': {"foo": {"type": "test", "properties": {"prop": {"fmt": "257B", "id": 1, "size": 0x101}}}}}) + #put the device's response into the input of the ganglion + fs.inp += b'\x00\x00' + #access the attribute + g.foo.prop = (0x41,)*0x101 + self.assertEqual(fs.out, b'\\#\x23\x42\x00\x02\x01\x01'+b'A'*0x101, 'Somehow pylibcerebrum sent a wrong command to the device.') + pass + + def test_attribute_forbidden_write(self): + fs = FakeSerial() + g = Ganglion(0x2342, ser=fs, jsonconfig = {'version': 0.17, 'builddate': '2012-05-23 23:42:17', 'members': {"foo": {"type": "test", "properties": {"prop": {"fmt": "B", "id": 1, "size": 1, "access": "r"}}}}}) + #access the attribute + with self.assertRaises(TypeError, msg="prop is a read-only property"): + g.foo.prop = 0x41 + +class TestMux(unittest.TestCase): + def test_probe(self): + fs = FakeSerial() + m = SerialMux(ser=fs) + fs.inp += b'\xFF' + self.assertTrue(m._send_probe(0x2342, 5)) + self.assertEqual(fs.out, b'\\#\xFF\xFF\x23\x42\x00\x05') + self.assertFalse(m._send_probe(0x2342, 5)) + + def test_discovery(self): + fs = FakeSerial() + m = SerialMux(ser=fs) + fs.inp += b'\xFF'*16 + self.assertEqual(m.discover(), [0]) + probepacket0 = lambda x: b'\\#\xFF\xFF\x00\x00'+x.to_bytes(2, 'big') + probepacket1 = lambda x: b'\\#\xFF\xFF'+(1<