Fixed ganglion callback return type handling
[cerebrum.git] / generator.py
blob789481cd4264192d4bd2e046081791c9291bb1ff
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 % for loopfunc in loop_functions:
71 ${loopfunc}();
72 % endfor
75 void callback_get_descriptor_auto(const comm_callback_descriptor* cb, void* argbuf_end){
76 //FIXME
77 uart_putc(auto_config_descriptor_length >> 8);
78 uart_putc(auto_config_descriptor_length & 0xFF);
79 for(const char* i=auto_config_descriptor; i < auto_config_descriptor+auto_config_descriptor_length; i++){
80 #if defined(__AVR__)
81 uart_putc(pgm_read_byte(i));
82 #else
83 uart_putc(*i);
84 #endif
88 //Generic getter used for any readable parameters.
89 //Please note one curious thing: This callback can not only be used to read, but also to write a variable. The only
90 //difference between the setter and the getter of a variable is that the setter does not read the entire variable's
91 //contents aloud.
92 void generic_getter_callback(const comm_callback_descriptor* cb, void* argbuf_end){
93 //response length
94 uart_putc(cb->argbuf_len>>8);
95 uart_putc(cb->argbuf_len&0xFF);
96 //response
97 for(char* i=((char*)cb->argbuf); i<((char*)cb->argbuf)+cb->argbuf_len; i++){
98 uart_putc(*i);
104 config_c_template = """\
105 /* AUTOGENERATED CODE AHEAD!
106 * This file contains the device configuration in lzma-ed json-format. It is
107 * autogenerated by "generate.py" (which should be found in this folder).
109 #include "config.h"
110 #ifndef PROGMEM
111 #define PROGMEM
112 #endif
114 unsigned int auto_config_descriptor_length = ${desc_len};
115 const char auto_config_descriptor[] PROGMEM = {${desc}};
118 #FIXME possibly make a class out of this one
119 #FIXME I think the target parameter is not used anywhere. Remove?
120 def generate(desc, device, build_path, builddate, target = 'all'):
121 members = desc["members"]
122 seqnum = 23 #module number (only used during build time to generate unique names)
123 current_id = 0
124 desc["builddate"] = str(builddate)
125 autocode = Template(autocode_header).render_unicode(version=desc["version"], builddate=builddate)
126 init_functions = []
127 loop_functions = []
128 callbacks = []
130 def register_callback(name, argbuf="global_argbuf", argbuf_len="ARGBUF_SIZE"):
131 nonlocal current_id
132 callbacks.append(("0" if name is None else "&"+name, argbuf, argbuf_len, current_id))
133 old_id = current_id
134 current_id += 1
135 return old_id
137 #Default callback number 0
138 register_callback("callback_get_descriptor_auto")
140 for mname, member in members.items():
141 mfile = member["type"]
142 mtype = mfile.replace('-', '_')
143 typepath = os.path.join(build_path, mfile + ".c.tp")
145 #CAUTION! These *will* exhibit strange behavior when called more than once!
146 def init_function():
147 fun = "init_{}_{}".format(mtype, seqnum)
148 init_functions.append(fun)
149 return fun
150 def loop_function():
151 fun = "loop_{}_{}".format(mtype, seqnum)
152 loop_functions.append(fun)
153 return fun
155 #module instance build config entries
156 properties = {}
157 functions = {}
159 #FIXME possibly swap the positions of ctype and fmt
160 def modulevar(name, ctype=None, fmt=None, array=False, access="rw", accid=None):
161 """Get the c name of a module variable and possibly register the variable with the code generator.
163 If only name is given, the autogenerated c name of the module variable will be returned.
165 If you provide fmt, virtual accessor methods for the variable will be registered and the variable will
166 be registered as a property in the build config (using the previously mentioned accessor methods).
167 In this context, "virtual" means that there will be callbacks in the callback list, but the setter will
168 not be backed by an actual function and the getter will just point to a global generic getter.
169 If you also provide ctype the accessors will also be generated.
170 array can be used to generated accessors for module variables that are arrays.
172 access can be used independent with at lest fmt given to specify the access type of the new module
173 parameter. The string will be copied to the build config 1:1 though this generator currently only
174 differentiates between "rw" and "r".
177 varname = "modvar_{}_{}_{}".format(mtype, seqnum, name)
178 if fmt is not None:
179 aval = 1
180 if array != False:
181 aval = array
183 if accid is None:
184 accid = register_callback("generic_getter_callback", ("" if array else "&")+varname, "sizeof("+varname+")")
185 if "w" in access:
186 register_callback(None, ("" if array else "&")+varname, "sizeof("+varname+")")
188 properties[name] = {
189 "size": struct.calcsize(fmt),
190 "id": accid,
191 "fmt": fmt}
192 if access is not "rw":
193 #Save some space in the build config (that later gets burned into the µC's really small flash!)
194 properties[name]["access"] = access
196 if ctype is not None:
197 array_component = ""
198 if array == True:
199 array_component = "[]"
200 elif array:
201 array_component = "[{}]".format(array)
202 return "{} {}{}".format(ctype, varname, array_component)
203 else:
204 assert(ctype is None)
206 return varname
208 def module_callback(name, argformat="", retformat=""):
209 """Register a regular module callback.
211 I hereby officially discourage the (sole) use of this function since these callbacks or functions as they
212 appear at the Cerebrum level cannot be automatically mapped to snmp MIBs in any sensible manner. Thus, please
213 use accessors for everything if possible, even if it is stuff that you would not traditionally use them for.
214 For an example on how to generate and register custom accessor methods please see simple-io.c.tp .
217 cbname = 'callback_{}_{}_{}'.format(mtype, seqnum, name)
218 cbid = register_callback(cbname)
219 func = { 'id': cbid }
220 #Save some space in the build config (that later gets burned into the µC's really small flash!)
221 if argformat is not '':
222 func['args'] = argformat
223 if retformat is not '':
224 func['returns'] = retformat
225 functions[name] = func
226 return cbname
228 #Flesh out the module template!
229 tp = Template(filename=typepath)
230 autocode += tp.render_unicode(
231 init_function=init_function,
232 loop_function=loop_function,
233 modulevar=modulevar,
234 module_callback=module_callback,
235 register_callback=register_callback,
236 member=member,
237 device=device)
239 #Save some space in the build config (that later gets burned into the µC's really small flash!)
240 if functions:
241 member['functions'] = functions
242 if properties:
243 member['properties'] = properties
245 #increment the module number
246 seqnum += 1
248 #finish the code generation and write the generated code to a file
249 autocode += Template(autocode_footer).render_unicode(init_functions=init_functions, loop_functions=loop_functions, callbacks=callbacks)
250 with open(os.path.join(build_path, 'autocode.c'), 'w') as f:
251 f.write(autocode)
252 #compress the build config and write it out
253 #config = lzma.compress(bytes(json.JSONEncoder(separators=(',',':')).encode(desc), 'ASCII'))
254 config = bytes(json.JSONEncoder(separators=(',',':')).encode(desc), 'ASCII')
255 with open(os.path.join(build_path, 'config.c'), 'w') as f:
256 f.write(Template(config_c_template).render_unicode(desc_len=len(config), desc=','.join(map(str, config))))
257 #compile the whole stuff
258 make_env = os.environ.copy()
259 make_env['MCU'] = device.get('mcu')
260 subprocess.call(['/usr/bin/env', 'make', '--no-print-directory', '-C', build_path, 'clean', target], env=make_env)
262 return desc
264 def commit(device, build_path, args):
265 """Flash the newly generated firmware onto the device"""
266 make_env = os.environ.copy()
267 make_env['MCU'] = device.get('mcu')
268 make_env['PORT'] = args.port
269 make_env['PROGRAMMER'] = device.get('programmer')
270 make_env['BAUDRATE'] = str(device.get('baudrate'))
271 subprocess.call(['/usr/bin/env', "make",'--no-print-directory', '-C', build_path, 'program'], env=make_env)
273 class TestBuild(unittest.TestCase):
275 def setUp(self):
276 pass
278 def test_basic_build(self):
279 generate({'members': {}, 'version': 0.17}, {'mcu': 'test'}, 'test', '2012-05-23 23:42:17')
281 def compareJSON(bytesa, bytesb):
282 jsona = json.JSONDecoder().decode(str(bytesa, "ASCII"))
283 normstra = bytes(json.JSONEncoder(separators=(',',':')).encode(jsona), 'ASCII')
284 jsonb = json.JSONDecoder().decode(str(bytesb, "ASCII"))
285 normstrb = bytes(json.JSONEncoder(separators=(',',':')).encode(jsonb), 'ASCII')
286 return normstra == normstrb
288 class TestCommStuff(unittest.TestCase):
290 def setUp(self):
291 generate({'members': {'test': {'type': 'test'}}, 'version': 0.17}, {'mcu': 'test'}, 'test', '2012-05-23 23:42:17')
293 def new_test_process(self):
294 #spawn a new communication test process
295 p = subprocess.Popen([os.path.join(os.path.dirname(__file__), 'test', 'main')], stdin=subprocess.PIPE, stdout=subprocess.PIPE)
297 #start a thread killing that process after a few seconds
298 def kill_subprocess():
299 time.sleep(5)
300 if p.poll() is None or p.returncode < 0:
301 p.terminate()
302 self.assert_(False, 'Communication test process terminated due to a timeout')
304 t = Thread(target=kill_subprocess)
305 t.daemon = True
306 t.start()
307 return (p, p.stdin, p.stdout, t)
309 def test_config_descriptor(self):
310 (p, stdin, stdout, t) = self.new_test_process();
312 stdin.write(b'\\#\x00\x00\x00\x00')
313 stdin.flush()
314 stdin.close()
316 (length,) = struct.unpack('>H', stdout.read(2))
317 #FIXME this fixed size comparision is *not* helpful.
318 #self.assertEqual(length, 227, 'Incorrect config descriptor length')
319 data = stdout.read(length)
320 #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')
321 #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.
322 #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))
323 #FIXME somehow, this commented-out device descriptor check fails randomly even though nothing is actually wrong.
325 def test_multipart_call(self):
326 (p, stdin, stdout, t) = self.new_test_process();
328 stdin.write(b'\\#\x00\x01\x00\x41AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA')
329 stdin.flush()
330 stdin.close()
332 #wait for test process to terminate. If everything else fails, the timeout thread will kill it.
333 p.wait()
334 self.assertEqual(p.returncode, 0, "The test process caught an error from the c code. Please watch stderr for details.")
336 def test_meta_multipart_call(self):
337 """Test whether the test function actually fails when given invalid data."""
338 (p, stdin, stdout, t) = self.new_test_process();
340 stdin.write(b'\\#\x00\x01\x00\x41AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAA')
341 stdin.flush()
342 stdin.close()
344 #wait for test process to terminate. If everything else fails, the timeout thread will kill it.
345 p.wait()
346 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.")
348 def test_multipart_call_long_args(self):
349 (p, stdin, stdout, t) = self.new_test_process();
351 stdin.write(b'\\#\x00\x05\x01\x01'+b'A'*257)
352 stdin.write(b'\\#\x00\x06\x00\x00')
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.")
359 self.assertEqual(p.returncode, 0, "The test process caught an error from the c code. Please watch stderr for details.")
361 def test_meta_multipart_call_long_args(self):
362 """Test whether the test function actually fails when given invalid data."""
363 (p, stdin, stdout, t) = self.new_test_process();
365 stdin.write(b'\\#\x00\x05\x01\x01'+b'A'*128+b'B'+b'A'*128)
366 stdin.flush()
367 stdin.close()
369 #wait for test process to terminate. If everything else fails, the timeout thread will kill it.
370 p.wait()
371 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.")
373 def test_attribute_accessors_multipart(self):
374 (p, stdin, stdout, t) = self.new_test_process();
376 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
377 stdin.write(b'\\#\x00\x04\x00\x00') # call check_test_buffer
378 stdin.flush()
379 stdin.close()
381 #wait for test process to terminate. If everything else fails, the timeout thread will kill it.
382 p.wait()
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_attribute_accessors_multipart(self):
386 (p, stdin, stdout, t) = self.new_test_process();
388 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
389 stdin.write(b'\\#\x00\x04\x00\x00') # call check_test_buffer
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.")