Fixed an attribute bug throwing errors when closing a ganglion
[cerebrum.git] / generator.py
blobddd6e95eb9986ced88c13e17ce1b1a459067206a
1 #Copyright (C) 2012 jaseg <s@jaseg.de>
3 #This program is free software; you can redistribute it and/or
4 #modify it under the terms of the GNU General Public License
5 #version 3 as published by the Free Software Foundation.
7 import subprocess
8 import os.path
9 import time
10 from threading import Thread
11 import struct
12 from inspect import isfunction
13 from mako.template import Template
14 import binascii
15 import json
16 try:
17 import lzma
18 except:
19 import pylzma as lzma
20 import codecs
21 import unittest
22 """Automatic Cerebrum c code generator"""
24 # Code templates. Actually, this is the only device-dependent place in this whole
25 # file, and actually only a very few lines are device-dependent.
26 # FIXME: Break this stuff into a common "C code generator" which is included from
27 # here and from the msp code generator and which is feeded with the two or three
28 # device-dependent lines
30 autocode_header = """\
31 /* AUTOGENERATED CODE FOLLOWS!
32 * This file contains the code generated from the module templates as well as
33 * some glue logic. It is generated following the device config by "generate.py"
34 * in this very folder. Please refrain from modifying it, modify the templates
35 * and generation logic instead.
37 * Build version: ${version}, build date: ${builddate}
40 #include <string.h>
41 #include "autocode.h"
42 #include "comm.h"
43 #include "uart.h"
44 """
46 #This one contains the actual callback/init/loop magick.
47 autocode_footer = """
48 #include "config.h"
49 #if defined(__AVR__)
50 #include <avr/pgmspace.h>
51 #endif
53 void generic_getter_callback(const comm_callback_descriptor* cb, void* argbuf_end);
55 const comm_callback_descriptor comm_callbacks[] = {
56 % for (callback, argbuf, argbuf_len, id) in callbacks:
57 {${callback}, (void*)${argbuf}, ${argbuf_len}}, //${id}
58 % endfor
61 const uint16_t callback_count = (sizeof(comm_callbacks)/sizeof(comm_callback_descriptor)); //${len(callbacks)};
63 void init_auto(){
64 % for initfunc in init_functions:
65 ${initfunc}();
66 % endfor
69 void loop_auto(){
70 comm_loop();
71 % for loopfunc in loop_functions:
72 ${loopfunc}();
73 % endfor
76 void callback_get_descriptor_auto(const comm_callback_descriptor* cb, void* argbuf_end){
77 //FIXME
78 uart_putc(auto_config_descriptor_length >> 8);
79 uart_putc(auto_config_descriptor_length & 0xFF);
80 for(const char* i=auto_config_descriptor; i < auto_config_descriptor+auto_config_descriptor_length; i++){
81 #if defined(__AVR__)
82 uart_putc(pgm_read_byte(i));
83 #else
84 uart_putc(*i);
85 #endif
89 //Generic getter used for any readable parameters.
90 //Please note one curious thing: This callback can not only be used to read, but also to write a variable. The only
91 //difference between the setter and the getter of a variable is that the setter does not read the entire variable's
92 //contents aloud.
93 void generic_getter_callback(const comm_callback_descriptor* cb, void* argbuf_end){
94 //response length
95 uart_putc(cb->argbuf_len>>8);
96 uart_putc(cb->argbuf_len&0xFF);
97 //response
98 for(char* i=((char*)cb->argbuf); i<((char*)cb->argbuf)+cb->argbuf_len; i++){
99 uart_putc(*i);
105 config_c_template = """\
106 /* AUTOGENERATED CODE AHEAD!
107 * This file contains the device configuration in lzma-ed json-format. It is
108 * autogenerated by "generate.py" (which should be found in this folder).
110 #include "config.h"
111 #ifndef PROGMEM
112 #define PROGMEM
113 #endif
115 unsigned int auto_config_descriptor_length = ${desc_len};
116 const char auto_config_descriptor[] PROGMEM = {${desc}};
119 #FIXME possibly make a class out of this one
120 #FIXME I think the target parameter is not used anywhere. Remove?
121 def generate(desc, device, build_path, builddate, target = 'all'):
122 members = desc["members"]
123 seqnum = 23 #module number (only used during build time to generate unique names)
124 current_id = 0
125 desc["builddate"] = str(builddate)
126 autocode = Template(autocode_header).render_unicode(version=desc["version"], builddate=builddate)
127 init_functions = []
128 loop_functions = []
129 callbacks = []
131 def register_callback(name, argbuf="global_argbuf", argbuf_len="ARGBUF_SIZE"):
132 nonlocal current_id
133 callbacks.append(("0" if name is None else "&"+name, argbuf, argbuf_len, current_id))
134 old_id = current_id
135 current_id += 1
136 return old_id
138 #Default callback number 0
139 register_callback("callback_get_descriptor_auto")
141 for mname, member in members.items():
142 mfile = member["type"]
143 mtype = mfile.replace('-', '_')
144 typepath = os.path.join(build_path, mfile + ".c.tp")
146 #CAUTION! These *will* exhibit strange behavior when called more than once!
147 def init_function():
148 fun = "init_{}_{}".format(mtype, seqnum)
149 init_functions.append(fun)
150 return fun
151 def loop_function():
152 fun = "loop_{}_{}".format(mtype, seqnum)
153 loop_functions.append(fun)
154 return fun
156 #module instance build config entries
157 properties = {}
158 functions = {}
160 #FIXME possibly swap the positions of ctype and fmt
161 def modulevar(name, ctype=None, fmt=None, array=False, callbacks=(0, 0)):
162 """Get the c name of a module variable and possibly register the variable with the code generator.
164 If only `name` is given, the autogenerated c name of the module variable will be returned.
166 If you provide `fmt`, virtual accessor methods for the variable will be registered and the variable will
167 be registered as a property in the build config (using the previously mentioned accessor methods).
168 In this context, "virtual" means that there will be callbacks in the callback list, but the setter will
169 not be backed by an actual function and the getter will just point to a global generic getter.
171 If you also provide `ctype` the accessors will also be generated.
173 `array` can be used to generated accessors for module variables that are arrays.
175 `callbacks` can be a tuple of one or two values. Each value corresponds to one callback. If the tuple contains
176 only one value, no setter will be generated and the variable will be marked read-only. A value of `None` prompts
177 the generation of the "default" accessor function. A value of `True` prompts the registration of an accessor
178 function of the form `callback_(get|set)_${modulevar(name)}` whose argument is stored in the module variable
179 buffer itself and which you must implement yourself. You may also specify a tuple of the form `(cbname, buf, buflen)`
180 where `cbname` is the name of your callback and `buf` and `buflen` are the argument buffer and argument buffer length,
181 respectively.
183 varname = "modvar_{}_{}_{}".format(mtype, seqnum, name)
184 if fmt is not None:
185 aval = 1
186 if array != False:
187 aval = array
189 def accessor_callback(desc, cbtype, defcb):
190 if desc == 0:
191 return register_callback(defcb, ("" if array else "&")+varname, "sizeof("+varname+")")
192 elif desc == 1:
193 return register_callback("callback_{}_{}".format(cbtype, varname), ("" if array else "&")+varname, "sizeof("+varname+")")
194 elif desc == 2:
195 return register_callback("callback_{}_{}".format(cbtype, varname), "global_argbuf", "ARGBUF_SIZE")
196 else:
197 cbname, buf, buflen = desc
198 if cbname is True:
199 cbname = "callback_{}_{}".format(cbtype, varname)
200 return register_callback(cbname, buf, buflen)
202 properties[name] = {
203 "size": struct.calcsize(fmt),
204 "id": accessor_callback(callbacks[0], 'get', 'generic_getter_callback'),
205 "fmt": fmt}
207 if callbacks[1] is not None:
208 accessor_callback(callbacks[1], 'set', None)
209 else:
210 #Save some space in the build config (that later gets burned into the µC's
211 #really small flash!) by only putting this here in case of read-only access
212 properties[name]["access"] = 'r'
214 if ctype is not None:
215 array_component = ""
216 if array == True:
217 array_component = "[]"
218 elif array:
219 array_component = "[{}]".format(array)
220 return "{} {}{}".format(ctype, varname, array_component)
221 else:
222 assert(ctype is None)
224 return varname
226 def module_callback(name, argformat="", retformat="", regname=None):
227 """Register a regular module callback.
229 I hereby officially discourage the (sole) use of this function since these callbacks or functions as they
230 appear at the Cerebrum level cannot be automatically mapped to snmp MIBs in any sensible manner. Thus, please
231 use accessors for everything if possible, even if it is stuff that you would not traditionally use them for.
232 For an example on how to generate and register custom accessor methods please see simple-io.c.tp .
235 cbname = 'callback_{}_{}_{}'.format(mtype, seqnum, name)
236 cbid = register_callback(regname or cbname)
237 func = { 'id': cbid }
238 #Save some space in the build config (that later gets burned into the µC's really small flash!)
239 if argformat is not '':
240 func['args'] = argformat
241 if retformat is not '':
242 func['returns'] = retformat
243 functions[name] = func
244 return cbname
246 #Flesh out the module template!
247 tp = Template(filename=typepath)
248 autocode += tp.render_unicode(
249 init_function=init_function,
250 loop_function=loop_function,
251 modulevar=modulevar,
252 setter=lambda x: 'callback_set_'+modulevar(x),
253 getter=lambda x: 'callback_get_'+modulevar(x),
254 module_callback=module_callback,
255 register_callback=register_callback,
256 member=member,
257 device=device)
259 #Save some space in the build config (that later gets burned into the µC's really small flash!)
260 if functions:
261 member['functions'] = functions
262 if properties:
263 member['properties'] = properties
265 #increment the module number
266 seqnum += 1
268 #finish the code generation and write the generated code to a file
269 autocode += Template(autocode_footer).render_unicode(init_functions=init_functions, loop_functions=loop_functions, callbacks=callbacks)
270 with open(os.path.join(build_path, 'autocode.c'), 'w') as f:
271 f.write(autocode)
272 #compress the build config and write it out
273 #Depending on whether you want to store the device config as plain text or lzma'ed plain text comment out one of the following lines
274 #The first byte is used as a magic here. The first byte of a JSON string will always be a '{'
275 config = b'#' + lzma.compress(bytes(json.JSONEncoder(separators=(',',':')).encode(desc), 'utf-8'))
276 #config = bytes(json.JSONEncoder(separators=(',',':')).encode(desc), 'utf-8')
277 with open(os.path.join(build_path, 'config.c'), 'w') as f:
278 f.write(Template(config_c_template).render_unicode(desc_len=len(config), desc=','.join(map(str, config))))
279 #compile the whole stuff
280 make_env = os.environ.copy()
281 make_env['MCU'] = device.get('mcu')
282 make_env['CLOCK'] = str(device.get('clock'))
283 make_env['CEREBRUM_BAUDRATE'] = str(device.get('cerebrum_baudrate'))
284 subprocess.check_call(['/usr/bin/env', 'make', '--no-print-directory', '-C', build_path, 'clean', target], env=make_env)
286 return desc
288 def commit(device, build_path, args):
289 """Flash the newly generated firmware onto the device"""
290 make_env = os.environ.copy()
291 make_env['MCU'] = device.get('mcu')
292 make_env['PORT'] = args.port
293 make_env['PROGRAMMER'] = device.get('programmer')
294 make_env['PROGRAMMER_BAUDRATE'] = str(device.get('programmer_baudrate'))
295 subprocess.check_call(['/usr/bin/env', "make",'--no-print-directory', '-C', build_path, 'program'], env=make_env)
297 class TestBuild(unittest.TestCase):
299 def setUp(self):
300 pass
302 def test_basic_build(self):
303 generate({'members': {}, 'version': 0.17}, {'mcu': 'test'}, 'test', '2012-05-23 23:42:17')
305 def compareJSON(bytesa, bytesb):
306 jsona = json.JSONDecoder().decode(str(bytesa, "ASCII"))
307 normstra = bytes(json.JSONEncoder(separators=(',',':')).encode(jsona), 'ASCII')
308 jsonb = json.JSONDecoder().decode(str(bytesb, "ASCII"))
309 normstrb = bytes(json.JSONEncoder(separators=(',',':')).encode(jsonb), 'ASCII')
310 return normstra == normstrb
312 class TestCommStuff(unittest.TestCase):
314 def setUp(self):
315 generate({'members': {'test': {'type': 'test'}}, 'version': 0.17}, {'mcu': 'test'}, 'test', '2012-05-23 23:42:17')
317 def new_test_process(self):
318 #spawn a new communication test process
319 p = subprocess.Popen([os.path.join(os.path.dirname(__file__), 'test', 'main')], stdin=subprocess.PIPE, stdout=subprocess.PIPE)
321 #start a thread killing that process after a few seconds
322 def kill_subprocess():
323 time.sleep(5)
324 if p.poll() is None or p.returncode < 0:
325 p.terminate()
326 self.assert_(False, 'Communication test process terminated due to a timeout')
328 t = Thread(target=kill_subprocess)
329 t.daemon = True
330 t.start()
331 return (p, p.stdin, p.stdout, t)
333 def test_config_descriptor(self):
334 (p, stdin, stdout, t) = self.new_test_process();
336 stdin.write(b'\\#\x00\x00\x00\x00')
337 stdin.flush()
338 stdin.close()
340 (length,) = struct.unpack('>H', stdout.read(2))
341 #FIXME this fixed size comparision is *not* helpful.
342 #self.assertEqual(length, 227, 'Incorrect config descriptor length')
343 data = stdout.read(length)
344 #self.assertEqual(data, b']\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', 'wrong config descriptor returned')
345 #Somehow, each time this is compiled, the json encode shuffles the fields of the object in another way. Thus it does not suffice to compare the plain strings.
346 #self.assert_(compareJSON(data, b'{"version":0.17,"builddate":"2012-05-23 23:42:17","members":{"test":{"functions":{"test_multipart":{"args":"65B","id":1},"check_test_buffer":{"id":4}},"type":"test","properties":{"test_buffer":{"size":65,"id":2,"fmt":"65B"}}}}}'), "The generated test program returns a wrong config descriptor: {}.".format(data))
347 #FIXME somehow, this commented-out device descriptor check fails randomly even though nothing is actually wrong.
349 def test_multipart_call(self):
350 (p, stdin, stdout, t) = self.new_test_process();
352 stdin.write(b'\\#\x00\x01\x00\x41AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA')
353 stdin.flush()
354 stdin.close()
356 #wait for test process to terminate. If everything else fails, the timeout thread will kill it.
357 p.wait()
358 self.assertEqual(p.returncode, 0, "The test process caught an error from the c code. Please watch stderr for details.")
360 def test_meta_multipart_call(self):
361 """Test whether the test function actually fails when given invalid data."""
362 (p, stdin, stdout, t) = self.new_test_process();
364 stdin.write(b'\\#\x00\x01\x00\x41AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAA')
365 stdin.flush()
366 stdin.close()
368 #wait for test process to terminate. If everything else fails, the timeout thread will kill it.
369 p.wait()
370 self.assertEqual(p.returncode, 1, "The test process did not catch an error it was supposed to catch from the c code. Please watch stderr for details.")
372 def test_multipart_call_long_args(self):
373 (p, stdin, stdout, t) = self.new_test_process();
375 stdin.write(b'\\#\x00\x05\x01\x01'+b'A'*257)
376 stdin.write(b'\\#\x00\x06\x00\x00')
377 stdin.flush()
378 stdin.close()
380 #wait for test process to terminate. If everything else fails, the timeout thread will kill it.
381 p.wait()
382 self.assertEqual(p.returncode, 0, "The test process caught an error from the c code. Please watch stderr for details.")
383 self.assertEqual(p.returncode, 0, "The test process caught an error from the c code. Please watch stderr for details.")
385 def test_meta_multipart_call_long_args(self):
386 """Test whether the test function actually fails when given invalid data."""
387 (p, stdin, stdout, t) = self.new_test_process();
389 stdin.write(b'\\#\x00\x05\x01\x01'+b'A'*128+b'B'+b'A'*128)
390 stdin.flush()
391 stdin.close()
393 #wait for test process to terminate. If everything else fails, the timeout thread will kill it.
394 p.wait()
395 self.assertEqual(p.returncode, 1, "The test process did not catch an error it was supposed to catch from the c code. Please watch stderr for details.")
397 def test_attribute_accessors_multipart(self):
398 (p, stdin, stdout, t) = self.new_test_process();
400 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
401 stdin.write(b'\\#\x00\x04\x00\x00') # call check_test_buffer
402 stdin.flush()
403 stdin.close()
405 #wait for test process to terminate. If everything else fails, the timeout thread will kill it.
406 p.wait()
407 self.assertEqual(p.returncode, 0, "The test process caught an error from the c code. Please watch stderr for details.")
409 def test_meta_attribute_accessors_multipart(self):
410 (p, stdin, stdout, t) = self.new_test_process();
412 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
413 stdin.write(b'\\#\x00\x04\x00\x00') # call check_test_buffer
414 stdin.flush()
415 stdin.close()
417 #wait for test process to terminate. If everything else fails, the timeout thread will kill it.
418 p.wait()
419 self.assertEqual(p.returncode, 1, "The test process did not catch an error it was supposed to catch from the c code. Please watch stderr for details.")