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.
10 from threading
import Thread
12 from inspect
import isfunction
13 from mako
.template
import Template
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}
46 #This one contains the actual callback/init/loop magick.
50 #include <avr/pgmspace.h>
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}
61 const uint16_t callback_count = (sizeof(comm_callbacks)/sizeof(comm_callback_descriptor)); //${len(callbacks)};
64 % for initfunc in init_functions:
71 % for loopfunc in loop_functions:
76 void callback_get_descriptor_auto(const comm_callback_descriptor* cb, void* argbuf_end){
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++){
82 uart_putc(pgm_read_byte(i));
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
93 void generic_getter_callback(const comm_callback_descriptor* cb, void* argbuf_end){
95 uart_putc(cb->argbuf_len>>8);
96 uart_putc(cb->argbuf_len&0xFF);
98 for(char* i=((char*)cb->argbuf); i<((char*)cb->argbuf)+cb->argbuf_len; 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).
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)
125 desc
["builddate"] = str(builddate
)
126 autocode
= Template(autocode_header
).render_unicode(version
=desc
["version"], builddate
=builddate
)
131 def register_callback(name
, argbuf
="global_argbuf", argbuf_len
="ARGBUF_SIZE"):
133 callbacks
.append(("0" if name
is None else "&"+name
, argbuf
, argbuf_len
, current_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!
148 fun
= "init_{}_{}".format(mtype
, seqnum
)
149 init_functions
.append(fun
)
152 fun
= "loop_{}_{}".format(mtype
, seqnum
)
153 loop_functions
.append(fun
)
156 #module instance build config entries
160 #FIXME possibly swap the positions of ctype and fmt
161 def modulevar(name
, ctype
=None, fmt
=None, array
=False, callbacks
=(None, None)):
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,
183 varname
= "modvar_{}_{}_{}".format(mtype
, seqnum
, name
)
189 def accessor_callback(desc
, cbtype
, defcb
):
191 return register_callback(defcb
, ("" if array
else "&")+varname
, "sizeof("+varname
+")")
193 return register_callback("callback_{}_{}".format(cbtype
, varname
), ("" if array
else "&")+varname
, "sizeof("+varname
+")")
195 cbname
, buf
, buflen
= desc
197 cbname
= "callback_{}_{}".format(cbtype
, varname
)
198 return register_callback(cbname
, buf
, buflen
)
201 "size": struct
.calcsize(fmt
),
202 "id": accessor_callback(callbacks
[0], 'get', 'generic_getter_callback'),
205 if len(callbacks
) is 2:
206 accessor_callback(callbacks
[1], 'set', None)
208 #Save some space in the build config (that later gets burned into the µC's
209 #really small flash!) by only putting this here in case of read-only access
210 properties
[name
]["access"] = 'r'
212 if ctype
is not None:
215 array_component
= "[]"
217 array_component
= "[{}]".format(array
)
218 return "{} {}{}".format(ctype
, varname
, array_component
)
220 assert(ctype
is None)
224 def module_callback(name
, argformat
="", retformat
=""):
225 """Register a regular module callback.
227 I hereby officially discourage the (sole) use of this function since these callbacks or functions as they
228 appear at the Cerebrum level cannot be automatically mapped to snmp MIBs in any sensible manner. Thus, please
229 use accessors for everything if possible, even if it is stuff that you would not traditionally use them for.
230 For an example on how to generate and register custom accessor methods please see simple-io.c.tp .
233 cbname
= 'callback_{}_{}_{}'.format(mtype
, seqnum
, name
)
234 cbid
= register_callback(cbname
)
235 func
= { 'id': cbid
}
236 #Save some space in the build config (that later gets burned into the µC's really small flash!)
237 if argformat
is not '':
238 func
['args'] = argformat
239 if retformat
is not '':
240 func
['returns'] = retformat
241 functions
[name
] = func
244 #Flesh out the module template!
245 tp
= Template(filename
=typepath
)
246 autocode
+= tp
.render_unicode(
247 init_function
=init_function
,
248 loop_function
=loop_function
,
250 module_callback
=module_callback
,
251 register_callback
=register_callback
,
255 #Save some space in the build config (that later gets burned into the µC's really small flash!)
257 member
['functions'] = functions
259 member
['properties'] = properties
261 #increment the module number
264 #finish the code generation and write the generated code to a file
265 autocode
+= Template(autocode_footer
).render_unicode(init_functions
=init_functions
, loop_functions
=loop_functions
, callbacks
=callbacks
)
266 with
open(os
.path
.join(build_path
, 'autocode.c'), 'w') as f
:
268 #compress the build config and write it out
269 #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
270 #The first byte is used as a magic here. The first byte of a JSON string will always be a '{'
271 config
= b
'#' + lzma
.compress(bytes(json
.JSONEncoder(separators
=(',',':')).encode(desc
), 'utf-8'))
272 #config = bytes(json.JSONEncoder(separators=(',',':')).encode(desc), 'utf-8')
273 with
open(os
.path
.join(build_path
, 'config.c'), 'w') as f
:
274 f
.write(Template(config_c_template
).render_unicode(desc_len
=len(config
), desc
=','.join(map(str, config
))))
275 #compile the whole stuff
276 make_env
= os
.environ
.copy()
277 make_env
['MCU'] = device
.get('mcu')
278 subprocess
.check_call(['/usr/bin/env', 'make', '--no-print-directory', '-C', build_path
, 'clean', target
], env
=make_env
)
282 def commit(device
, build_path
, args
):
283 """Flash the newly generated firmware onto the device"""
284 make_env
= os
.environ
.copy()
285 make_env
['MCU'] = device
.get('mcu')
286 make_env
['PORT'] = args
.port
287 make_env
['PROGRAMMER'] = device
.get('programmer')
288 make_env
['BAUDRATE'] = str(device
.get('baudrate'))
289 subprocess
.check_call(['/usr/bin/env', "make",'--no-print-directory', '-C', build_path
, 'program'], env
=make_env
)
291 class TestBuild(unittest
.TestCase
):
296 def test_basic_build(self
):
297 generate({'members': {}, 'version': 0.17}, {'mcu': 'test'}, 'test', '2012-05-23 23:42:17')
299 def compareJSON(bytesa
, bytesb
):
300 jsona
= json
.JSONDecoder().decode(str(bytesa
, "ASCII"))
301 normstra
= bytes(json
.JSONEncoder(separators
=(',',':')).encode(jsona
), 'ASCII')
302 jsonb
= json
.JSONDecoder().decode(str(bytesb
, "ASCII"))
303 normstrb
= bytes(json
.JSONEncoder(separators
=(',',':')).encode(jsonb
), 'ASCII')
304 return normstra
== normstrb
306 class TestCommStuff(unittest
.TestCase
):
309 generate({'members': {'test': {'type': 'test'}}, 'version': 0.17}, {'mcu': 'test'}, 'test', '2012-05-23 23:42:17')
311 def new_test_process(self
):
312 #spawn a new communication test process
313 p
= subprocess
.Popen([os
.path
.join(os
.path
.dirname(__file__
), 'test', 'main')], stdin
=subprocess
.PIPE
, stdout
=subprocess
.PIPE
)
315 #start a thread killing that process after a few seconds
316 def kill_subprocess():
318 if p
.poll() is None or p
.returncode
< 0:
320 self
.assert_(False, 'Communication test process terminated due to a timeout')
322 t
= Thread(target
=kill_subprocess
)
325 return (p
, p
.stdin
, p
.stdout
, t
)
327 def test_config_descriptor(self
):
328 (p
, stdin
, stdout
, t
) = self
.new_test_process();
330 stdin
.write(b
'\\#\x00\x00\x00\x00')
334 (length
,) = struct
.unpack('>H', stdout
.read(2))
335 #FIXME this fixed size comparision is *not* helpful.
336 #self.assertEqual(length, 227, 'Incorrect config descriptor length')
337 data
= stdout
.read(length
)
338 #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')
339 #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.
340 #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))
341 #FIXME somehow, this commented-out device descriptor check fails randomly even though nothing is actually wrong.
343 def test_multipart_call(self
):
344 (p
, stdin
, stdout
, t
) = self
.new_test_process();
346 stdin
.write(b
'\\#\x00\x01\x00\x41AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA')
350 #wait for test process to terminate. If everything else fails, the timeout thread will kill it.
352 self
.assertEqual(p
.returncode
, 0, "The test process caught an error from the c code. Please watch stderr for details.")
354 def test_meta_multipart_call(self
):
355 """Test whether the test function actually fails when given invalid data."""
356 (p
, stdin
, stdout
, t
) = self
.new_test_process();
358 stdin
.write(b
'\\#\x00\x01\x00\x41AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAA')
362 #wait for test process to terminate. If everything else fails, the timeout thread will kill it.
364 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.")
366 def test_multipart_call_long_args(self
):
367 (p
, stdin
, stdout
, t
) = self
.new_test_process();
369 stdin
.write(b
'\\#\x00\x05\x01\x01'+b
'A'*257)
370 stdin
.write(b
'\\#\x00\x06\x00\x00')
374 #wait for test process to terminate. If everything else fails, the timeout thread will kill it.
376 self
.assertEqual(p
.returncode
, 0, "The test process caught an error from the c code. Please watch stderr for details.")
377 self
.assertEqual(p
.returncode
, 0, "The test process caught an error from the c code. Please watch stderr for details.")
379 def test_meta_multipart_call_long_args(self
):
380 """Test whether the test function actually fails when given invalid data."""
381 (p
, stdin
, stdout
, t
) = self
.new_test_process();
383 stdin
.write(b
'\\#\x00\x05\x01\x01'+b
'A'*128+b
'B'+b
'A'*128)
387 #wait for test process to terminate. If everything else fails, the timeout thread will kill it.
389 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.")
391 def test_attribute_accessors_multipart(self
):
392 (p
, stdin
, stdout
, t
) = self
.new_test_process();
394 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
395 stdin
.write(b
'\\#\x00\x04\x00\x00') # call check_test_buffer
399 #wait for test process to terminate. If everything else fails, the timeout thread will kill it.
401 self
.assertEqual(p
.returncode
, 0, "The test process caught an error from the c code. Please watch stderr for details.")
403 def test_meta_attribute_accessors_multipart(self
):
404 (p
, stdin
, stdout
, t
) = self
.new_test_process();
406 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
407 stdin
.write(b
'\\#\x00\x04\x00\x00') # call check_test_buffer
411 #wait for test process to terminate. If everything else fails, the timeout thread will kill it.
413 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.")