Text
Page: 1
Practical mruby/c firmware
development with CRuby
HASUMI Hitoshi @hasumikin
RubyKaigi 2019
April 19, 2019
Fukuoka International Congress Center
Page: 4
what is mruby/c?
github.com/mrubyc/mrubyc
one of the mruby family
`/c` symbolizes compact,
concurrent and capability
especially dedicated to
one-chip microcontroller
Page: 5
mruby and mruby/c
mruby
mruby/c
v1.0.0 in Jan 2014
v1.0 in Jan 2017
for general embedded
for one-chip
software
microcontroller
RAM < 400KB
RAM < 40KB
sometimes mruby is still too big to run on
microcontroller
Page: 6
both mruby and mruby/c
bytecodes are compiled by `mrbc`
virtual machine (VM) executes the bytecode
Page: 7
bytecode
a kind of intermediate representation
virtual machine dynamically interprets the bytecode
and processes the program
Page: 8
mruby on microcontroller
RTOS (Real-Time OS) manages mruby VMs. RTOS has
features like multi tasking, etc.
Page: 9
mruby/c on microcontroller
mruby/c has its own mechanism to manage the
runtime: rrt0
Page: 10
mruby/c - virtual machine (VM)
much smaller than mruby's one
that's why mruby/c runs on smaller RAM
accordingly, mruby/c has less functionality than
mruby
Page: 12
how less? - for example
mruby/c doesn't have module, hence there is no
Kernel module
then you must wonder how can you `#puts`?
in mruby/c, `#puts` is implemented in Object class
Page: 13
how less? - for example
mruby/c doesn't have #send, #eval, nor
#method_missing
moreover, mruby/c neither have your favorite
features like TracePoint nor Refinements 😞
Page: 14
how less? - actually
the full list of mruby/c's classes
Array, FalseClass, Fixnum, Float, Hash, Math,
Mutex, NilClass, Numeric, Object, Proc, Range,
String, Symbol, TrueClass, VM
Page: 15
despite the fact,
no problem in practical use of microcontroller
as far as IoT go, mruby/c is enough Ruby as I expect
we can fully develop firmwares with features of
mruby/c
Page: 17
Today's agenda
きょうはこんな話をします
Page: 18
Little more Rubyish
もうちょいRubyっぽくやろう
Page: 20
mruby/c firmware is made up of three parts
1) peripheral API wrapper (C)
2) business logic (mruby)
3) infinite loop (mruby)
Page: 21
mruby/c firmware is made up of three parts
1) peripheral API wrapper (C)
2) business logic (mruby) - model
3) infinite loop (mruby) - controller
Page: 22
things make situation difficult
peripheral API needs real hardware
business logic needs peripheral APIs really work
infinite loop needs real data from business logic
Page: 23
mruby/c firmware is made up of three parts
1) peripheral API wrapper (C)
2) business logic (mruby)
3) infinite loop (mruby)
Page: 24
peripheral API wapper
https://rubykaigi.org/2018
Page: 25
mruby/c firmware is made up of three parts
1) peripheral API (C)
2) business logic (mruby)
3) infinite loop (mruby)
Page: 26
mruby/c firmware is made up of three parts
Page: 27
mruby/c firmware is made up of three parts
Page: 28
mruby/c firmware is made up of three parts
Page: 29
mruby/c firmware is made up of three parts
Page: 30
mruby/c firmware is made up of three parts
Page: 34
will calling fuga raise error?
Page: 35
methods still not implemented
we often should write business logic without hitting
peripherals
it will cost a lot in some case
it is possible the design of peripheral details might
not be finished yet
what you expect in this situation?
Page: 38
Test Driven
Development for
Embedded Ruby
Page: 39
(DEMO)
github.com/hasumikin/mrubyc-test
Page: 40
when I started to use mruby/c
there is no testing tool
even mruby/c itself sometimes regressed 😨
I had difficulties of writing my application
Page: 41
so, why did I use mruby/c?
Page: 43
Anyway, I started to create
mrubyc-test.gem
Page: 44
mrubyc-test.gem
it's the first testing tool for mruby/c ever
I wanted to go Rubyish in order to make it
but mruby/c doesn't have enough features to make
testing tool as you saw just before
Page: 45
mrubyc-test.gem - designed as
a RubyGem, implemented in CRuby instead of
mruby
Test::Unit-like API
supports stub and mock
now you can test your business logic without
implementing peripheral functions like #fuga
Page: 46
mrubyc-test.gem - stub
# app code
class Sample
attr_accessor :result
def do_something(arg)
@result = arg + still_not_defined_method
end
end
# test code
class SampleTest < MrubycTestCase
def stub_case
sample_obj = Sample.new
stub(sample_obj).still_not_defined_method { ", it must be Ruby" }
sample_obj.do_something("If it behaves like Ruby")
assert_equal "If it behaves like Ruby, it must be Ruby", sample_obj.result
end
end
Page: 47
mrubyc-test.gem - mock
# app code
class Sample
def do_other_thing
to_be_hit()
end
end
# test code
class SampleTest < MrubycTestCase
def mock_case
sample_obj = Sample.new
mock(sample_obj).to_be_hit
sample_obj.do_other_thing
end
end
Page: 48
it was my personal tool
github.com/hasumikin/mrubyc-test
Page: 49
but already abandoned because
github.com/hasumikin/mrubyc-test
Page: 50
now it's official 🎉
github.com/mrubyc/mrubyc-test
Page: 51
mrubyc-test.gem
adopted as the testing tool for mruby/c itself
so now you can safely send pull request to mruby/c
you can write mruby/c application with confidence
Page: 52
mrubyc-test.gem - internal
the gist is creating test.rb by `test code generator`
implemented in CRuby
Page: 53
mrubyc-test.gem - how to make the test.rb
gathers information of test cases by
#method_added
I learned this technique from Test::Unit
generates stub methods and mock methods
makes all-in-one script: test.rb
all the indispensable mechanism of assertion, stub,
mock, app code and test code get together
Page: 54
mrubyc-test.gem - Module#method_added
class MrubycTestCase
def self.method_added(name)
return false if %i(method_missing setup teardown).include?(name)
location = caller_locations(1, 1)[0]
path = location.absolute_path || location.path
line = location.lineno
@@added_methods << {
method_name: name.to_s,
path:
File.expand_path(path),
line:
line
}
Page: 55
mrubyc-test.gem
class SampleTest < MrubycTestCase
desc "stub test sample"
def stub_case # hooks #method_added
sample_obj = Sample.new
stub(sample_obj).still_not_defined_method {
", it must be Ruby"
}
test code inherits MrubycTestCase to be analyzed
Page: 56
mrubyc-test.gem -
BasicObject#method_missing
class MrubycTestCase
def method_missing(method_name, *args)
case method_name
when :stub, :mock
location = caller_locations(1, 1)[0]
Mrubyc::Test::Generator::Double.new(
method_name, args[0], location
)
Page: 57
mrubyc-test.gem - generated stub method
# part of test.rb
class Sample
def still_not_defined_method
", it must be Ruby"
end
end
Page: 58
mrubyc-test.gem - template of stub
<% test_cases.each do |test_case| -%>
<% test_case[:stubs].each do |stub| -%>
class <%= stub[:class_name] %>
attr_accessor <%= stub[:instance_variables] %>
def <%= stub[:method_name] %>
<% if stub[:return_value].is_a?(String) -%>
"<%= stub[:return_value] %>"
<% else -%>
<%= stub[:return_value] %>
<% end -%>
end
end
<% end -%>
Page: 60
mruby/c firmware is made up of three parts
1) peripheral API (C)
2) business logic (mruby)
3) infinite loop (mruby)
Page: 61
mruby/c firmware is made up of three parts
Page: 62
we have multiple infinite loops
firmware programming is essentially thread
programming which consists of multiple infinite loops
they keep watch on status like user input, changing
sensor value and BLE/WiFi message, then display
some information to indicate internal status
Page: 63
the loops of mruby/c are
user space threads managed by mruby/c's runtime
/* main.c */
#define MEMORY_SIZE (1024 * 40) /* 40KB */
static uint8_t mrubyc_vm_pool[MEMORY_SIZE];
int main(void) {
mrbc_init(mrubyc_vm_pool, MEMORY_SIZE);
mrbc_create_task(watch_user_interace, 0);
mrbc_create_task(change_display, 0);
mrbc_create_task(watch_sensor_value, 0);
mrbc_run();
}
Page: 64
threads of CRuby
correspond to native threads (with GVL)
def start_loops
threads = []
threads << Thread.new { watch_user_interface }
threads << Thread.new { change_display }
threads << Thread.new { watch_sensor_value }
threads.each(&:join)
end
Page: 65
(DEMO)
github.com/hasumikin/mrubyc-debugger
Page: 66
mrubyc-debugger.gem
mrubyc-debugger runs mruby/c loop script as a
CRuby thread
it simultaneously shows which lines are being
executed
besides, it have to take over the debug print of the
script
in order to do that, we can use your favorite CRuby
features like ...
Page: 68
mrubyc-debugger.gem - TracePoint
tasks = Dir.glob(File.join(Dir.pwd, "mrubyc_loops_dir", "*.rb"))
TracePoint.new(:c_call, :call, :line) do |tp|
number = nil
caller_locations(1, 1).each do |caller_location|
tasks.each_with_index do |task, i|
number = i if caller_location.to_s.include?(File.basename(task))
end
if number
@@mutex.lock
event = {
method_id:
tp.method_id,
lineno:
tp.lineno,
caller_location: caller_location,
binding:
tp.binding }
$event_queues[number].push event
@@mutex.unlock
Page: 70
mrubyc-debugger.gem - Refinements
module DebugQueue
refine Kernel do
def puts(text)
$debug_queues[Thread.current[:index]] << {
level: :debug,
body: text }
assuming mruby/c loops use `#puts` for print
debug on serial console,
mrubyc-debugger takes it over to print on Curses
window
Page: 72
mrubyc-debugger.gem - Curses
include Curses
debug = $debug_queues[i].pop # took over by Refinements
wins[i][:out].addstr " #{debug[:level]} " + debug[:body]
event = $event_queues[i].pop # event info by TracePoint
(1..(wins[i][:src].maxy - 2)).each do |y|
wins[i][:src].setpos(y, 1)
if !@srcs[i][y]
wins[i][:src].addstr ' ' * wins[i][:src].maxx
else
# hilighten current line
wins[i][:src].attron(A_REVERSE) if y == event[:lineno]
end
end
vars = {}
event[:tp_binding].local_variables.each do |var|
vars[var] = event[:tp_binding].local_variable_get(var).inspect
end
Page: 74
mrubyc-debugger.gem - Binding
binding.local_variables
# => [:var_a, :var_b, ...]
binding.local_variable_get(:var_a)
# => "foo"
binding.local_variable_set(:var_a, "bar")
binding.local_variable_get(:var_a)
# => "bar"
Page: 76
summary
mrubyc-test is the first testing tool for mruby/c. it
means mruby/c started to have its ecosystem
Page: 77
summary
mrubyc-test is the first testing tool for mruby/c. it
means mruby/c started to have its ecosystem
even if Matz hates test
Page: 78
summary
mrubyc-test is the first testing tool for mruby/c. it
means mruby/c started to have its ecosystem
even if Matz hates test
mrubyc-debugger is a visualization tool of
concurrent mruby/c loop tasks powered by CRuby's
Thread
Page: 79
summary
mrubyc-test is the first testing tool for mruby/c. it
means mruby/c started to have its ecosystem
even if Matz hates test
mrubyc-debugger is a visualization tool of
concurrent mruby/c loop tasks powered by CRuby's
Thread
no matter what Matz regrets
Page: 80
summary
at a glance, developing with mruby/c seems to be
very restricted due to lack of dynamic features
Page: 81
summary
at a glance, developing with mruby/c seems to be
very restricted due to lack of dynamic features
however, it will be more effective by using the
power of CRuby and our own tools
Page: 82
summary
at a glance, developing with mruby/c seems to be
very restricted due to lack of dynamic features
however, it will be more effective by using the
power of CRuby and our own tools
above all, Rubyish-terminal-based development is
fun!
Page: 83
me
HASUMI Hitoshi
@hasumikin
Monstar Lab, inc.
Shimane office
Sake 🍶
Soba 🍜
Coffee ☕