Added a setup.py. Thanks to riot (github.com/ri0t)!
[cerebrum.git] / generator.py
blobc50040dea99f9f74a6c9e9a7fb788e3c17c6caaa
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, 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 node_id = node_id or random.randint(0, 2**64-2)
129 autocode = Template(autocode_header).render_unicode(version=desc["version"], builddate=builddate)
130 init_functions = []
131 loop_functions = []
132 callbacks = []
134 def register_callback(name, argbuf="global_argbuf", argbuf_len="ARGBUF_SIZE"):
135 nonlocal current_id
136 callbacks.append(("0" if name is None else "&"+name, argbuf, argbuf_len, current_id))
137 old_id = current_id
138 current_id += 1
139 return old_id
141 #Default callback number 0
142 register_callback("callback_get_descriptor_auto")
144 for mname, member in members.items():
145 mfile = member["type"]
146 mtype = mfile.replace('-', '_')
147 typepath = os.path.join(build_path, mfile + ".c.tp")
149 #CAUTION! These *will* exhibit strange behavior when called more than once!
150 def init_function():
151 fun = "init_{}_{}".format(mtype, seqnum)
152 init_functions.append(fun)
153 return fun
154 def loop_function():
155 fun = "loop_{}_{}".format(mtype, seqnum)
156 loop_functions.append(fun)
157 return fun
159 #module instance build config entries
160 properties = {}
161 functions = {}
163 #FIXME possibly swap the positions of ctype and fmt
164 def modulevar(name, ctype=None, fmt=None, array=False, callbacks=(0, 0)):
165 """Get the c name of a module variable and possibly register the variable with the code generator.
167 If only `name` is given, the autogenerated c name of the module variable will be returned.
169 If you provide `fmt`, virtual accessor methods for the variable will be registered and the variable will
170 be registered as a property in the build config (using the previously mentioned accessor methods).
171 In this context, "virtual" means that there will be callbacks in the callback list, but the setter will
172 not be backed by an actual function and the getter will just point to a global generic getter.
174 If you also provide `ctype` the accessors will also be generated.
176 `array` can be used to generated accessors for module variables that are arrays.
178 `callbacks` can be a tuple of one or two values. Each value corresponds to one callback. If the tuple contains
179 only one value, no setter will be generated and the variable will be marked read-only. A value of `None` prompts
180 the generation of the "default" accessor function. A value of `True` prompts the registration of an accessor
181 function of the form `callback_(get|set)_${modulevar(name)}` whose argument is stored in the module variable
182 buffer itself and which you must implement yourself. You may also specify a tuple of the form `(cbname, buf, buflen)`
183 where `cbname` is the name of your callback and `buf` and `buflen` are the argument buffer and argument buffer length,
184 respectively.
186 varname = "modvar_{}_{}_{}".format(mtype, seqnum, name)
187 if fmt is not None:
188 aval = 1
189 if array != False:
190 aval = array
192 def accessor_callback(desc, cbtype, defcb):
193 if desc == 0:
194 return register_callback(defcb, ("" if array else "&")+varname, "sizeof("+varname+")")
195 elif desc == 1:
196 return register_callback("callback_{}_{}".format(cbtype, varname), ("" if array else "&")+varname, "sizeof("+varname+")")
197 elif desc == 2:
198 return register_callback("callback_{}_{}".format(cbtype, varname), "global_argbuf", "ARGBUF_SIZE")
199 else:
200 cbname, buf, buflen = desc
201 if cbname is True:
202 cbname = "callback_{}_{}".format(cbtype, varname)
203 return register_callback(cbname, buf, buflen)
205 properties[name] = {
206 "size": struct.calcsize(fmt),
207 "id": accessor_callback(callbacks[0], 'get', 'generic_getter_callback'),
208 "fmt": fmt}
210 if callbacks[1] is not None:
211 accessor_callback(callbacks[1], 'set', None)
212 else:
213 #Save some space in the build config (that later gets burned into the µC's
214 #really small flash!) by only putting this here in case of read-only access
215 properties[name]["access"] = 'r'
217 if ctype is not None:
218 array_component = ""
219 if array == True:
220 array_component = "[]"
221 elif array:
222 array_component = "[{}]".format(array)
223 return "{} {}{}".format(ctype, varname, array_component)
224 else:
225 assert(ctype is None)
227 return varname
229 def module_callback(name, argformat="", retformat="", regname=None):
230 """Register a regular module callback.
232 I hereby officially discourage the (sole) use of this function since these callbacks or functions as they
233 appear at the Cerebrum level cannot be automatically mapped to snmp MIBs in any sensible manner. Thus, please
234 use accessors for everything if possible, even if it is stuff that you would not traditionally use them for.
235 For an example on how to generate and register custom accessor methods please see simple-io.c.tp .
238 cbname = 'callback_{}_{}_{}'.format(mtype, seqnum, name)
239 cbid = register_callback(regname or cbname)
240 func = { 'id': cbid }
241 #Save some space in the build config (that later gets burned into the µC's really small flash!)
242 if argformat is not '':
243 func['args'] = argformat
244 if retformat is not '':
245 func['returns'] = retformat
246 functions[name] = func
247 return cbname
249 try:
250 #Flesh out the module template!
251 tp = Template(filename=typepath)
252 autocode += tp.render_unicode(
253 init_function=init_function,
254 loop_function=loop_function,
255 modulevar=modulevar,
256 setter=lambda x: 'callback_set_'+modulevar(x),
257 getter=lambda x: 'callback_get_'+modulevar(x),
258 module_callback=module_callback,
259 register_callback=register_callback,
260 member=member,
261 device=device)
262 except Exception as e:
263 print('-----[\x1b[91;1mException occurred while rendering a module\x1b[0m]-----')
264 print(exceptions.text_error_template().render().strip())
265 print('-----[end]-----')
266 raise e
268 #Save some space in the build config (that later gets burned into the µC's really small flash!)
269 if functions:
270 member['functions'] = functions
271 if properties:
272 member['properties'] = properties
274 #increment the module number
275 seqnum += 1
277 #finish the code generation and write the generated code to a file
278 autocode += Template(autocode_footer).render_unicode(init_functions=init_functions, loop_functions=loop_functions, callbacks=callbacks)
279 with open(os.path.join(build_path, 'autocode.c'), 'w') as f:
280 f.write(autocode)
281 #compress the build config and write it out
282 #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
283 #The first byte is used as a magic here. The first byte of a JSON string will always be a '{'
284 config = b'#' + lzma.compress(bytes(json.JSONEncoder(separators=(',',':')).encode(desc), 'utf-8'))
285 #config = bytes(json.JSONEncoder(separators=(',',':')).encode(desc), 'utf-8')
286 with open(os.path.join(build_path, 'config.c'), 'w') as f:
287 f.write(Template(config_c_template).render_unicode(desc_len=len(config), desc=','.join(map(str, config))))
288 #compile the whole stuff
289 make_env = os.environ.copy()
290 make_env['MCU'] = device.get('mcu')
291 make_env['CLOCK'] = str(device.get('clock'))
292 make_env['CEREBRUM_BAUDRATE'] = str(device.get('cerebrum_baudrate'))
293 make_env['CONFIG_MAC'] = str(node_id) #0xFFFF,FFFF,FFFF,FFFF is reserved as discovery address
294 subprocess.check_call(['/usr/bin/env', 'make', '--no-print-directory', '-C', build_path, target], env=make_env)
296 desc['node_id'] = node_id
297 print('\x1b[92;1mNode ID:\x1b[0m {:#016x}'.format(node_id))
299 return desc
301 def commit(device, build_path, args):
302 """Flash the newly generated firmware onto the device"""
303 make_env = os.environ.copy()
304 make_env['MCU'] = device.get('mcu')
305 make_env['PORT'] = args.port
306 make_env['PROGRAMMER'] = device.get('programmer')
307 make_env['PROGRAMMER_BAUDRATE'] = str(device.get('programmer_baudrate'))
308 subprocess.check_call(['/usr/bin/env', "make",'--no-print-directory', '-C', build_path, 'program'], env=make_env)
310 class TestBuild(unittest.TestCase):
312 def setUp(self):
313 pass
315 def test_basic_build(self):
316 generate({'members': {}, 'version': 0.17}, {'mcu': 'test'}, 'test', '2012-05-23 23:42:17', node_id=0x2342)
318 class TestCommStuff(unittest.TestCase):
320 def setUp(self):
321 generate({'members': {'test': {'type': 'test'}}, 'version': 0.17}, {'mcu': 'test'}, 'test', '2012-05-23 23:42:17', node_id=0x2342)
323 def new_test_process(self):
324 #spawn a new communication test process
325 p = subprocess.Popen([os.path.join(os.path.dirname(__file__), 'test', 'main')], stdin=subprocess.PIPE, stdout=subprocess.PIPE)
327 #start a thread killing that process after a few seconds
328 def kill_subprocess():
329 time.sleep(5)
330 if p.poll() is None or p.returncode < 0:
331 p.terminate()
332 self.assert_(False, 'Communication test process terminated due to a timeout')
334 t = Thread(target=kill_subprocess)
335 t.daemon = True
336 t.start()
337 return (p, p.stdin, p.stdout, t)
339 def test_config_descriptor(self):
340 (p, stdin, stdout, t) = self.new_test_process();
342 stdin.write(b'\\#\x23\x42\x00\x00\x00\x00')
343 stdin.flush()
344 stdin.close()
346 (length,) = struct.unpack('>H', stdout.read(2))
347 #FIXME this fixed size comparision is *not* helpful.
348 #self.assertEqual(length, 227, 'Incorrect config descriptor length')
349 data = stdout.read(length)
350 #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')
351 #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.
352 #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))
353 #FIXME somehow, this commented-out device descriptor check fails randomly even though nothing is actually wrong.
355 def test_multipart_call(self):
356 (p, stdin, stdout, t) = self.new_test_process();
358 stdin.write(b'\\#\x23\x42\x00\x01\x00\x41AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA')
359 stdin.flush()
360 stdin.close()
362 #wait for test process to terminate. If everything else fails, the timeout thread will kill it.
363 p.wait()
364 self.assertEqual(p.returncode, 0, "The test process caught an error from the c code. Please watch stderr for details.")
366 def test_meta_multipart_call(self):
367 """Test whether the test function actually fails when given invalid data."""
368 (p, stdin, stdout, t) = self.new_test_process();
370 stdin.write(b'\\#\x23\x42\x00\x01\x00\x41AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAA')
371 stdin.flush()
372 stdin.close()
374 #wait for test process to terminate. If everything else fails, the timeout thread will kill it.
375 p.wait()
376 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.")
378 def test_multipart_call_long_args(self):
379 (p, stdin, stdout, t) = self.new_test_process();
381 stdin.write(b'\\#\x23\x42\x00\x05\x01\x01'+b'A'*257)
382 stdin.write(b'\\#\x23\x42\x00\x06\x00\x00')
383 stdin.flush()
384 stdin.close()
386 #wait for test process to terminate. If everything else fails, the timeout thread will kill it.
387 p.wait()
388 self.assertEqual(p.returncode, 0, "The test process caught an error from the c code. Please watch stderr for details.")
389 self.assertEqual(p.returncode, 0, "The test process caught an error from the c code. Please watch stderr for details.")
391 def test_meta_multipart_call_long_args(self):
392 """Test whether the test function actually fails when given invalid data."""
393 (p, stdin, stdout, t) = self.new_test_process();
395 stdin.write(b'\\#\x23\x42\x00\x05\x01\x01'+b'A'*128+b'B'+b'A'*128)
396 stdin.flush()
397 stdin.close()
399 #wait for test process to terminate. If everything else fails, the timeout thread will kill it.
400 p.wait()
401 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.")
403 def test_attribute_accessors_multipart(self):
404 (p, stdin, stdout, t) = self.new_test_process();
406 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
407 stdin.write(b'\\#\x23\x42\x00\x04\x00\x00') # call check_test_buffer
408 stdin.flush()
409 stdin.close()
411 #wait for test process to terminate. If everything else fails, the timeout thread will kill it.
412 p.wait()
413 self.assertEqual(p.returncode, 0, "The test process caught an error from the c code. Please watch stderr for details.")
415 def test_meta_attribute_accessors_multipart(self):
416 (p, stdin, stdout, t) = self.new_test_process();
418 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
419 stdin.write(b'\\#\x23\x42\x00\x04\x00\x00') # call check_test_buffer
420 stdin.flush()
421 stdin.close()
423 #wait for test process to terminate. If everything else fails, the timeout thread will kill it.
424 p.wait()
425 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.")