Practical mruby/c firmware development with CRuby

: author

HASUMI Hitoshi @hasumikin

: content-source

RubyKaigi 2019

: date

April 19, 2019

: allotted-time

38m

: place

Fukuoka International Congress Center

: theme

theme

Sake IoT project

# image
# src = images/kamos.jpg
# relative_height = 100

Sake IoT project

# image
# src = images/collage01.jpg
# relative_height = 100

what is mruby/c?

* github.com/mrubyc/mrubyc
* one of the mruby family
* `/c` symbolizes compact,\nconcurrent and capability
* especially dedicated to\none-chip microcontroller
# image
# src = images/psoc5lp_chip.jpg
# align = right
# relative_height = 70

mruby and mruby/c

# RT

mruby, mruby/c

v1.0.0 in Jan 2014, v1.0 in Jan 2017
for general embedded software, for one-chip microcontroller
RAM < 400KB, RAM < 40KB

((both)) mruby and mruby/c

* bytecodes are compiled by `mrbc`\nvirtual machine (VM) executes the bytecode
# image
# src = images/mruby_and_mrubyc.png
# align = center
# relative-height = 100

bytecode

* a kind of intermediate representation
* virtual machine dynamically interprets the bytecode and processes the program
# image
# src = images/bytecode.png
# align = center
# relative-width = 100

mruby on microcontroller

* RTOS (Real-Time OS) manages mruby VMs. RTOS has features like multi tasking, etc.
# image
# src = images/mruby_and_mrubyc-mruby.png
# align = center
# relative-height = 100

mruby/c on microcontroller

* mruby/c has its own mechanism to manage the runtime: ((*rrt0*))
# image
# src = images/mruby_and_mrubyc-mrubyc.png
# align = center
# relative-height = 100

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

how ((less))?

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

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 😞

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

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

((* *))

(('tag:center'))nnn (('tag:xx-large:So'))nn (('tag:xx-large:というわけで'))

プロパティ

: hide-title

true

((* *))

(('tag:center'))nnn (('tag:xx-large:Today's agenda'))nn (('tag:x-large:きょうはこんな話をします'))

プロパティ

: hide-title

true

((* *))

(('tag:center'))nnn (('tag:xx-large:Little more Rubyish')) nn (('tag:x-large:もうちょいRubyっぽくやろう'))

プロパティ

: hide-title

true

matsue.rb

# image
# src = images/松江城_Matsue.rb.16x9.jpg
# relative_width = 110
# relative_margin_top = -3

prop

: hide-title

true

mruby/c firmware is made up of three parts

* 1) peripheral API wrapper (C)
* 2) business logic (mruby)
* 3) infinite loop (mruby)
# image
# src = images/three_parts.png
# relative_width = 100

mruby/c firmware is made up of three parts

* 1) peripheral API wrapper (C)
* 2) business logic (mruby) - ((*model*))
* 3) infinite loop (mruby) - ((*controller*))
# image
# src = images/three_parts.png
# relative_width = 100

things make situation difficult

* peripheral API needs ((*real*)) hardware
* business logic needs peripheral APIs ((*really*)) work
* infinite loop needs ((*real*)) data from business logic
# image
# src = images/three_parts.png
# relative_width = 100

mruby/c firmware is made up of three parts

* 1) ((*peripheral API wrapper (C)*))
* 2) business logic (mruby)
* 3) infinite loop (mruby)
# image
# src = images/three_parts_peripheral.png
# relative_width = 100

peripheral API wapper

* https://rubykaigi.org/2018
# image
# src = images/rubykaigi2018.png
# relative_width = 100

mruby/c firmware is made up of three parts

* 1) peripheral API (C)
* 2) ((*business logic (mruby)*))
* 3) infinite loop (mruby)
# image
# src = images/three_parts_model.png
# relative_width = 100

mruby/c firmware is made up of three parts

# image
# src = images/three_sources_0.png
# align = center
# relative_height = 100

mruby/c firmware is made up of three parts

# image
# src = images/three_sources_1.png
# align = center
# relative_height = 100

mruby/c firmware is made up of three parts

# image
# src = images/three_sources_2.png
# align = center
# relative_height = 100

mruby/c firmware is made up of three parts

# image
# src = images/three_sources_3.png
# align = center
# relative_height = 100

mruby/c firmware is made up of three parts

# image
# src = images/three_sources_4.png
# align = center
# relative_height = 100

by the way,

# image
# src = images/three_sources_2.png
# align = center
# relative_height = 100

fuga?

# image
# src = images/three_sources_2_fuga_1.png
# align = center
# relative_height = 100

what is fuga?

# image
# src = images/three_sources_2_fuga_4.png
# align = center
# relative_height = 100

will calling fuga raise error?

# image
# src = images/three_sources_2_fuga_1.png
# align = center
# relative_height = 100

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?

((* *))

(('tag:center'))nnnn(('tag:xx-large:Stub'))

プロパティ

: hide-title

true

((* *))

(('tag:center'))nnnn(('tag:xx-large:Mock'))

プロパティ

: hide-title

true

((* *))

(('tag:center'))nn (('tag:xx-large:Test Driven'))n (('tag:xx-large:Development for'))n (('tag:xx-large:Embedded Ruby'))

プロパティ

: hide-title

true

(DEMO)

(('tag:center'))nnnngithub.com/hasumikin/mrubyc-test

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

so, why did I use mruby/c?

(('tag:center'))nnnn(('tag:x-large:so, why did I use mruby/c?'))

プロパティ

: hide-title

true

so, why did I use mruby/c?

(('tag:center'))nnnn((*(('tag:xx-large:DESTINO - 運命'))*))

プロパティ

: hide-title

true

((* *))

(('tag:center'))nnn(('tag:x-large:Anyway, I started to create')) n(('tag:x-large:mrubyc-test.gem'))

プロパティ

: hide-title

true

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

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*))

mrubyc-test.gem - stub

# enscript ruby
# 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

mrubyc-test.gem - mock

# enscript ruby
# 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

it was my personal tool

(('tag:center'))nnnngithub.com/hasumikin/mrubyc-test

but already abandoned because

(('tag:center'))nnnn(('del:github.com/hasumikin/mrubyc-test'))

now it's official 🎉

(('tag:center'))nnnn(('tag:large:github.com/'))((*(('tag:large:mrubyc'))*))(('tag:large:/mrubyc-test'))

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

mrubyc-test.gem - internal

* the gist is creating ((*test.rb*)) by `test code generator` implemented in CRuby
# image
# src = images/how-mrubyc-test-works.png
# align = center
# relative-width = 100

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

mrubyc-test.gem - Module#method_added

# enscript ruby
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
    }

mrubyc-test.gem

# enscript ruby
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"
    }

mrubyc-test.gem - BasicObject#method_missing

# enscript ruby
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
      )

mrubyc-test.gem - generated stub method

# enscript ruby
# part of test.rb
class Sample
  def still_not_defined_method
    ", it must be Ruby"
  end
end

mrubyc-test.gem - template of stub

# enscript ruby
<% 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 -%>

宍道湖

# image
# src = images/shinjiko.jpg
# relative_width = 110
# relative_margin_top = -3

prop

: hide-title

true

mruby/c firmware is made up of three parts

* 1) peripheral API (C)
* 2) business logic (mruby)
* 3) ((*infinite loop (mruby)*))
# image
# src = images/three_parts_loop.png
# relative_width = 100

mruby/c firmware is made up of three parts

# image
# src = images/three_sources_4.png
# align = center
# relative_height = 100

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
# image
# src = images/loops.png
# align = center
# relative_width = 100

the loops of mruby/c are

* user space threads managed by mruby/c's runtime
# enscript c
/* 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();
}

threads of CRuby

* correspond to native threads (with GVL)
# enscript ruby
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

(DEMO)

(('tag:center'))nnnngithub.com/hasumikin/mrubyc-debugger

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 ...

((* *))

(('tag:center'))nnnn(('tag:xx-large:TracePoint'))

プロパティ

: hide-title

true

mrubyc-debugger.gem - TracePoint

# enscript ruby

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

((* *))

(('tag:center'))nnnn(('tag:xx-large:Refinements'))

プロパティ

: hide-title

true

mrubyc-debugger.gem - Refinements

# enscript ruby
module DebugQueue
  refine Kernel do
    def puts(text)
      $debug_queues[Thread.current[:index]] << {
        level: :debug,
        body: text }

((* *))

(('tag:center'))nnnn(('tag:xx-large:Curses'))

プロパティ

: hide-title

true

mrubyc-debugger.gem - Curses

# enscript ruby

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

((* *))

(('tag:center'))nnnn(('tag:xx-large:Binding'))

プロパティ

: hide-title

true

mrubyc-debugger.gem - Binding

# enscript ruby
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"

summary

summary

* mrubyc-test is the first testing tool for mruby/c. it means mruby/c started to have its ecosystem

summary

* mrubyc-test is the first testing tool for mruby/c. it means mruby/c started to have its ecosystem\neven if Matz hates test

summary

* mrubyc-test is the first testing tool for mruby/c. it means mruby/c started to have its ecosystem\neven if Matz hates test
* mrubyc-debugger is a visualization tool of concurrent mruby/c loop tasks powered by CRuby's Thread

summary

* mrubyc-test is the first testing tool for mruby/c. it means mruby/c started to have its ecosystem\neven if Matz hates test
* mrubyc-debugger is a visualization tool of concurrent mruby/c loop tasks powered by CRuby's Thread\nno matter what Matz regrets

summary

* at a glance, developing with mruby/c seems to be very restricted due to lack of dynamic features

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

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!

me

* HASUMI Hitoshi\n@hasumikin
* Monstar Lab, inc.\nShimane office
* Sake 🍶\nSoba 🍜\nCoffee ☕
# image
# src = images/hasumi.jpg
# align = right
# relative-height = 90

thank you!

(('tag:center'))nnnn(('tag:xx-large:Thank you!'))

プロパティ

: hide-title

true