Merge branch 'master' of c-leuse:cerebrum
[cerebrum.git] / generator.py
blob3bb6a77bd76518d37176461a47a382678abeac0e
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 import random
11 from threading import Thread
12 import struct
13 from inspect import isfunction
14 from mako.template import Template
15 from mako import exceptions
16 import binascii
17 import json
18 try:
19 import lzma
20 except:
21 import pylzma as lzma
22 import codecs
23 import unittest
24 """Automatic Cerebrum C code generator"""
26 # Code templates. Actually, this is the only device-dependent place in this whole
27 # file, and actually only a very few lines are device-dependent.
28 # FIXME: Break this stuff into a common "C code generator" which is included from
29 # here and from the msp code generator and which is feeded with the two or three
30 # device-dependent lines
32 autocode_header = """\
33 /* AUTOGENERATED CODE FOLLOWS!
34 * This file contains the code generated from the module templates as well as
35 * some glue logic. It is generated following the device config by "generate.py"
36 * in this very folder. Please refrain from modifying it, modify the templates
37 * and generation logic instead.
39 * Build version: ${version}, build date: ${builddate}
42 #include <string.h>
43 #include "autocode.h"
44 #include "comm.h"
45 #include "uart.h"
46 """
48 #This one contains the actual callback/init/loop magick.
49 autocode_footer = """
50 #include "config.h"
51 #if defined(__AVR__)
52 #include <avr/pgmspace.h>
53 #endif
55 void generic_getter_callback(const comm_callback_descriptor* cb, void* argbuf_end);
57 const comm_callback_descriptor comm_callbacks[] = {
58 % for (callback, argbuf, argbuf_len, id) in callbacks:
59 {${callback}, (void*)${argbuf}, ${argbuf_len}}, //${id}
60 % endfor
63 const uint16_t callback_count = (sizeof(comm_callbacks)/sizeof(comm_callback_descriptor)); //${len(callbacks)};
65 void init_auto(){
66 % for initfunc in init_functions:
67 ${initfunc}();
68 % endfor
71 void loop_auto(){
72 comm_loop();
73 % for loopfunc in loop_functions:
74 ${loopfunc}();
75 % endfor
78 void callback_get_descriptor_auto(const comm_callback_descriptor* cb, void* argbuf_end){
79 //FIXME
80 uart_putc(auto_config_descriptor_length >> 8);
81 uart_putc(auto_config_descriptor_length & 0xFF);
82 for(const char* i=auto_config_descriptor; i < auto_config_descriptor+auto_config_descriptor_length; i++){
83 #if defined(__AVR__)
84 uart_putc(pgm_read_byte(i));
85 #else
86 uart_putc(*i);
87 #endif
91 //Generic getter used for any readable parameters.
92 //Please note one curious thing: This callback can not only be used to read, but also to write a variable. The only
93 //difference between the setter and the getter of a variable is that the setter does not read the entire variable's
94 //contents aloud.
95 void generic_getter_callback(const comm_callback_descriptor* cb, void* argbuf_end){
96 //response length
97 uart_putc(cb->argbuf_len>>8);
98 uart_putc(cb->argbuf_len&0xFF);
99 //response
100 for(char* i=((char*)cb->argbuf); i<((char*)cb->argbuf)+cb->argbuf_len; i++){
101 uart_putc(*i);
107 config_c_template = """\
108 /* AUTOGENERATED CODE AHEAD!
109 * This file contains the device configuration in lzma-ed json-format. It is
110 * autogenerated by "generate.py" (which should be found in this folder).
112 #include "config.h"
113 #ifndef PROGMEM
114 #define PROGMEM
115 #endif
117 unsigned int auto_config_descriptor_length = ${desc_len};
118 char const auto_config_descriptor[] PROGMEM = {${desc}};
121 #FIXME possibly make a class out of this one
122 #FIXME I think the target parameter is not used anywhere. Remove?
123 def generate(desc, device, build_path, builddate, buildname=None, target = 'all', node_id=None):
124 members = desc["members"]
125 seqnum = 23 #module number (only used during build time to generate unique names)
126 current_id = 0
127 desc['builddate'] = str(builddate)
128 if buildname:
129 desc['name'] = buildname
130 node_id = node_id or random.randint(0, 2**64-2)
131 autocode = Template(autocode_header).render_unicode(version=desc["version"], builddate=builddate)
132 init_functions = []
133 loop_functions = []
134 callbacks = []
136 def register_callback(name, argbuf="global_argbuf", argbuf_len="ARGBUF_SIZE"):
137 nonlocal current_id
138 callbacks.append(("0" if name is None else "&"+name, argbuf, argbuf_len, current_id))
139 old_id = current_id
140 current_id += 1
141 return old_id
143 #Default callback number 0
144 register_callback("callback_get_descriptor_auto")
146 for mname, member in members.items():
147 mfile = member["type"]
148 mtype = mfile.replace('-', '_')
149 typepath = os.path.join(build_path, mfile + ".c.tp")
151 #CAUTION! These *will* exhibit strange behavior when called more than once!
152 def init_function():
153 fun = "init_{}_{}".format(mtype, seqnum)
154 init_functions.append(fun)
155 return fun
156 def loop_function():
157 fun = "loop_{}_{}".format(mtype, seqnum)
158 loop_functions.append(fun)
159 return fun
161 #module instance build config entries
162 properties = {}
163 functions = {}
165 #FIXME possibly swap the positions of ctype and fmt
166 def modulevar(name, ctype=None, fmt=None, array=False, callbacks=(0, 0)):
167 """Get the c name of a module variable and possibly register the variable with the code generator.
169 If only `name` is given, the autogenerated c name of the module variable will be returned.
171 If you provide `fmt`, virtual accessor methods for the variable will be registered and the variable will
172 be registered as a property in the build config (using the previously mentioned accessor methods).
173 In this context, "virtual" means that there will be callbacks in the callback list, but the setter will
174 not be backed by an actual function and the getter will just point to a global generic getter.
176 If you also provide `ctype` the accessors will also be generated.
178 `array` can be used to generated accessors for module variables that are arrays.
180 `callbacks` can be a tuple of one or two values. Each value corresponds to one callback. If the tuple contains
181 only one value, no setter will be generated and the variable will be marked read-only. A value of 0 prompts
182 the generation of the "default" accessor function. A value of 1 prompts the registration of an accessor
183 function of the form `callback_(get|set)_${modulevar(name)}` whose argument is stored in the module variable
184 buffer itself and which you must implement yourself. A value of 2 does the same storing the data in the global
185 argument buffer. You may also specify a tuple of the form `(cbname, buf, buflen)`
186 where `cbname` is the name of your callback and `buf` and `buflen` are the argument buffer and argument buffer length,
187 respectively.
189 varname = "modvar_{}_{}_{}".format(mtype, seqnum, name)
190 if fmt is not None:
191 aval = 1
192 if array != False:
193 aval = array
195 def accessor_callback(desc, cbtype, defcb):
196 if desc == 0:
197 return register_callback(defcb, ("" if array else "&")+varname, "sizeof("+varname+")")
198 elif desc == 1:
199 return register_callback("callback_{}_{}".format(cbtype, varname), ("" if array else "&")+varname, "sizeof("+varname+")")
200 elif desc == 2:
201 return register_callback("callback_{}_{}".format(cbtype, varname), "global_argbuf", "ARGBUF_SIZE")
202 else:
203 cbname, buf, buflen = desc
204 if cbname is True:
205 cbname = "callback_{}_{}".format(cbtype, varname)
206 return register_callback(cbname, buf, buflen)
208 properties[name] = {
209 "size": struct.calcsize(fmt),
210 "id": accessor_callback(callbacks[0], 'get', 'generic_getter_callback'),
211 "fmt": fmt}
213 if callbacks[1] is not None:
214 accessor_callback(callbacks[1], 'set', None)
215 else:
216 #Save some space in the build config (that later gets burned into the µC's
217 #really small flash!) by only putting this here in case of read-only access
218 properties[name]["access"] = 'r'
220 if ctype is not None:
221 array_component = ""
222 if array == True:
223 array_component = "[]"
224 elif array:
225 array_component = "[{}]".format(array)
226 return "{} {}{}".format(ctype, varname, array_component)
227 else:
228 assert(ctype is None)
230 return varname
232 def module_callback(name, argformat="", retformat="", regname=None):
233 """Register a regular module callback.
235 I hereby officially discourage the (sole) use of this function since these callbacks or functions as they
236 appear at the Cerebrum level cannot be automatically mapped to snmp MIBs in any sensible manner. Thus, please
237 use accessors for everything if possible, even if it is stuff that you would not traditionally use them for.
238 For an example on how to generate and register custom accessor methods please see simple-io.c.tp .
241 cbname = 'callback_{}_{}_{}'.format(mtype, seqnum, name)
242 cbid = register_callback(regname or cbname)
243 func = { 'id': cbid }
244 #Save some space in the build config (that later gets burned into the µC's really small flash!)
245 if argformat is not '':
246 func['args'] = argformat
247 if retformat is not '':
248 func['returns'] = retformat
249 functions[name] = func
250 return cbname
252 try:
253 #Flesh out the module template!
254 tp = Template(filename=typepath)
255 autocode += tp.render_unicode(
256 init_function=init_function,
257 loop_function=loop_function,
258 modulevar=modulevar,
259 setter=lambda x: 'callback_set_'+modulevar(x),
260 getter=lambda x: 'callback_get_'+modulevar(x),
261 module_callback=module_callback,
262 register_callback=register_callback,
263 member=member,
264 device=device)
265 except:
266 print('-----[\x1b[91;1mException occurred while rendering module {}\x1b[0m]-----'.format(mname))
267 print('Current module definition:')
268 print(json.dumps(member, indent=4, separators=(',', ': ')))
269 print(exceptions.text_error_template().render().strip())
270 print('-----[end]-----')
271 raise
273 #Save some space in the build config (that later gets burned into the µC's really small flash!)
274 if functions:
275 member['functions'] = functions
276 if properties:
277 member['properties'] = properties
279 #increment the module number
280 seqnum += 1
282 #finish the code generation and write the generated code to a file
283 autocode += Template(autocode_footer).render_unicode(init_functions=init_functions, loop_functions=loop_functions, callbacks=callbacks)
284 with open(os.path.join(build_path, 'autocode.c'), 'w') as f:
285 f.write(autocode)
286 #compress the build config and write it out
287 #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
288 #The first byte is used as a magic here. The first byte of a JSON string will always be a '{'
289 config = b'#' + lzma.compress(bytes(json.JSONEncoder(separators=(',',':')).encode(desc), 'utf-8'))
290 #config = bytes(json.JSONEncoder(separators=(',',':')).encode(desc), 'utf-8')
291 with open(os.path.join(build_path, 'config.c'), 'w') as f:
292 f.write(Template(config_c_template).render_unicode(desc_len=len(config), desc=','.join(map(str, config))))
293 #compile the whole stuff
294 make_env = os.environ.copy()
295 make_env['MCU'] = device.get('mcu')
296 make_env['CLOCK'] = str(device.get('clock'))
297 make_env['CEREBRUM_BAUDRATE'] = str(device.get('cerebrum_baudrate'))
298 make_env['CONFIG_MAC'] = str(node_id) #0xFFFF,FFFF,FFFF,FFFF is reserved as discovery address
299 subprocess.check_call(['/usr/bin/env', 'make', '--no-print-directory', '-C', build_path, target], env=make_env)
301 desc['node_id'] = node_id
302 print('\x1b[92;1mNode ID:\x1b[0m {:#016x}'.format(node_id))
304 return desc
306 def commit(device, build_path, args):
307 """Flash the newly generated firmware onto the device"""
308 make_env = os.environ.copy()
309 make_env['MCU'] = device.get('mcu')
310 make_env['PORT'] = args.port
311 make_env['PROGRAMMER'] = device.get('programmer')
312 make_env['PROGRAMMER_BAUDRATE'] = str(device.get('programmer_baudrate'))
313 subprocess.check_call(['/usr/bin/env', "make",'--no-print-directory', '-C', build_path, 'program'], env=make_env)
315 class TestBuild(unittest.TestCase):
317 def setUp(self):
318 pass
320 def test_basic_build(self):
321 generate({'members': {}, 'version': 0.17}, {'mcu': 'test'}, 'test', '2012-05-23 23:42:17', node_id=0x2342)
323 class TestCommStuff(unittest.TestCase):
325 def setUp(self):
326 generate({'members': {'test': {'type': 'test'}}, 'version': 0.17}, {'mcu': 'test'}, 'test', '2012-05-23 23:42:17', node_id=0x2342)
328 def new_test_process(self):
329 #spawn a new communication test process
330 p = subprocess.Popen([os.path.join(os.path.dirname(__file__), 'test', 'main')], stdin=subprocess.PIPE, stdout=subprocess.PIPE)
332 #start a thread killing that process after a few seconds
333 def kill_subprocess():
334 time.sleep(5)
335 if p.poll() is None or p.returncode < 0:
336 p.terminate()
337 self.assert_(False, 'Communication test process terminated due to a timeout')
339 t = Thread(target=kill_subprocess)
340 t.daemon = True
341 t.start()
342 return (p, p.stdin, p.stdout, t)
344 def test_config_descriptor(self):
345 (p, stdin, stdout, t) = self.new_test_process();
347 stdin.write(b'\\#\x23\x42\x00\x00\x00\x00')
348 stdin.flush()
349 stdin.close()
351 (length,) = struct.unpack('>H', stdout.read(2))
352 #FIXME this fixed size comparision is *not* helpful.
353 #self.assertEqual(length, 227, 'Incorrect config descriptor length')
354 data = stdout.read(length)
355 #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')
356 #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.
357 #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))
358 #FIXME somehow, this commented-out device descriptor check fails randomly even though nothing is actually wrong.
360 def test_multipart_call(self):
361 (p, stdin, stdout, t) = self.new_test_process();
363 stdin.write(b'\\#\x23\x42\x00\x01\x00\x41AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA')
364 stdin.flush()
365 stdin.close()
367 #wait for test process to terminate. If everything else fails, the timeout thread will kill it.
368 p.wait()
369 self.assertEqual(p.returncode, 0, "The test process caught an error from the c code. Please watch stderr for details.")
371 def test_meta_multipart_call(self):
372 """Test whether the test function actually fails when given invalid data."""
373 (p, stdin, stdout, t) = self.new_test_process();
375 stdin.write(b'\\#\x23\x42\x00\x01\x00\x41AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAA')
376 stdin.flush()
377 stdin.close()
379 #wait for test process to terminate. If everything else fails, the timeout thread will kill it.
380 p.wait()
381 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.")
383 def test_multipart_call_long_args(self):
384 (p, stdin, stdout, t) = self.new_test_process();
386 stdin.write(b'\\#\x23\x42\x00\x05\x01\x01'+b'A'*257)
387 stdin.write(b'\\#\x23\x42\x00\x06\x00\x00')
388 stdin.flush()
389 stdin.close()
391 #wait for test process to terminate. If everything else fails, the timeout thread will kill it.
392 p.wait()
393 self.assertEqual(p.returncode, 0, "The test process caught an error from the c code. Please watch stderr for details.")
394 self.assertEqual(p.returncode, 0, "The test process caught an error from the c code. Please watch stderr for details.")
396 def test_meta_multipart_call_long_args(self):
397 """Test whether the test function actually fails when given invalid data."""
398 (p, stdin, stdout, t) = self.new_test_process();
400 stdin.write(b'\\#\x23\x42\x00\x05\x01\x01'+b'A'*128+b'B'+b'A'*128)
401 stdin.flush()
402 stdin.close()
404 #wait for test process to terminate. If everything else fails, the timeout thread will kill it.
405 p.wait()
406 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.")
408 def test_attribute_accessors_multipart(self):
409 (p, stdin, stdout, t) = self.new_test_process();
411 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
412 stdin.write(b'\\#\x23\x42\x00\x04\x00\x00') # call check_test_buffer
413 stdin.flush()
414 stdin.close()
416 #wait for test process to terminate. If everything else fails, the timeout thread will kill it.
417 p.wait()
418 self.assertEqual(p.returncode, 0, "The test process caught an error from the c code. Please watch stderr for details.")
420 def test_meta_attribute_accessors_multipart(self):
421 (p, stdin, stdout, t) = self.new_test_process();
423 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
424 stdin.write(b'\\#\x23\x42\x00\x04\x00\x00') # call check_test_buffer
425 stdin.flush()
426 stdin.close()
428 #wait for test process to terminate. If everything else fails, the timeout thread will kill it.
429 p.wait()
430 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.")